The moment you introduce encryption, a new responsibility appears: key management. Losing a key in an audit log is nothing like forgetting a database password. Lose the key and every past record protected by it becomes permanently unreadable. No escrow, no backdoor, no recovery path. This chapter covers the keyring versioning system and the two operational paths — key rotation and re-key.
Key rotation (adding a new key) is cheap and routine. Re-key (re-encrypting old → new after a leak) happens exactly once after a compromise — it's expensive and heavy. Don't confuse the two.
KeyRing: versioned key management
KeyRing tracks RSA key pairs and HMAC keys by version number. The version is a u32, with 0 reserved as a "no key" sentinel — so real key versions start at 1.
crates/quipu-core/src/crypto.rs — KeyVersion, KEYLESSpub type KeyVersion = u32;
pub const KEYLESS: KeyVersion = 0; // sentinel for SHA-256-protected field digests
pub struct KeyRing {
rsa: BTreeMap<KeyVersion, RsaPair>, // BTreeMap: versions in ascending order
macs: BTreeMap<KeyVersion, Vec<u8>>,
}
// active key = highest version: BTreeMap::keys().next_back()
Every new record is written with the highest version (the active key). Each stored record carries a key_version field that records which key was used to protect it. Reads look up that specific version from the keyring.
Rotation — cheap and routine
Regular key cycling (rotation) is basic security hygiene. In Quipu-Log, rotation is straightforward: add a new key version to the KeyRing. Existing records don't need to be touched.
quipu-server config.json — switching to a versioned key list"keys": {
"hmac_keys": [
{ "version": 1, "file": "/etc/quipu/hmac-v1.key" },
{ "version": 2, "file": "/etc/quipu/hmac-v2.key" } // new active key
],
"rsa_keys": [
{ "version": 1, "public_key_pem_file": "/etc/quipu/rsa-v1-pub.pem" },
{ "version": 2, "public_key_pem_file": "/etc/quipu/rsa-v2-pub.pem" }
]
}
After a server restart, v2 becomes the active key. New records are encrypted with v2. Old records made with v1 are still readable because v1 remains in the keyring. Simple.
This is exactly why records carry a key_version field. The record itself tells you "this is which key was used when I was created," so the keyring can automatically select the right key even when multiple versions are loaded. Without versioning, there'd be no way to answer "which key made this?" and rotation would be impossible.
Re-key — the emergency response after a leak
Unlike routine rotation, re-key is the response to a key compromise. A leaked RSA private key can decrypt every ciphertext currently stored. Stopping that requires re-encrypting stored records with a new key.
| Rotation | Re-key | |
|---|---|---|
| When | regularly (quarterly, annually, etc.) | after a key compromise |
| What it does | adds a new key version | re-encrypts the entire registry |
| Cost | cheap (config change only) | expensive (read + write every record) |
| Downtime | none (restart only) | yes (offline, standalone run) |
| Result | only new records use the new key | all records use the new key |
The re-key pass is run with quipu-server rekey config.json (or the embedded AuditStore::rekey()). It is offline-only — the server must be down (the store lock prevents concurrent execution).
How RSA re-key actually works — rewrap
Re-keying an RSA field does not mean re-encrypting all the data from scratch. The AES ciphertext and nonce stay exactly as they are; only the RSA wrapping around the DEK (the AES key) is replaced. That's what KeyRing::rewrap() does.
crates/quipu-core/src/crypto.rs — KeyRing::rewrappub fn rewrap(&self, stored: &StoredValue) -> Result<StoredValue> {
// 1. Decrypt the DEK using the old version's private key (unwrap)
let dek = old.decrypt(Oaep::new::<Sha256>(), &b64::decode(wrapped_key))?;
// 2. Re-encrypt the DEK with the active version's public key (re-wrap)
let wrapped = self.rsa_public_of(active)?.encrypt(&mut rand::thread_rng(),
Oaep::new::<Sha256>(), &dek)?;
// nonce and ciphertext are untouched — the DEK itself is unchanged, payload integrity preserved
Ok(StoredValue::Rsa { key_version: active, wrapped_key: b64::encode(&wrapped),
nonce: nonce.clone(), ciphertext: ciphertext.clone() })
}
Because the AES ciphertext is never touched, a power loss mid-re-key cannot corrupt the payload. Only the re-wrapping of the DEK fails; any record that was already re-wrapped is immediately readable with the new key.
The integrity problem with re-key — RekeyEvent
Re-key rewrites the entire registry table. From the Merkle tree's perspective, that is indistinguishable from tampering — the whole tree changes. The mechanism that separates "legitimate rewrite" from "unauthorized tampering" is RekeyEvent.
crates/quipu-core/src/store.rs — RekeyEventpub struct RekeyEvent {
pub occurred_at: u64,
pub rsa_version: KeyVersion,
pub hmac_version: KeyVersion,
pub tables: Vec<RekeyedTable>, // per-table old_root → new_root transition
pub signing_key_version: KeyVersion,
pub signature: Vec<u8>, // RSA signature: proves the rewrite was audited
}
When the re-key pass finishes, this event is written to the meta table. verify_integrity() validates the signature and checks that each registry's current tree root is consistent with the signed new_root (a consistency proof). "Audited rewrite" and "silent tampering" are distinguishable.
HMAC fields cannot be re-keyed
There's one important constraint. Fields protected with HMAC or SHA-256 cannot be re-keyed. They are one-way — there is no path from a digest back to the original plaintext, and without the plaintext, a new digest under the new key cannot be computed.
If an HMAC key is leaked, every digest recorded before the leak is permanently exposed. An attacker who holds the key and the digests can mount a dictionary attack. HMAC keys must be guarded extremely carefully, and regular rotation is the only way to limit the damage window after a potential compromise. There is no safety net of "we can fix it if it leaks."
Key loss = permanent data loss
Quipu-Log has no key escrow, no master key, no recovery backdoor. This is a deliberate design choice — backdoors were eliminated to defend against a wider threat model that includes insider threats. The price is that losing an RSA private key means every record protected by that key is permanently undecryptable.
Do not back up keys in the same location as the store. If an attacker seizes a store backup and finds a key backup in the same place, the encryption protection collapses completely. Back up keys on a separate, independent path from the store, and consider using an HSM (hardware security module) or KMS.
Signing key isolation
The RSA private key used for checkpoint signing should be kept separate from the key used for data encryption. If the signing key sits on the data node (the server running the store), an insider with disk access can use the signing key to forge checkpoints. Moving the signing key to a separate host or HSM closes that attack path.
Recap
- KeyRing: manages RSA + HMAC keys by version number (u32). Highest version = active key, lower versions = for reading older records.
- Rotation (routine): add a new key version only. Existing records untouched. Cheap and zero-downtime.
- Re-key (after a compromise): re-wrap the DEK to re-protect RSA fields. Offline, expensive. Audited via RekeyEvent.
- HMAC fields cannot be re-keyed — one-way functions mean no path back to plaintext. A leaked HMAC key permanently exposes existing digests.
- Key loss = unrecoverable — no escrow. Key backups must be kept separate from the store.
① Explain the difference between key rotation and re-key across three dimensions: when, what it does, and cost.
② Explain the mathematical reason why HMAC-protected fields cannot be re-keyed.
③ Why is it dangerous to store the RSA private key backup in the same location as the store backup?