Skip to content

Add MLKEM768-X25519 hybrid KEM support in HPKE#14695

Merged
reaperhulk merged 8 commits intomainfrom
claude/add-ml-kem-hybrid-support-BLjg3
Apr 23, 2026
Merged

Add MLKEM768-X25519 hybrid KEM support in HPKE#14695
reaperhulk merged 8 commits intomainfrom
claude/add-ml-kem-hybrid-support-BLjg3

Conversation

@alex
Copy link
Copy Markdown
Member

@alex alex commented Apr 19, 2026

Summary

  • Adds the MLKEM768-X25519 hybrid KEM (X-Wing, HPKE KEM ID 0x647A) as KEM.MLKEM768_X25519, specified in draft-connolly-cfrg-xwing-kem and draft-ietf-hpke-pq.
  • Introduces minimal MLKEM768X25519PrivateKey(mlkem_key, x25519_key) / MLKEM768X25519PublicKey(mlkem_key, x25519_key) constructor-only types in cryptography.hazmat.primitives.hpke that compose existing ML-KEM-768 and X25519 key objects. The private key exposes public_key().
  • Encap/decap, the SHA3-256 X-Wing combiner, and all component key access live inside the HPKE backend and go through the same Python-level interfaces (encapsulate, decapsulate, exchange, public_key, public_bytes_raw) already used by the pure ML-KEM and X25519 HPKE paths.

Test plan

  • All existing HPKE vectors (including kem_id: 0x647A from hpke-pq-test-vectors.json) decrypt correctly via test_vector_decryption; the X-Wing 32-byte seed is expanded to component keys in the test via hashlib.shake_256.
  • New unit tests cover hybrid roundtrip, ciphertext length (1120 + 4 + 16), wrong-key / wrong-type decryption failures, and using the hybrid public key with a non-hybrid KEM suite.
  • OPENSSL_DIR=/opt/aws-lc nox -e local passes (ruff, clippy, mypy, cargo check, 3612 pytest passed / 634 skipped, cargo test).

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv

claude added 6 commits April 19, 2026 16:07
Implements the MLKEM768-X25519 hybrid KEM (KEM ID 0x647A) as specified in
draft-connolly-cfrg-xwing-kem and draft-ietf-hpke-pq. This combines
ML-KEM-768 with X25519 to provide both classical and post-quantum security.

Adds new MLKEM768X25519PrivateKey / MLKEM768X25519PublicKey types backed
by a 32-byte seed that is SHAKE256-expanded into the underlying component
keys, and wires them into HPKE via the new KEM.MLKEM768_X25519 variant.
Only available on backends with ML-KEM support (AWS-LC, BoringSSL).

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
Replaces the previous design (standalone backend module plus abstract
classes in cryptography.hazmat.primitives.asymmetric.mlkem) with a minimal
constructor-only API exposed directly from
cryptography.hazmat.primitives.hpke:

    MLKEM768X25519PrivateKey(mlkem_sk, x25519_sk)
    MLKEM768X25519PublicKey(mlkem_pk, x25519_pk)

Everything else (X-Wing combiner, encap/decap, seed expansion) is kept
internal to the HPKE backend. Tests handle the X-Wing seed expansion
explicitly via hashlib.shake_256 when deriving component keys from an
X-Wing 32-byte seed.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
- Drop the backend-specific cfg gates on the MLKEM768_X25519 KEM variant
  and its match arms, mirroring how MLKEM768/MLKEM1024 are always part of
  the KEM enum.
- Merge the ML-KEM unreachable!() arms for the generate_key /
  serialize_public_key / deserialize_public_key / exchange /
  kem_hash_algorithm methods so all three ML-KEM variants share one arm.
- Switch MlKem768X25519PrivateKey/PublicKey to store PyAny and go through
  Python method calls (encapsulate/decapsulate/exchange/public_key/
  public_bytes_raw), matching the pattern the existing ML-KEM and X25519
  HPKE paths already use. Types are validated at construction time via
  the existing types::MLKEM768_*/X25519_* LazyPyImport entries.
