Skip to content

Add MlKem768P256 (ML-KEM-768 + P-256) hybrid KEM - #16

Closed
danieldia-dev wants to merge 2 commits into
symbolicsoft:mainfrom
danieldia-dev:mlkem768-p256-hybrid
Closed

Add MlKem768P256 (ML-KEM-768 + P-256) hybrid KEM#16
danieldia-dev wants to merge 2 commits into
symbolicsoft:mainfrom
danieldia-dev:mlkem768-p256-hybrid

Conversation

@danieldia-dev

Copy link
Copy Markdown
Contributor

This PR adds the MLKEM768-P256 hybrid KEM (draft-irtf-cfrg-hybrid-kems) to the pq feature. This includes only the implementation: the (spec-conformance) test suite that validates it is written and passing, but will be submitted as a standalone follow-up PR (see testing note below). The one production behavior change here (in particular, to the P-256 ephemeral path) is interop-neutral by construction (since nothing on the wire changes, only the sender's local sampling) and justified below.

The hybrid KEM is a generic combiner, and is Kem-only by design. Unlike X-Wing, which is a purpose-built combiner, encap/decap here drive ML-KEM-768 and P-256 independently and combine them with SHA3-256(ss_pq || ss_t || ct_t || ek_t || "MLKEM768-P256"). The type implements Kem but deliberately has no AuthKem variant: a post-quantum KEM can't carry sender authentication the way a DH KEM can, so the absence is enforced at the type level rather than a runtime issue. Public keys are 1249 bytes (1184 ML-KEM ek || 65 uncompressed SEC1 P-256), encapsulated keys 1153 bytes (1088 ML-KEM ct || 65 P-256 ephemeral), private keys a 32-byte seed, shared secrets 32 bytes. Both halves of a parsed public key are decoded once at construction so encap never re-decodes wire bytes.

Key derivation returns Result, and the private key trades a little memory for a faster decap. The 32-byte seed expands via a SHAKE-256 fan-out (i.e. first 64 bytes seed ML-KEM-768, and then the next 128 feed P-256 rejection sampling). Because that rejection sampling can (in principle) fail, keypair_from_seed_mlkem_p256 returns a Result and propagates DeriveKeyPairError rather than panicking, matching the existing posture of derive_p_curve_sk in dh.rs (unlike X-Wing whose seed-to-key map is total and therefore infallible). The private key stores the seed + expanded dk_pq/dk_t and a cached 65-byte ek_t_bytes; caching the recipient's own serialized P-256 public key removes a base-point scalar multiplication from every decap (which is the same decap optimization we already apply to DHKEM private keys). Zeroization mirrors the crate's existing workarounds: dk_pq is Option-wrapped so zeroize() can force its inner drop, and since p256::SecretKey exposes no Zeroize, dk_t is overwritten with a dummy scalar so the real scalar's Drop scrubs it. The combiner buffer is hashed by slice and zeroized immediately after, with ss_pq scrubbed as soon as it is copied in.

The P-256 ephemeral is generated via the spec's GenerateKeyPair procedure, not p256's native sampler. Instead of having drawn the ephemeral with p256::ecdh::EphemeralSecret::random, it draws a fixed 128-byte candidate buffer directly from the RNG and rejection-samples it identically to key generation. There are three reasons for this choice: (1) it matches the draft's algorithm definition (which generates the traditional ephemeral the same way it generates a static key); (2) it makes the sender's randomness draw deterministic and reproducible; and (3) the recipient never observes how the ephemeral was sampled (so any valid scalar decapsulates identically). The last of these three properties is what makes the deferred encap direction KAT possible in the first place.

Testing (validated; will be submitted as a separate PR). A KAT suite was written for this KEM and ran against rust-hpke's vectors; the implementation passes it. The decap direction reproduces each vector's shared secret byte-for-byte from the seed and ciphertext, exercising the full combiner path (label bytes, field order, SEC1-uncompresed encoding, SHA3-256) against an implementation that isn't ours. The encap direction was validated by replaying each vector's randomness through the real encap via a cfg-gated deterministic entry point (to be added in next PR) and asserts both ciphertext and shared secret match, with a tripwire that distinguishes a bug in the randomness's layout from a combiner bug (this observation was precisely what led to the ephemeral-generation change). Passing this suite confirmed the correctness of 0x0050 ID, the label and its position, the field order, the SEC1 encoding, and the SHAKE seed fan-out against the reference. That suite (along with the replay_rng/encap_det test scaffolding, fuzz-target, and benchmark coverage for parity with the other KEMs) will be submitted as a standalone follow-up PR.

This PR compiles and runs cleanly under:

RUSTFLAGS="-C target-cpu=native" cargo test --features pq

Generic hybrid combiner per draft-irtf-cfrg-hybrid-kems §5.5 (IANA HPKE KEM ID 0x0050). Unlike XWingDraft06, drives ML-KEM-768 and P-256 independently and combines via SHA3-256. No AuthKem impl, same as other PQ KEMs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants