Export Center KMS Envelope Pattern (age + AES-GCM)
Audience: Export Center service engineers and operators implementing or verifying encrypted bundle payloads. Purpose: Define the deterministic envelope-encryption scheme StellaOps uses for mirror bundle encryption and general export signing, so that bundles encrypt and decrypt identically across reruns and remain verifiable offline.
This pattern covers the worker (write) path, the verification (read) path, and the provenance metadata that ties the two together. It supports two interchangeable wrapping mechanisms — age recipients for fully offline deployments and a KMS envelope for installations with Authority/HSM reachability.
Key hierarchy
- Content key (DEK): 32-byte random generated per export run. Used for AES-256-GCM over encrypted payloads (
/datasubtree for mirror; optional for others). - Nonce: 12-byte random per file; stored alongside ciphertext; derive Additional Authenticated Data (AAD) as
{runId}:{relativePath}to bind file path and run. - Wrapping keys:
- age recipients (preferred for offline): each tenant can list one or more age public keys. DEK is wrapped once per recipient using age X25519. Store
recipient,wrappedKey(base64), and optionalkeyIdin provenance. - KMS envelope (Authority/HSM): DEK wrapped with tenant-scoped KMS key alias
export/envelope. StorekmsKeyId(authority URI or external ARN) andwrappedKey(base64) plus KMS-providedalgorithm.
- age recipients (preferred for offline): each tenant can list one or more age public keys. DEK is wrapped once per recipient using age X25519. Store
Write path (worker)
- Generate DEK (32 bytes) per run; zeroize after use.
- For each encrypted file, derive AAD =
{runId}:{relativePath}; encrypt with AES-256-GCM (nonce per file). Storenonceandciphertext. - Wrap DEK for all configured recipients:
- age:
age --encrypt --recipient <pub>over DEK bytes → base64. - KMS:
Encrypt/WrapKeywithKeyId=export/envelopeandEncryptionContext={runId,tenant}→ base64.
- age:
- Record wrapping metadata in
provenance.jsonunderenvironment.encryption.recipients[]preserving deterministic order (age recipients lexicographically byrecipient, then KMS entries bykmsKeyId). - Include
encryption.mode(ageoraes-gcm+kms),aadFormat, andnonceFormatin provenance for verification tooling.
Read/verification path
- Select a recipient entry that matches available keys (age private key or KMS key).
- Unwrap DEK:
- age:
age --decrypt→ DEK bytes. - KMS:
Decrypt/UnwrapKeywith same encryption context.
- age:
- For each encrypted file, recompute AAD from
{runId}:{relativePath}, decrypt with AES-256-GCM using storednonce, verify tag. - Recompute SHA-256 of decrypted payload and compare with
export.jsonentries.
Determinism & offline posture
- Recipient lists and wrapped keys are ordered deterministically to keep
provenance.jsonhashes stable across retries. - age path works fully offline; KMS path requires Authority/HSM availability but stores all metadata to allow later decryption once KMS is reachable.
- Use fixed casing and field names:
mode,recipients[] {type, recipient|kmsKeyId, wrappedKey, keyId?}andaadFormat.
Testing notes
- Add regression cases that encrypt/decrypt fixtures with both age and KMS paths, asserting identical manifest/provenance hashes across reruns.
- Ensure decryption fails when AAD does not match expected
{runId}:{relativePath}(prevents path swapping). - Keep tests air-gap friendly: mock KMS wrapper with deterministic stub keys.
Rollout guidance
- Default to age recipients for Offline Kit deployments; enable KMS wrapping where Authority/HSM is reachable.
- Configuration knobs:
ExportCenter:Encryption:Mode—ageorkms.ExportCenter:Encryption:Recipients— list of age public keys.ExportCenter:Encryption:KmsKeyId— tenant-specific key alias (when using KMS).
- When the encrypted payload layout changes, update the verification commands in the CLI and Console guides so they continue to reference this pattern.
