Add MLKEM768-X25519 hybrid KEM support in HPKE#14695
Merged
reaperhulk merged 8 commits intomainfrom Apr 23, 2026
Merged
Conversation
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
Member
Author
|
@reaperhulk should the Hybrid types have a |
alex
commented
Apr 19, 2026
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
Member
Author
|
@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
Member
|
How many types are we going to have? I prefer the verbose ones unless we're going to have a large proliferation. Re: |
Member
Author
|
It's 2x, this and mlkem1024/p384. Not sure, I don't think so though
All that is necessary for evil to succeed is for good people to do nothing.
…On Mon, Apr 20, 2026, 2:24 PM Paul Kehrer ***@***.***> wrote:
*reaperhulk* left a comment (pyca/cryptography#14695)
<#14695?email_source=notifications&email_token=AAAAGBCNL3MBKFJSY4BK6BL4WZTPTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRYGMZDSMRSG4Y2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4283292271>
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?
—
Reply to this email directly, view it on GitHub
<#14695?email_source=notifications&email_token=AAAAGBCNL3MBKFJSY4BK6BL4WZTPTA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRYGMZDSMRSG4Y2M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4283292271>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAAGBDL32NTUHATUWOTQAD4WZTPTAVCNFSM6AAAAACX6VH2EGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEOBTGI4TEMRXGE>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Member
|
Okay let's do the verbose ones then and not have from_seed in our public API for now? |
Member
Author
|
Great, this is r4r then
All that is necessary for evil to succeed is for good people to do nothing.
…On Mon, Apr 20, 2026, 2:39 PM Paul Kehrer ***@***.***> wrote:
*reaperhulk* left a comment (pyca/cryptography#14695)
<#14695?email_source=notifications&email_token=AAAAGBAA2PSW4WEXFJEGNND4WZVEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRYGMZTQNJUGEYKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4283385410>
Okay let's do the verbose ones then and not have from_seed in our public
API for now?
—
Reply to this email directly, view it on GitHub
<#14695?email_source=notifications&email_token=AAAAGBAA2PSW4WEXFJEGNND4WZVEVA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTIMRYGMZTQNJUGEYKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2LK4DSL5RW63LNMVXHIX3POBSW4X3DNRUWG2Y#issuecomment-4283385410>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAAAGBBFAOAFC2ZMJPNQVH34WZVEVAVCNFSM6AAAAACX6VH2EGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEOBTGM4DKNBRGA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
reaperhulk
approved these changes
Apr 23, 2026
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.
Summary
0x647A) asKEM.MLKEM768_X25519, specified in draft-connolly-cfrg-xwing-kem and draft-ietf-hpke-pq.MLKEM768X25519PrivateKey(mlkem_key, x25519_key)/MLKEM768X25519PublicKey(mlkem_key, x25519_key)constructor-only types incryptography.hazmat.primitives.hpkethat compose existing ML-KEM-768 and X25519 key objects. The private key exposespublic_key().encapsulate,decapsulate,exchange,public_key,public_bytes_raw) already used by the pure ML-KEM and X25519 HPKE paths.Test plan
kem_id: 0x647Afromhpke-pq-test-vectors.json) decrypt correctly viatest_vector_decryption; the X-Wing 32-byte seed is expanded to component keys in the test viahashlib.shake_256.OPENSSL_DIR=/opt/aws-lc nox -e localpasses (ruff, clippy, mypy, cargo check, 3612 pytest passed / 634 skipped, cargo test).https://claude.ai/code/session_019VW3s2nq5QVCdpUnTvPJDv