Add MlKem768P256 (ML-KEM-768 + P-256) hybrid KEM - #16
Closed
danieldia-dev wants to merge 2 commits into
Closed
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds the
MLKEM768-P256hybrid KEM (draft-irtf-cfrg-hybrid-kems) to thepqfeature. 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/decaphere drive ML-KEM-768 and P-256 independently and combine them withSHA3-256(ss_pq || ss_t || ct_t || ek_t || "MLKEM768-P256"). The type implementsKembut deliberately has noAuthKemvariant: 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 soencapnever 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_p256returns aResultand propagatesDeriveKeyPairErrorrather than panicking, matching the existing posture ofderive_p_curve_skindh.rs(unlike X-Wing whose seed-to-key map is total and therefore infallible). The private key stores the seed + expandeddk_pq/dk_tand a cached 65-byteek_t_bytes; caching the recipient's own serialized P-256 public key removes a base-point scalar multiplication from everydecap(which is the samedecapoptimization we already apply to DHKEM private keys). Zeroization mirrors the crate's existing workarounds:dk_pqisOption-wrapped sozeroize()can force its inner drop, and sincep256::SecretKeyexposes noZeroize,dk_tis overwritten with a dummy scalar so the real scalar'sDropscrubs it. The combiner buffer is hashed by slice and zeroized immediately after, withss_pqscrubbed as soon as it is copied in.The P-256 ephemeral is generated via the spec's
GenerateKeyPairprocedure, notp256's native sampler. Instead of having drawn the ephemeral withp256::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 deferredencapdirection 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. Thedecapdirection 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. Theencapdirection was validated by replaying each vector'srandomnessthrough the realencapvia acfg-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 of0x0050ID, the label and its position, the field order, the SEC1 encoding, and the SHAKE seed fan-out against the reference. That suite (along with thereplay_rng/encap_dettest 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: