Part IV — Engineering Practice · Chapter 30
Key Management
Key Management
A key stored in a file is a key waiting to be leaked. A key stored in hardware is a key waiting to be used.
ℹPrerequisites▼
Before reading this chapter, you should be comfortable with: Chapters 6, 16 (Signatures, Procedural Primitives). Key management governs the signing keys used in emit_dsse() — you need to understand what is being signed.
Every Canon attestation carries a seal. The seal contains three things: a fingerprint that identifies a public key, a URL where that public key lives, and a signature over the attestation's chain hash. The seal is only as trustworthy as the key lifecycle that produced it. If the private key was generated carelessly, stored recklessly, or never rotated, then every attestation the key touched is potentially compromised — regardless of how correct the cryptography is.
This chapter is about the part of the system that is not cryptography. Key management is plumbing — and it is where most production cryptographic systems fail. The math is almost never wrong; the operational practices routinely are.
At a glance
- Canon uses Ed25519 keypairs. The private key signs attestations and lives in the macOS Keychain (or a platform equivalent). The public key is published at a stable URL; its SHA-256 fingerprint is embedded in every seal.
- Rotation is proactive: generate a new keypair, sign a rotation attestation with the old key linking old fingerprint to new, retire the old key. Prior attestations remain valid against their declared fingerprint.
- Canon v0.1.1 has rotation but not formal revocation. This chapter is honest about that gap.
Learning objectives
By the end of this chapter you should be able to:
- Generate an Ed25519 keypair using
meridian-canon keygenand explain what each line of output means. - Explain why the Keychain is a better storage location than a PEM file on disk, and what threat model each protects against. - Execute a key rotation, verify that prior attestations still pass the seven-step protocol under the old fingerprint, and verify that new attestations use the new fingerprint. - Articulate the difference between rotation and revocation, and identify which threat model each addresses. - Configure thekeyrings.altfile backend for headless Linux or CI environments, and explain why this backend is not appropriate for production key storage. ## The key as lock and lookup Start with the metaphor. A house key does two things: it opens the lock (the private function), and its shape defines what the lock accepts (the public function). Anyone who has the key can open the door. Anyone who has the lock can confirm whether a given key fits. The private key is the physical key; the public key is the lock's shape, publishable freely without enabling unauthorized entry. Canon uses this asymmetry for attestation. The private key is used exactly once per attestation: to sign the chain hash. The public key is used by every recipient who wants to verify the signature. The verifier never needs the private key. The verifier only needs the public key — and the fingerprint that confirms the public key has not been substituted since the attestation was issued. The precise definition: an Ed25519 keypair consists of a 32-byte private scalar and a 32-byte public point on the Edwards25519 curve (RFC 8032 §5.1). The private scalar is generated from a cryptographically secure random source; the public point is deterministically derived from it. The signature operation isEd25519Sign(private_key, message_bytes), producing a 64-byte signature. The verification isEd25519Verify(public_key, message_bytes, signature), returning a boolean. In Canon, themessage_bytesare the UTF-8 encoding of the chain hash string, inclusive of thesha256:prefix. That detail matters — a verifier that strips the prefix before verifying will produce a mismatch. See Chapter 6 for why signing the hash string (rather than signing the raw content) is the correct construction.
Generation
Key generation is minting a coin. The coin has two faces: the private face (yours alone) and the public face (the world's). The mint runs once. The result is permanent until you deliberately retire it.
In Meridian-Cannon, key generation is a single CLI command:
$ meridian-canon keygen --custodian=white-dossier-2026
Custodian: white-dossier-2026
Fingerprint: sha256:a3f8b1c9...
Public PEM: /Users/pat/.meridian/keys/sha256_a3f8b1c9....pem
Private key: Keychain (service=meridian-canon, account=white-dossier-2026)
The custodian name is an arbitrary identifier — it becomes the Keychain account name under which the private key is stored. The fingerprint is the SHA-256 hash of the PEM bytes of the public key; it appears in every attestation's seal. The public PEM is written to ~/.meridian/keys/ for publication. The private key is written to the Keychain only. The implementation in meridian/canon/keys.py:
def keygen(custodian: str) -> tuple[Ed25519PrivateKey, Ed25519PublicKey, str]:
private_key, public_key = signing.generate_keypair()
pem_public = signing.public_key_to_pem(public_key)
fingerprint = public_key_fingerprint(pem_public)
pem_private = signing.private_key_to_pem(private_key)
keyring.set_password(KEYRING_SERVICE, custodian, pem_private.decode("ascii"))
pem_dir = _ensure_dir()
pem_path = pem_dir / f"{fingerprint.replace(':', '_')}.pem"
pem_path.write_bytes(pem_public)
return private_key, public_key, fingerprint
Three operations: generate the keypair, store the private key in Keychain, write the public PEM to disk. None requires a network connection. The private key bytes never touch a file.
◆ Going Deeper — Ed25519 and RFC 8410.
The Python
cryptographylibrary serializes Ed25519 public keys to SubjectPublicKeyInfo format (RFC 5480 + RFC 8410). The Ed25519 OID is1.3.101.112. A SubjectPublicKeyInfo DER blob for a 32-byte Ed25519 public key is 44 bytes: a 12-byte header (sequence, algorithm identifier, OID, bit-string wrapper) followed by the 32 key bytes. PEM wraps these 44 bytes in base64, producing 60 base64 characters between the-----BEGIN PUBLIC KEY-----/-----END PUBLIC KEY-----delimiters. > > The OID lets a PEM parser identify the algorithm without out-of-band signaling. The key's permanent name —1.3.101.112— is distinct from its fingerprint, which is the SHA-256 of the entire PEM block. ## Storage: why the Keychain A.pemfile on disk is a secret that behaves like an ordinary file: it can begit add'd accidentally, backed up to iCloud, synced to a cloud service, or exfiltrated by any process running as the same user. macOS applies no special protection to files ending in.pem. The macOS Keychain is different. Keychain entries are stored in an encrypted database maintained by the Security framework. Access requires the user's login credential or Touch ID. On Apple Silicon devices, Keychain entries backed by the Secure Enclave cannot be exported at all — the key material exists only inside the chip. Thekeyringlibrary writes to the login keychain, which is hardware-backed on modern hardware and login-password-protected otherwise. The threat model this addresses: an attacker who gains read access to the filesystem — via a compromised cloud backup, a stolen disk image, a misconfigured S3 bucket holding dotfiles, or a malicious package — cannot exfiltrate a Keychain-stored private key. They can read the public PEM file, but the public PEM is already public; that exfiltration teaches them nothing. The threat model this does not address: an attacker running code in the same login session with Keychain access, or an attacker with the user's login credential. The Keychain is a software boundary for offline attackers. A production system whose attestations carry legal weight should use a dedicated HSM for higher assurance. Canon does not require this, but the migration path is straightforward: replacekeyring.set_passwordwith an HSM API; the rest of the pipeline is unchanged. ### Cross-platform keyring backends Thekeyringlibrary selects a backend automatically based on the running platform. The v0.2.0 documentation makes this platform map explicit: | Platform | Backend | Notes | |---|---|---| | macOS | Keychain | Default; no configuration needed | | Windows | Windows Credential Manager | Default; no configuration needed | | Linux GUI | SecretService (GNOME Keyring / KWallet) | Default on desktop Linux | | Linux headless / CI / Docker |keyrings.altfile backend | Requires manual configuration | The headless case requires explicit setup because the default SecretService backend cannot run without a D-Bus session. Install the alternative backend and configure it before running anykeygenoremitoperation:
pip install keyrings.alt>=5.0
export PYTHON_KEYRING_BACKEND=keyrings.alt.file.PlaintextKeyring
In a CI test suite, set this in conftest.py so it applies automatically:
import os
os.environ.setdefault("PYTHON_KEYRING_BACKEND", "keyrings.alt.file.PlaintextKeyring")
The file backend stores private keys in
~/.local/share/python_keyring/ (or the path set by PYTHON_KEYRING_BACKEND_FILE_PATH). The key material is plaintext on disk. This is appropriate for ephemeral CI VMs where the disk is discarded after the job completes; it is not appropriate for production key storage. CI keys should come from encrypted secrets (GitHub Actions encrypted secrets, etc.) injected into the environment, never committed to the repository. > § For the Record — Never commit private keys. > > The headless keyring stores keys in a file path that could accidentally > enter source control. Add ~/.local/share/python_keyring/ and any path > set by PYTHON_KEYRING_BACKEND_FILE_PATH to the repository's > .gitignore. The public PEM in ~/.meridian/keys/ is designed to be > published; the keyring file is not. > ☉ In the Wild — The Verkada breach (2021). > > In March 2021, attackers compromised more than 150,000 security cameras operated by hospitals, prisons, schools, and a Tesla factory. The entry point was a "super admin" credential stored in an internal cloud environment with insufficient access controls — not in a secrets manager, not in hardware. It was accessible to anyone who reached the right network path. > > The failure mode is not exotic. It is the ordinary failure mode of credentials stored as files: they end up in the wrong place. A private key stored in the Secure Enclave cannot be exported to a text file. A private key stored as a .pem file on a developer's laptop is three cp commands away from the same outcome.
Publication: the public key URL
Every attestation's seal declares:
"seal": {
"public_key_url": "https://raw.githubusercontent.com/user/repo/main/keys/white-2026.pem",
"public_key_fingerprint": "sha256:a3f8b1c9...",
...
}
The URL tells the verifier where to fetch the public key. The fingerprint tells the verifier what the public key's SHA-256 must be after fetching. The verifier fetches, hashes, and compares; a mismatch fails step 1 of the seven-step protocol (Chapter 25).
This design tolerates a compromised URL. If an attacker replaces the PEM file at the URL, the fingerprint check fails. The attacker cannot forge new attestations against the old fingerprint — that requires the old private key. They cannot fool the verifier into accepting a substituted fingerprint — the fingerprint is signed inside the seal itself.
What the design requires: the URL must be stable for the life of the attestation. For long-term archival evidence — records that must remain verifiable for a decade — use a durable URL: a committed and tagged file on a major forge, an IPFS content-addressed URL, or a self-hosted endpoint with a written availability policy.
§ For the Record — NIST SP 800-57 Part 1 Rev. 5, §5.3.
"Cryptographic key management encompasses the generation, storage, distribution, use, retirement, revocation, and destruction of keys. All of these operations must be performed carefully to ensure that the keys are protected throughout their entire lifecycle."
Canon v0.1.1 covers: generation (
keygen), storage (Keychain), distribution (public PEM at stable URL), use (emit+ seal), and rotation (rotate-key). Formal revocation and destruction are noted as future work in spec §10.4. ## Rotation Key rotation is the controlled, proactive replacement of a keypair before it is compromised. Rekeying a lock: you change the lock on schedule, before anyone breaks it; you cut new keys; you recall the old ones; you keep a record linking old to new. In Canon, rotation works as follows: 1. Runmeridian-canon rotate-key --custodian=white-dossier-2026. The command removes the old private key from Keychain, then generates a new one under the same custodian name. 2. Publish the new public PEM at a URL. The old PEM remains at its original URL for as long as prior attestations may need verification. 3. Create a rotation attestation — a Canon attestation whose subject is the key rotation event, whose witness records both the old and new fingerprints, and which is signed by the old key before it is retired. After rotation, all new attestations use the new keypair. All prior attestations remain verifiable against the old fingerprint. Therotate-keyimplementation incli.py:
def _cmd_rotate(args: argparse.Namespace) -> int:
keys.revoke(args.custodian)
return _cmd_keygen(args)
The rotation attestation is a separate, explicit step — rotate-key is a key-management primitive, not an attestation primitive. Build and verify the rotation attestation with the emit pipeline before calling rotate-key. > ▼ Why It Matters — Sequence is irreversible. > > rotate-key destroys the old private key as its first operation. Once the old key is gone, no attestation can be signed "by the old key" — the rotation attestation that proves continuity must be sealed before you call rotate-key. The correct sequence is: (1) emit the rotation attestation using the old custodian; (2) verify it passes meridian-canon verify; (3) call rotate-key. Reversing steps 1 and 3 is a permanent error — there is no recovery if the rotation attestation was never sealed. > ◆ Going Deeper — Rotation vs. revocation. > > Rotation is proactive: you replace a key you believe is still secure, on a schedule. Revocation is reactive: you invalidate a key because it has been compromised or lost. > > With rotation, recipients verify old attestations against the old key and new attestations against the new key. No one needs to take immediate action. With revocation, recipients holding attestations from the compromised key must decide whether those attestations are trustworthy — an attacker may have been able to sign forged ones during the compromise window. Revocation without a CRL or OCSP endpoint requires out-of-band communication to reach every verifier. > > Canon v0.1.1 has rotation but not formal revocation. For litigation: if your private key is compromised, disclose the compromise to every party who received attestations from that key. The rotation attestation is the record you produce; the disclosure is the act you perform. ## The running case: mid-proceeding rotation The running case involves evidence sealed under one keypair across six months of a TPR proceeding. In month four, the parent replaces their laptop and cannot transfer the Keychain entry. They generate a new keypair. The attorney's verifier encounters two different fingerprints across the attestation set. Pre-rotation attestations verify against the old fingerprint; post-rotation against the new. The rotation attestation, signed by the old key before the laptop was retired, explains the transition. At the evidentiary hearing, opposing counsel challenges: "The fingerprints don't match." The expert: "There is a rotation attestation, signed by the old key, linking the two fingerprints. Here is what it says and when it was issued." > ▼ Why It Matters — Key provenance in court. > > A party challenging the authenticity of a Canon-sealed exhibit may argue that the key was generated after the fact — that evidence was manufactured, sealed retroactively, and presented as contemporaneous. Two things defeat that argument. First, the issued_at timestamp is inside the signed seal; altering it breaks the signature. Second, the public key URL is subject to forensic analysis: when was the PEM first committed to the repository? The key's publication history is an independent third-party record. ## Working example Running meridian-canon keygen --custodian=white-dossier-2026 traverses: 1. cli.py:_cmd_keygen calls keys.keygen("white-dossier-2026"). 2. keys.keygen calls signing.generate_keypair() → Ed25519PrivateKey.generate() (OpenSSL's CSRNG). 3. Both keys are serialized to PEM. 4. hashing.public_key_fingerprint(pem_public) computes "sha256:" + sha256_hex(pem_public). 5. keyring.set_password("meridian-canon", "white-dossier-2026", pem_private_str) writes to the macOS Keychain. 6. The public PEM is written to ~/.meridian/keys/<fingerprint>.pem. 7. The CLI prints the custodian, fingerprint, and PEM path. The private key bytes are never printed. To retrieve the private key later, emit.py calls keys.load_private(custodian) → keyring.get_password("meridian-canon", "white-dossier-2026"). No file I/O; no disk artifact containing the private key material.
keyrings.alt.file.PlaintextKeyring for headless CI and Docker environments. - Private keys live in the keyring (hardware-backed where available, never written to disk as files) while public PEMs live on disk at ~/.meridian/keys/ for publication — the two storage locations reflect two different threat models: exfiltration vs. substitution. - revoke() (called internally by rotate-key) removes the private key from the keyring, ending its ability to sign new attestations; the public PEM at its stable URL is deliberately preserved so that pre-revocation attestations can still be verified against the declared fingerprint. - The custodian name must remain stable across key rotations because it is the Keychain account name and the institutional identifier linking old and new fingerprints in the rotation attestation — a personal name that changes with personnel turnover breaks this continuity. - The conftest.py pattern sets PYTHON_KEYRING_BACKEND=keyrings.alt.file.PlaintextKeyring via os.environ.setdefault() so that all tests in a CI suite use the file backend without a D-Bus session, making keyring-dependent tests portable across headless runners. meridian-canon keygen --custodian=test-key. Open the generated .pem file. The first four base64 characters of any Ed25519 SubjectPublicKeyInfo PEM are MCow — the base64 encoding of the DER sequence header 0x30 0x2a 0x30. The OID bytes 0x06 0x03 0x2b 0x65 0x70 encode 1.3.101.112 in DER. Once you can read the header by eye, a PEM file is a parseable record, not a magic blob. ## Exercises ### Warm-up 1. Run meridian-canon keygen --custodian=<yourname>. Open Keychain Access on macOS. Find the entry under service meridian-canon. Confirm the account name matches your custodian string. 2. Open the generated public PEM in a text editor. Base64-decode the body. Confirm the decoded length is 44 bytes. Find the 5-byte OID sequence 06 03 2b 65 70 in the hex dump. ### Core 3. Run meridian-canon rotate-key --custodian=<yourname>. Confirm in Keychain Access that the old entry is gone and a new entry exists. Check ~/.meridian/keys/ and confirm both PEM files are present. 4. Seal a minimal attestation with the new keypair. Verify with meridian-canon verify. Replace the public_key_fingerprint in the seal with the old fingerprint. Verify again. Identify which step fails and explain why. 5. Write a Python function verify_rotation_chain(old_fp, new_fp, rotation_attestation) that confirms: (a) the attestation was signed by the old key, (b) it references both fingerprints, (c) its issued_at is later than any prior attestation using the old fingerprint in a given set. ### Stretch 6. Configure key storage for a headless Linux CI environment using the keyrings.alt file backend. Set PYTHON_KEYRING_BACKEND and confirm that keys.keygen and keys.load_private work correctly. Then repeat the exercise with the SecretService backend on a desktop Linux instance. Document which backend is appropriate for each deployment context. 7. The current rotate-key destroys the old private key before generating the new one. Identify one failure mode this creates and implement a safer rotation using a temporary custodian name with an atomic rename only after the new key is confirmed stored. ## Lab 23 — Full rotation cycle with a live attestation The lab is in labs/ch23_key_management/. ### Lab 23.1 — Generate, publish, attest 1. Generate a keypair with a test custodian name. 2. Copy the public PEM to a location accessible via a file:// URL. 3. Use emit.py to build and seal a minimal ObservationAttestation. 4. Run meridian-canon verify. All seven steps should pass. ### Lab 23.2 — Rotate and verify backward compatibility Sequence warning: read before running any command. Step 3 below calls rotate-key, which destroys the old private key immediately. Steps 1 and 2 must be completed and verified before you reach step 3. If you run step 3 first, the rotation attestation can never be sealed with the old key and the continuity record is permanently lost. 1. Seal a rotation attestation using the OLD key. Build it with the emit pipeline, using your current test custodian. The attestation must include old_fingerprint, new_fingerprint, rotated_at, and reason fields in its witness or subject metadata. 2. Verify the rotation attestation passes meridian-canon verify. Do not proceed until exit code is 0 and all seven steps pass. This is your proof of continuity — it must exist and be valid before the old key is gone. 3. (ONLY after step 2 passes) Run meridian-canon rotate-key with your test custodian. The old private key is now destroyed. 4. Publish the new public PEM at a new URL. 5. Seal a second attestation with the new key. Verify both the original attestation (from Lab 23.1) and this new attestation pass the verifier — the original against its old URL, the new one against the new URL. ### Acceptance criteria - Both attestations verify with exit code 0. - The rotation attestation JSON exists and carries a valid seal. - ~/.meridian/keys/ contains both PEM files. ## Build-your-own prompt For your capstone matter: what is the right custodian naming scheme for your domain? If multiple attorneys each seal their own attestations, each issuer needs their own keypair and Keychain entry. Design the key-management policy before building the emission pipeline. Key management is architecture; treat it as such. ## Further reading - RFC 8032 — Edwards-Curve Digital Signature Algorithm (EdDSA) (IETF, 2017). - RFC 8410 — Algorithm Identifiers for Ed25519, Ed448, X25519, and X448 (IETF, 2018). - NIST SP 800-57 Part 1 Rev. 5 — Recommendation for Key Management (NIST, 2020). - NIST SP 800-131A Rev. 2 — Transitioning the Use of Cryptographic Algorithms and Key Lengths (NIST, 2019). - Bernstein, D.J. and Lange, T., SafeCurves, https://safecurves.cr.yp.to. - The dossier research/01_cryptography_pedagogy.md.
Next: Chapter 24 — Hash-Chained Audit Logging.