Skip to main content

Module keyring

Module keyring 

Source
Expand description

Envelope encryption, and the erasure it makes provable.

§Why deleting is not erasing

BlobStore::expire drops a payload’s bytes and leaves a tombstone, and the hash chain still verifies because it only ever committed to a digest. That is the right shape, and it is not enough: it erases the bytes in the live store. Every backup taken before the request, every replica, every snapshot an operator forgot about still holds them, and an erasure obligation is not discharged by deleting one copy of something that exists in six places.

Chasing the copies does not work either. Backups are the point of backups — they are offline, offsite, and frequently immutable by design, because that is what makes them survive the incident they exist for. A retention story that requires rewriting immutable backups has two guarantees in direct conflict, and whichever one loses, loses silently.

§What this does instead

Payload bytes are sealed under a data key, and the data key is wrapped by a key this crate never holds — a KMS, an HSM, whatever the deployment trusts. Erasure destroys the data key. Every copy of the ciphertext becomes unreadable at the same instant, including the ones nobody can reach, because the thing that was destroyed was never in them.

This is the standard answer to the immutable-log-versus-erasure problem, and it is the only one that survives contact with a backup regime.

Two operations fall out of the same structure, which is the argument for it:

  • Erasure — destroy a scope’s wrapping key. Every payload ever sealed under that scope is unreadable, everywhere, including the copies nobody can reach.
  • Revocation — the same act for a different reason. The blast radius a compromised key should have is exactly the scope it wrapped.

§Sealed bytes are rotation-immutable

A third operation is conspicuously absent, and its absence is a decision rather than unfinished work. Envelope encryption’s usual selling point is that a wrapping key rotates by re-wrapping data keys, leaving bulk data alone. That operation cannot exist here, and offering it would be a control that quietly does nothing:

An envelope carries its wrapped data key inline, and the journal’s hash chain commits to the envelope bytes — which is what lets an auditor holding no keys verify a run whose payloads have been erased. Re-wrapping a journal payload therefore rewrites a record the chain covers, so it breaks the chain it sits inside. Nor could re-wrapping the other stores buy the operational thing rotation is wanted for: an erasure scope’s journal payloads and its case state share one wrapping key, so a scope stays pinned to the oldest version any of its journal envelopes names, and no amount of re-wrapping case rows moves that floor.

So the rule is the other half of the choice, stated rather than discovered: sealed payload bytes never change, and the erasure scope is the rotation unit. A scope is already narrow — one case, one run, one memory version — so a compromised wrapping key exposes that unit and nothing else, which is the blast radius rotation is bought for. Adding a key version is safe and needs nothing from this crate: envelopes sealed before a rotation keep opening, which is what AWS KMS does by construction — it retains every prior version of a key’s material in perpetuity, resolves the right one from the ciphertext, and lets you delete only the whole key, which is erasure.

Retiring a version is the exposure, and only some services offer it. Vault’s transit engine does: min_decryption_version refuses ciphertext below a floor, so raising that floor past a live envelope makes un-erased history unreadable — an erasure nobody requested and no retention record explains. That is why KeyError::Retired exists as its own answer; see its documentation for why it must not be reported as loss.

§What is deliberately absent

No key material is generated by, or stored in, this crate beyond the process that is using it. KeyRing is a seam: a deployment points it at the thing that already holds its keys. There is a MemoryKeyRing for tests, and it lives behind the testkit feature rather than here: a key ring in the same process as the data it protects protects it from nobody, so the gate is the guarantee instead of a warning in a doc comment.

Re-exports§

pub use coordinator::ErasureCoordinator;
pub use coordinator::Lease;
pub use coordinator::LocalCoordinator;
pub use coordinator::UnderLock;
pub use coordinator::under_lock;

Modules§

coordinator
The lifecycle lock cryptographic memory erasure needs, and who can hold it.

Structs§

DataKey
A key that seals payload bytes.
EncryptedBlobs
A BlobStore that seals payloads under a scope’s data key.
EncryptedMemoryStore
A memory store whose content is unreadable once its items’ keys are destroyed.
Erasure
What a cryptographic erasure did: the key, then the live ciphertext.
SealedCases
A CaseStore that seals case state under a key ring.
SealedEvents
An EventStore that seals buffered payloads under a key ring.
SealedJournal
A JournalStore that seals payloads under a key ring.
SealedPush
A PushStore that seals webhook credentials under a key ring.
SealedTasks
A TaskStore that seals task proposals under a key ring.
VaultTransit
Vault’s transit engine, reached over HTTP.
WrappedKey
A data key as it may safely be stored: sealed by a key this crate never has.

Enums§

KeyError
Why a key operation did not happen.

Constants§

ENVELOPE_FORMAT_VERSION
The sealed-envelope construction this build writes, and the only one it reads.

Traits§

KeyRing
Where data keys are made, wrapped, and destroyed.

Functions§

event_scope
The erasure scope of one buffered event, named by its (source, id) pair.
probe_sealed_case_state
Prove a case’s sealed state still opens, without keeping the plaintext.
scope
The erasure scope for a unit within a tenant.