- Remove the crate-visible pkey() accessors that are no longer needed.
- Tighten the MLKEM768_X25519 doc entry (drop the KEM ID and the
  "classical + post-quantum" phrasing) to mirror the pure ML-KEM entries.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
- Add a public_key() method to MLKEM768X25519PrivateKey so callers can
  derive the matching MLKEM768X25519PublicKey from a private key without
  having to re-plumb the component .public_key() calls themselves. Uses
  the existing Python-level .public_key() methods on the stored component
  keys.
- Simplify the MLKEM768_X25519 branches of test_roundtrip and
  test_roundtrip_no_info to use sk_r.public_key() — they no longer need
  to be special-cased.
- Drop the CHANGELOG entry for this. All of HPKE is new in this release
  and is already documented by the line just above it.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
openssl::hash::MessageDigest::sha3_256() in the openssl Rust crate is
cfg-gated to ossl111 / libressl380 / awslc — it is not compiled in for
BoringSSL, which is what was breaking the BoringSSL CI job.

Switch the X-Wing combiner to route through the existing hashes Hash
struct (types::SHA3_256 + Hash::new), matching how hpke_labeled_derive
already resolves SHAKE. That uses EVP_get_digestbyname at runtime, which
works on every backend that ships SHA3-256.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
BoringSSL does not support SHA3, so the X-Wing combiner (SHA3-256) can't
run there even though the ML-KEM component is available. Gate the
hybrid paths in test_roundtrip, test_roundtrip_no_info, test_vector_
decryption, and the per-KEM tests on backend.hash_supported(SHA3_256()).

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
@alex
Copy link
Copy Markdown
Member Author

alex commented Apr 19, 2026

@reaperhulk should the Hybrid types have a from_seed() API (currently in the tests) that uses the X-wing expansion? (Claude: do not implement anything until we give you a clear instruction)

Comment thread src/rust/src/backend/hpke.rs Outdated
Comment thread src/rust/src/backend/hpke.rs
Comment thread src/rust/src/backend/hpke.rs
Comment thread src/rust/src/backend/hpke.rs
Comment thread src/rust/src/backend/hpke.rs
Comment thread src/rust/src/backend/hpke.rs
Address coverage review comments from alex on #14695:
- Add test_mlkem768_x25519_constructor_type_errors covering all four
  TypeError paths in the MLKEM768X25519Private/PublicKey constructors.
- Drop the length check inside
  MlKem768X25519PrivateKey::decapsulate. Suite::decrypt_inner always
  splits at enc_length() (1120), so the check was unreachable; note
  that in a comment instead.
- Add a test_mlkem768_x25519_secret_length Rust unit test, matching
  the pattern used for ML-KEM-768 and ML-KEM-1024.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
@alex
Copy link
Copy Markdown
Member Author

alex commented Apr 19, 2026

@reaperhulk oh, other question: do we want a dedicated type per hybrid (after this there'll be MLKEM1024/P384), or should it be one shared Hybrid{Public,Private}Key type? (I think I prefer this way, but it's more verbose)

The pyclass attribute macro expansion for MlKem768X25519PrivateKey
registers uncovered lines on the attribute; wrap the four-line
#[pyo3::pyclass(...)] declaration in NO-COVERAGE-START/END per
reviewer direction.

https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv
@reaperhulk
Copy link
Copy Markdown
Member

How many types are we going to have? I prefer the verbose ones unless we're going to have a large proliferation.

Re: from_seed -- since we're following Go here I'd be inclined to do what they do. Is there a public seed construction on their APIs?

@alex
Copy link
Copy Markdown
Member Author

alex commented Apr 20, 2026 via email

@reaperhulk
Copy link
Copy Markdown
Member

Okay let's do the verbose ones then and not have from_seed in our public API for now?

@alex
Copy link
Copy Markdown
Member Author

alex commented Apr 20, 2026 via email

@reaperhulk reaperhulk merged commit 7b63ff9 into main Apr 23, 2026
68 checks passed
@reaperhulk reaperhulk deleted the claude/add-ml-kem-hybrid-support-BLjg3 branch April 23, 2026 15:23
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.

3 participants