Expand description
Per-tenant envelope key custody (ADR 0004 §D5).
EXPERIMENTAL / pre-external-review (ADR 0004 §D7). This is a self-contained crypto building block, deliberately not wired to any request flow or stored-key feature yet. The default hosted plane is pure per-request BYOK — nothing tenant-supplied is stored — so there is no consumer for stored ciphertext today. This module exists so that when a stored-key feature lands, the vetted primitive is already here and already reviewed. It has not yet had the external crypto review ADR 0004 §D7 requires; do not treat it as blessed.
§What it does
KeyCustody is the seam ADR 0004 §D5 names: encrypt/decrypt blobs under a per-tenant
envelope so a cloud-KMS implementation can slot in later behind the same trait. The local
tier — LocalKeyCustody — is implemented here with real AES-256-GCM (the vetted
aes_gcm / RustCrypto crate), a single 32-byte key-encryption key (KEK) from config, and
the tenant id bound as AEAD associated data so a blob sealed for tenant A cannot be opened
under tenant B.
§Construction (the exact bytes on disk)
- Cipher: AES-256-GCM (
Aes256Gcm), 256-bit KEK, 128-bit auth tag. - Nonce: fresh random 96 bits per
encryptcall, drawn from the OS ambient CSPRNG (getrandom’sSysRng). A nonce is never reused with the KEK; a 96-bit random nonce per message is the standard GCM construction. - AAD: the tenant id (
tenant.as_bytes()) is passed as associated data on both encrypt and decrypt. It is authenticated but not encrypted; a mismatch fails the auth tag. - Blob layout:
nonce (12 bytes) ‖ ciphertext ‖ tag (16 bytes). Self-describing: the nonce is a fixed-width prefix, andciphertext ‖ tagis exactly whataes-gcmreturns. The blob is safe to store at rest; it reveals only the plaintext length.
§KEK loading
LocalKeyCustody::from_env reads, in order of precedence:
FIRSTPASS_KEK— the KEK hex-encoded (64 lowercase/uppercase hex chars = 32 bytes). Surrounding whitespace is trimmed. Hex (not base64) is used for parity with the rest of the crate, which already depends onhex(e.g. audit-trace digests).FIRSTPASS_KEK_FILE— path to a file whose contents are exactly 32 raw bytes.
A missing, short, over-long, or malformed KEK is a hard error (KeyCustodyError); the
loader is fail-closed and never falls back to a zero/default key.
Structs§
- Local
KeyCustody - Local-tier
KeyCustody: real AES-256-GCM under a single process-held KEK.
Enums§
- KeyCustody
Error - Errors from key custody. No variant ever carries key material or plaintext.
Traits§
- KeyCustody
- Encrypt/decrypt opaque blobs under a per-tenant envelope.