Post-quantum encryption: hybrid ML-KEM-768 identities with upgrade-on-touch (#28) #107

Merged
Cordy merged 12 commits from feat/pq-encryption into main 2026-08-04 11:32:44 +00:00
Owner

Implements #28, unblocked by Nikola's research: age v1.3.0+ natively supports hybrid post-quantum recipients, and because age wraps ONE file key into an independent header stanza per recipient, multi-recipient encryption (user+recovery, group spaces #18) carries over unchanged. We already pin age v1.3.1 — zero new dependencies.

Config: storage.encryption.postQuantum: true (or CAIRN_ENC_POST_QUANTUM=true) switches ALL custody backends (keycloak-profile, openbao, deployment) to minting hybrid ML-KEM-768+X25519 identities (AGE-SECRET-KEY-PQ-1…).

The constraint, enforced: hybrid recipients emit a postquantum label and refuse to mix with classical recipients. Startup validation therefore requires the recovery identity (and deployment identity) to match the flavor, with recoveryIdentityLegacy / deploymentIdentityLegacy as decrypt-only slots for the pre-migration keys. A test proves age rejects a mixed set.

Migration — upgrade-on-touch: existing principals hold classical identities. On first encrypted operation under PQ, a hybrid identity is minted and stored FIRST; the classical identity is RETAINED (multi-value Keycloak attribute / legacyIdentities in OpenBao) so every pre-PQ file still decrypts. Rollback (postQuantum: false) selects the retained classical identity again — nothing minted, nothing orphaned. OpenBao upgrades use versioned check-and-set so racing replicas can't clobber keys.

Internals: new flavor-aware keyset layer (pq.go: ParseIdentity, IsHybrid, resolveKeyset); Keycloak user+group custody, OpenBao, and StaticKeys all rewritten on it; Recovery fields widened to age.Identity. instanceSeed (#31) with a PQ deployment identity is refused at startup (HKDF derivation is X25519-only — documented follow-up).

Tests: flavor parsing, hybrid StaticKeys roundtrip + recovery, the mixing guard, Keycloak PQ lazy provision, full upgrade-on-touch matrix (legacy decrypt / attribute retention / classical-cannot-read-new-content / hybrid recovery works), rollback, OpenBao upgrade with legacy retention. Full suite green.

Costs (from #28): ~2000-char identities (bigger attribute values), ~2× keygen, negligible enc/dec.

Implements #28, unblocked by Nikola's research: age v1.3.0+ natively supports hybrid post-quantum recipients, and because age wraps ONE file key into an independent header stanza per recipient, multi-recipient encryption (user+recovery, group spaces #18) carries over unchanged. We already pin age v1.3.1 — zero new dependencies. **Config:** `storage.encryption.postQuantum: true` (or `CAIRN_ENC_POST_QUANTUM=true`) switches ALL custody backends (keycloak-profile, openbao, deployment) to minting hybrid ML-KEM-768+X25519 identities (`AGE-SECRET-KEY-PQ-1…`). **The constraint, enforced:** hybrid recipients emit a `postquantum` label and refuse to mix with classical recipients. Startup validation therefore requires the recovery identity (and deployment identity) to match the flavor, with `recoveryIdentityLegacy` / `deploymentIdentityLegacy` as decrypt-only slots for the pre-migration keys. A test proves age rejects a mixed set. **Migration — upgrade-on-touch:** existing principals hold classical identities. On first encrypted operation under PQ, a hybrid identity is minted and stored FIRST; the classical identity is RETAINED (multi-value Keycloak attribute / `legacyIdentities` in OpenBao) so every pre-PQ file still decrypts. Rollback (`postQuantum: false`) selects the retained classical identity again — nothing minted, nothing orphaned. OpenBao upgrades use versioned check-and-set so racing replicas can't clobber keys. **Internals:** new flavor-aware keyset layer (`pq.go`: `ParseIdentity`, `IsHybrid`, `resolveKeyset`); Keycloak user+group custody, OpenBao, and StaticKeys all rewritten on it; `Recovery` fields widened to `age.Identity`. `instanceSeed` (#31) with a PQ deployment identity is refused at startup (HKDF derivation is X25519-only — documented follow-up). **Tests:** flavor parsing, hybrid StaticKeys roundtrip + recovery, the mixing guard, Keycloak PQ lazy provision, full upgrade-on-touch matrix (legacy decrypt / attribute retention / classical-cannot-read-new-content / hybrid recovery works), rollback, OpenBao upgrade with legacy retention. Full suite green. **Costs (from #28):** ~2000-char identities (bigger attribute values), ~2× keygen, negligible enc/dec.
Cordy merged commit 9f9312a9c0 into main 2026-08-04 11:32:44 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#107
No description provided.