| name | pq-migration-plan |
|---|---|
| authored | 2026-05-05 |
| runbook | pq-readiness-secure-data |
| runbook_milestone | M2 |
| research_basis | docs/slo/research/pq-readiness-secure-data/synthesis.md |
| status | locked-in (M1); implemented through M2 |
Status (2026-05-06): M2 is implemented behind --features pq; the locked X25519 + ML-KEM-768 / HKDF-SHA-256 v2 envelope wrap is now executable.
One-line summary.
secure_datahas gained a hybrid X25519 + ML-KEM-768 / HKDF-SHA-256 KEM behind apqfeature flag. M1 reserved the public surface (enum slot, envelopecombiner_idfield, error variants, size constants); M2 ships the implementation; M3 lands the cross-version compatibility matrix; M4 documents the FIPS-track posture honestly.
Critical-infrastructure consumers with secrecy windows of 10 years or more must demonstrate a documented, executable post-quantum migration path. A "harvest now, decrypt later" adversary recording today's classical envelopes can decrypt them at a future date once a sufficiently capable quantum computer exists. The migration target is therefore the key wrap — the data-key encryption — not the AEAD itself: AES-256-GCM and XChaCha20-Poly1305 remain Grover-resistant under credible models, but the key transport must shift to a primitive the adversary cannot break in advance.
The chosen path is a hybrid construction (classical X25519 ⊕ post-quantum ML-KEM-768) rather than a PQ-only construction, because:
- ML-KEM-768 was finalised as FIPS 203 in 2024, but no FIPS 140-3 cryptographic module covers it as of 2026-05 (AWS-LC FIPS 3.0 is on CMVP "modules in process" since 2024-12 with no public ETA, per the research dossier). Hybrid keeps the classical X25519 leg in the security argument so a hypothetical future ML-KEM cryptanalysis cannot retroactively compromise everything.
- NIST and the CFRG have recommended hybrid KEMs for the migration period in multiple drafts and statements; production deployments at AWS, Cloudflare, and Google in 2024–2025 have followed this pattern.
- Hybrid is a strict superset of either leg's security: an attacker must break both X25519 and ML-KEM-768 to recover the wrap key.
These decisions come from the research dossier (29 sources; pinned 2026-05-05) and are not re-litigated in M2:
| Decision | Value | Rationale |
|---|---|---|
Primary ML-KEM crate (default pq) |
RustCrypto/ml-kem v0.3.0 |
Pure-Rust; matches secure_data's existing no-FFI posture; well-audited maintainership; forbid(unsafe_code)-compatible |
FIPS-track ML-KEM crate (future pq-aws-lc feature) |
aws-lc-rs |
Aligns with the existing fips feature; AWS-LC is on the CMVP "modules in process" list — when validated, this becomes the FIPS path |
| Wire format | TLS-style concatenation (ML-KEM_ct ‖ X25519_share) with shared secrets fed through HKDF-SHA-256 |
Aligns with draft-ietf-tls-hybrid-design consensus as of 2026-Q1; readable, debuggable, no exotic combiner needed |
| Combiner | HKDF-SHA-256 with deterministic info string "sunlit-pq-x25519-ml-kem-768/v1" |
Standardised, audited, fits in the pure-Rust budget |
| Symmetric AEAD | AES-256-GCM (the existing default) — unchanged | The PQ migration affects key wrap, not the AEAD |
| Combiner identifier | 0x01 for the M2 combiner; 0x80–0xFE reserved for future combiners (X-Wing, CFRG QSF); 0xFF permanent fail-closed sentinel |
Forward-compat for adopting newer combiners without a breaking wire-format change |
| FIPS-track posture for M4 | Monitor only. No CMVP cert covers ML-KEM as of 2026-05; fips × pq builds are honestly labelled "validation pending CMVP" |
No false claims of FIPS-validated PQ |
EnvelopeEncrypted {
version: "1"
algorithm: "AES-256-GCM" | "XChaCha20-Poly1305"
key_alias: <string>
key_version: <string>
wrapped_data_key: <bytes> // KEK-wrapped DEK
nonce: <bytes> // 12 (AES-GCM) or 24 (XChaCha20) bytes
ciphertext: <bytes> // AEAD-protected plaintext
aad: <bytes> // deterministic envelope header bytes
// (combiner_id absent in pre-M1 envelopes; defaults to None on deserialize)
}
M1 adds one optional field:
EnvelopeEncrypted {
...all v1 fields...
combiner_id: Option<u8> // None for classical; Some(id) for hybrid
}
#[serde(default, skip_serializing_if = "Option::is_none")] ensures:
- Pre-M1 envelopes (no
combiner_idfield on the wire) deserialize tocombiner_id == None. - Classical envelopes serialised post-M1 do not emit a
combiner_idfield — bytes-per-envelope unchanged for the dominant case. - Hybrid envelopes (M2 onward) emit
Some(0x01)explicitly.
EnvelopeEncrypted {
version: "2"
algorithm: "X25519+ML-KEM-768/HKDF-SHA-256"
key_alias: <string>
key_version: <string>
wrapped_data_key: <bytes> // = ML-KEM_ct ‖ X25519_share ‖ AES-GCM(KEK_HKDF, DEK)
nonce: <bytes> // 12 bytes (AES-GCM nonce for the inner data-key wrap)
ciphertext: <bytes> // unchanged: AES-256-GCM-protected plaintext
aad: <bytes> // deterministic envelope header bytes (now binds combiner_id)
combiner_id: Some(0x01) // X25519 + ML-KEM-768 / HKDF-SHA-256
}
The inner construction in wrapped_data_key:
ML-KEM-768 ciphertext (1088 bytes) ─┐
X25519 share ( 32 bytes) ─┤── concat ──┐
│
▼
HKDF-SHA-256
│
▼
wrap key (32 bytes, AES-256-GCM key)
│
▼
AES-GCM-wrapped DEK (variable bytes)
The recipient performs ML-KEM decapsulation + X25519 ECDH, concatenates the shared secrets in the same order, runs HKDF-SHA-256 with the locked info string, and unwraps the DEK with AES-256-GCM.
| Value | Combiner | Status |
|---|---|---|
0x00 |
Reserved (never on the wire — equivalent to None) |
accepted by validate_structure for legacy serialisers; not emitted |
0x01 |
X25519 + ML-KEM-768 / HKDF-SHA-256 | M2 default |
0x02 – 0x7F |
Reserved for future combiners (M3+) | rejected as AlgorithmRejectedByPolicy until explicitly recognised |
0x80 – 0xFE |
Reserved-future range (X-Wing, CFRG QSF, etc.) | rejected as AlgorithmRejectedByPolicy |
0xFF |
Permanent fail-closed sentinel | always rejected; never emitted by any version of this crate |
The migration plan addresses the following threat classes:
- Harvest now, decrypt later (10-year secrecy class). Hybrid construction means an adversary recording M2-era envelopes today must break X25519 (Shor on ECDLP) and ML-KEM-768 (lattice cryptanalysis) to recover plaintext, even decades later.
- Downgrade attack: an attacker who can substitute envelopes between producer and consumer cannot force a downgrade from v2 hybrid to v1 classical, because:
- M3's
AlgorithmPolicy::min_versionlets a consumer require v2-or-higher envelopes; v1 envelopes returnAlgorithmRejectedByPolicy. - The
aadbytes bind the algorithm string; any tampering fails AEAD authentication.
- M3's
- Combiner substitution: an attacker who flips
combiner_idon a v2 envelope from0x01to0xFF(fail-closed) or any reserved-future value getsAlgorithmRejectedByPolicyfromvalidate_structurebefore any cryptographic operation. No silent acceptance. - Build-skew misuse: a v2 envelope sent to a non-
pq-feature build returnsPqFeatureRequiredfromvalidate_structurebefore any cryptographic operation. No silent fallback to classical.
- Side-channel attacks on the ML-KEM implementation. RustCrypto
ml-kemv0.3.0 is constant-time per its specification, but no Rust implementation has the same level of hardware-level side-channel hardening as some C libraries with HSM-grade timing analysis. Mitigation path: when AWS-LC FIPS-validated ML-KEM lands, thepq-aws-lcfeature becomes the recommended path for high-assurance environments; until then, this is a documented residual risk. - Post-quantum signature schemes (ML-DSA, SLH-DSA). Out of scope for this runbook. Signatures require a separate runbook because key sizes and migration patterns differ materially from KEMs.
- KMS-side hybrid wrap. AWS KMS, HashiCorp Vault Transit, and Google Cloud KMS do not yet support PQ or hybrid wrapping primitives at the KMS API level (verified 2026-05). M2 performs the hybrid wrap above the existing classical KMS — the KMS still wraps the data key with a classical KEK, and the hybrid construction sits on top. When KMS providers add PQ wrap, a follow-up runbook adds it.
- Fully PQ-only deployments. M2 ships hybrid only; PQ-only is an explicit future opt-in via a
pq_onlypolicy setting once the FIPS path is validated.
Current state (2026-05): No FIPS 140-3 cryptographic module on the CMVP validated list covers ML-KEM-768. AWS-LC FIPS 3.0 is on the "modules in process" list since 2024-12-10 with no public ETA.
Honest labelling: A build with both --features fips and --features pq enabled will:
- Compile and behave identically to a
--features pqbuild (RustCryptoml-kem0.3.0 backend). - Emit
pq_fips_status: "pending_cmvp"inDebugoutput of the produced envelope. - Not claim FIPS validation for the hybrid path. README, dev-guide, CHANGELOG, and rustdoc all use the phrase "validation pending CMVP" — never "FIPS validated PQ."
- M4's CI lint will block any "FIPS validated PQ" string appearing in docs or changelog.
Promotion criteria (future runbook): When a CMVP cert covers ML-KEM-768 in a Rust-callable cryptographic module, a follow-up runbook:
- Adds a new
pq-aws-lcfeature (or similar) that selects the validated implementation. - Documents the cert number, the validated module version, and the OS/platform constraints.
- Updates this migration plan to reference the validated path.
- Removes the "validation pending CMVP" label for the validated combination.
This section is the consumer-facing playbook for adopting the M2 hybrid envelope.
After M1 lands, downstream applications can:
secure_data = "0.1.2" # or whatever the M1-shipping version isExisting classical encrypt/decrypt continues unchanged. The combiner_id field is part of the envelope shape but classical envelopes do not emit it on the wire.
secure_data = { version = "0.2", features = ["pq"] }use secure_data::algorithm::{AlgorithmPolicy, CryptoAlgorithm};
use secure_data::envelope::encrypt_with_policy;
let policy = AlgorithmPolicy::prefer(CryptoAlgorithm::HybridX25519MlKem768);
let envelope = encrypt_with_policy(plaintext, "data-key", &provider, &policy).await?;
// envelope.algorithm == "X25519+ML-KEM-768/HKDF-SHA-256"
// envelope.combiner_id == Some(0x01)Existing classical envelopes continue to decrypt unchanged. The hybrid path is opt-in per encrypt call.
let policy = AlgorithmPolicy::prefer(CryptoAlgorithm::HybridX25519MlKem768)
.with_min_envelope_version(2); // M3 APIDecrypt of a v1 envelope under this policy returns DataError::AlgorithmRejectedByPolicy.
- Deploy a build with
--features pqand the consumer-side decrypt for v1 + v2. - Wait until all producers are on the new build (zero v2 envelopes in circulation yet).
- Switch producer policy to
prefer(HybridX25519MlKem768). New writes are v2. - Existing v1 envelopes decrypt unchanged; new writes are v2.
- Once read traffic for v1 is below the application's tolerance threshold (typically a function of secrecy window), enforce
min_envelope_version=2policy.
- A specific timeline for when
pqbecomes the default (not opt-in). - A specific timeline for promoting the FIPS-track combination to validated status — depends on CMVP and the AWS-LC validation queue, both outside SunLit's control.
- ML-DSA / SLH-DSA signature support (separate runbook if needed).
- Replacing the symmetric AEAD with a PQ-AEAD (no PQ-AEAD exists; AES-256-GCM remains correct under credible PQ models).
- KMS-side hybrid wrap (depends on third-party KMS vendor support).
This plan is load-bearing for M2 design decisions. Any future PR that materially changes a "Locked-in decision" (§2) must also update this plan and link the change in the CHANGELOG. The plan is not refreshed for every minor version bump — only for design changes.
A future plan revision is expected when:
- AWS-LC FIPS-validated ML-KEM lands (§5 promotion criteria).
- A CFRG QSF combiner reaches IETF consensus and gains a new combiner identifier in the table (§3.4).
- A Rust ecosystem audit produces a finding that materially affects the dependency choice (§2).
| Source | Purpose |
|---|---|
docs/slo/research/pq-readiness-secure-data/dossier.md |
29-source research dossier; locked-in decisions trace here |
docs/slo/research/pq-readiness-secure-data/synthesis.md |
Synthesis paragraphs — "the design must handle X because |
docs/slo/idea/pq-readiness-secure-data.md |
Idea doc — wedge, target user, success criteria |
docs/slo/future/RUNBOOK-pq-readiness-secure-data.md |
v4 runbook — milestone plan + Contract Block per milestone |
| FIPS 203 (NIST, 2024) | ML-KEM specification |
| RFC 7748 | X25519 |
| RFC 5869 | HKDF |
draft-ietf-tls-hybrid-design |
Hybrid wire-format consensus pattern |
RustCrypto ml-kem v0.3.0 |
Locked primary KEM implementation |
aws-lc-rs |
Future FIPS-track implementation |