keypair
keypair
¶
SSH keypair generation, import, and export against the credential DB.
The DB is the canonical home for SSH keys. This module moves material in and out of that form:
generate_keypaircreates a fresh keypair in memory.import_ssh_keypairreads an existing OpenSSH keypair from files and registers it against a scope.export_ssh_keypairwrites a scope's key back to an OpenSSH file pair for handing to tools that cannot use the SSH agent.
Internally, private keys live in the DB as unencrypted PKCS#8 DER — the
single, opaque binary form the signer loads directly via
load_der_private_key. Import converts any supported inbound PEM to
that form at the boundary, and export re-armors it as OpenSSH PEM.
All flows share one vocabulary: the GeneratedKeypair dataclass is
the portable in-memory form, and fingerprint_of defines the
cross-call dedup key — the standard OpenSSH SHA256:<base64> fingerprint
of the SSH wire-format public blob.
DEFAULT_RSA_BITS = 3072
module-attribute
¶
Matches the ssh-keygen default as of OpenSSH 9.x.
PRIVATE_KEY_MODE = 384
module-attribute
¶
PUBLIC_KEY_MODE = 420
module-attribute
¶
GeneratedKeypair(key_type, private_der, public_blob, public_line, comment, fingerprint)
dataclass
¶
A keypair in the portable bytes form the vault stores.
private_der is unencrypted PKCS#8 DER — the raw-binary form we
persist and feed straight to the signer. The public half stays in its
usual SSH wire-format blob plus a pre-rendered public_line.
ImportResult(key_id, fingerprint, comment, already_present, scope_was_assigned)
dataclass
¶
Outcome of importing an OpenSSH keypair into the DB.
already_present reflects whether the key (by fingerprint) was
already in the ssh_keys table. scope_was_assigned reflects
whether the scope already owned a link to that key before this
call. The two combine into four honest call outcomes: minted +
linked, minted + re-linked (can't happen), re-used + linked (the
common "multi-scope share" path), and re-used + no-op.
ExportResult(key_id, fingerprint, private_path, public_path)
dataclass
¶
Paths written by export_ssh_keypair.
InfraKeypair(scope, private_pem, public_line, fingerprint, key_type, created)
dataclass
¶
A keypair backing a sandbox-reserved %scope infrastructure slot.
Returned by ensure_infra_keypair.
Carries the OpenSSH-PEM-serialised private (ready to ssh -i) and
the matching public-key line (ready to bake into an
authorized_keys file), so callers don't have to redo the
DER→PEM conversion or re-derive the public line from raw blobs.
created distinguishes "minted fresh in this call" from "loaded
from the existing assignment" — useful for surfacing first-use
diagnostics without a second DB roundtrip.
PasswordProtectedKeyError
¶
Bases: ValueError
Raised when an imported private key is encrypted with a passphrase.
KeypairMismatchError
¶
Bases: ValueError
Raised when provided public and private keys disagree.
generate_keypair(key_type, *, comment)
¶
Generate a fresh keypair entirely in memory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key_type
|
str
|
|
required |
comment
|
str
|
Comment to embed in the public line. Surfaces in
|
required |
Source code in src/terok_sandbox/vault/ssh/keypair.py
ensure_infra_keypair(scope, *, db, comment=None, key_type='ed25519')
¶
Load or generate the %scope infrastructure keypair.
The single place sandbox-internal callers go for the load-or-mint dance:
- If scope already has an assigned key, re-serialise it as OpenSSH PEM + render the public line and return.
- Otherwise mint a fresh keypair, persist it under scope with
assign_ssh_key(..., allow_infra=True), and return the same shape.
Only accepts %-prefixed scopes (the infrastructure form the
DB-layer safe-scope validator recognises) — user scopes go through
the normal ssh init / import_ssh_keypair
paths.
The load-or-mint sequence runs inside a single
db.transaction()
so two concurrent callers can't both observe "empty" and both
proceed to mint. Trust model: the returned private_pem is
plaintext key material; possession of an unlocked
CredentialDB is
already operator-equivalent in this design, so callers with a DB
handle can read any infra key. Callers MUST NOT log, serialise,
or otherwise persist private_pem outside the intended
consumer (e.g. ssh -i file or in-process signer). The keypair material is intended
for sandbox-owned services that need a stable host-side identity
(krun %host, future infrastructure slots); user-controlled
code never goes through this helper.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
scope
|
str
|
|
required |
db
|
CredentialDB
|
Open |
required |
comment
|
str | None
|
Comment to embed in the public line on fresh
generation. Ignored when the keypair already exists
(existing comment is preserved). Defaults to
|
None
|
key_type
|
str
|
|
'ed25519'
|
Returns:
| Type | Description |
|---|---|
InfraKeypair
|
An |
InfraKeypair
|
with the OpenSSH PEM private + public line. |
Source code in src/terok_sandbox/vault/ssh/keypair.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
import_ssh_keypair(db, scope, priv_path, pub_path=None, comment=None)
¶
Read a keypair from OpenSSH files and assign it to scope.
The public key is optional; when omitted it is derived from the private
key. When both are given they must match — fingerprint mismatch raises
KeypairMismatchError. Password-protected private keys raise
PasswordProtectedKeyError; the library stays diagnostic-only,
so callers own the remediation hint they render to the user.
Source code in src/terok_sandbox/vault/ssh/keypair.py
parse_openssh_keypair(priv_bytes, pub_bytes=None, *, comment_override=None)
¶
Parse raw OpenSSH bytes into the canonical GeneratedKeypair form.
Passphrase-protected keys raise PasswordProtectedKeyError.
Cryptography signals that condition with either TypeError ("Password
was not given but private key is encrypted" on older releases) or
ValueError (newer releases), depending on version — we catch both
and only translate when the message mentions encryption/password.
Malformed non-protected PEMs keep bubbling up as plain ValueError.
Source code in src/terok_sandbox/vault/ssh/keypair.py
export_ssh_keypair(db, scope, out_dir, key_id=None, out_name=None)
¶
Write a scope's key back out as a standard OpenSSH file pair.
The private bytes come out of the DB as PKCS#8 DER; this function
re-armors them as OpenSSH PEM — the same format ssh-keygen writes
and that ssh -i consumes. The directory is allowed to contain
unrelated files; only the output files are protected with O_EXCL
so nothing gets silently clobbered. Default filename stem:
id_<keytype>_<fp8> where fp8 is the first eight hex chars of
the raw SHA-256 digest of the public blob — stable and format-agnostic
for the user-facing fingerprint string.
Source code in src/terok_sandbox/vault/ssh/keypair.py
fingerprint_of(public_blob)
¶
Return the canonical OpenSSH fingerprint of public_blob.
Format matches what ssh-keygen -lf, ssh-add -l, GitHub's UI,
and gh ssh-key list all print: SHA256:<base64-unpadded> over
the raw SHA-256 digest of the SSH wire-format public blob.
Source code in src/terok_sandbox/vault/ssh/keypair.py
openssh_pem_of(private_der)
¶
Re-armor a stored PKCS#8 DER blob as OpenSSH PEM — the on-disk wire format.
This is what ssh-keygen writes and what ssh -i reads. Exposed for
the CLI export path and for test fixtures that need to round-trip a key
through the OpenSSH-file form.
Source code in src/terok_sandbox/vault/ssh/keypair.py
public_line_of(record)
¶
Render record as the one-line OpenSSH public key form.
Format: <algo> <base64-blob> <comment> — matches what
ssh-keygen writes to .pub files and what a remote's deploy-key
field expects. Callers that rendered this inline now go through this
single helper so the algo-name mapping lives in one place.