Skip to content

Commit 798801d

Browse files
alexclaude
andauthored
Add ML-KEM-1024 KEM support in HPKE (#14694)
* Add ML-KEM-1024 KEM support in HPKE Extends the existing HPKE ML-KEM-768 support to also handle ML-KEM-1024 (KEM ID 0x0042) per draft-connolly-cfrg-hpke-mlkem. * Address review: drop redundant ML-KEM-1024 unreachable tests, use pytest.mark.supported - Remove MLKEM1024 unreachable panic tests whose match arms are shared with MLKEM768 (generate_key, serialize/deserialize_public_key, exchange, kem_hash_algorithm). Keep the MLKEM1024 secret_length test since it exercises a distinct match arm. - Change homogeneous `(KEM.MLKEM768, KEM.MLKEM1024)` tuples to lists. - Convert ML-KEM single-KEM tests to pytest.mark.supported(only_if=...) instead of in-body backend.mlkem_supported() skip checks. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ccea683 commit 798801d

5 files changed

Lines changed: 184 additions & 37 deletions

File tree

docs/hazmat/primitives/hpke.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ specifying auxiliary authenticated information.
9898
ML-KEM-768. Post-quantum secure. Only available on backends that
9999
support ML-KEM.
100100

101+
.. attribute:: MLKEM1024
102+
103+
ML-KEM-1024. Post-quantum secure. Only available on backends that
104+
support ML-KEM.
105+
101106
.. class:: KDF
102107

103108
An enumeration of key derivation functions.

src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class KEM:
1111
P384: KEM
1212
P521: KEM
1313
MLKEM768: KEM
14+
MLKEM1024: KEM
1415

1516
class KDF:
1617
HKDF_SHA256: KDF
@@ -31,15 +32,17 @@ class Suite:
3132
plaintext: Buffer,
3233
public_key: x25519.X25519PublicKey
3334
| ec.EllipticCurvePublicKey
34-
| mlkem.MLKEM768PublicKey,
35+
| mlkem.MLKEM768PublicKey
36+
| mlkem.MLKEM1024PublicKey,
3537
info: Buffer | None = None,
3638
) -> bytes: ...
3739
def decrypt(
3840
self,
3941
ciphertext: Buffer,
4042
private_key: x25519.X25519PrivateKey
4143
| ec.EllipticCurvePrivateKey
42-
| mlkem.MLKEM768PrivateKey,
44+
| mlkem.MLKEM768PrivateKey
45+
| mlkem.MLKEM1024PrivateKey,
4346
info: Buffer | None = None,
4447
) -> bytes: ...
4548

@@ -48,7 +51,8 @@ def _encrypt_with_aad(
4851
plaintext: Buffer,
4952
public_key: x25519.X25519PublicKey
5053
| ec.EllipticCurvePublicKey
51-
| mlkem.MLKEM768PublicKey,
54+
| mlkem.MLKEM768PublicKey
55+
| mlkem.MLKEM1024PublicKey,
5256
info: Buffer | None = None,
5357
aad: Buffer | None = None,
5458
) -> bytes: ...
@@ -57,7 +61,8 @@ def _decrypt_with_aad(
5761
ciphertext: Buffer,
5862
private_key: x25519.X25519PrivateKey
5963
| ec.EllipticCurvePrivateKey
60-
| mlkem.MLKEM768PrivateKey,
64+
| mlkem.MLKEM768PrivateKey
65+
| mlkem.MLKEM1024PrivateKey,
6166
info: Buffer | None = None,
6267
aad: Buffer | None = None,
6368
) -> bytes: ...

src/rust/src/backend/hpke.rs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ mod kem_params {
4545
pub const MLKEM768_ID: u16 = 0x0041;
4646
pub const MLKEM768_NSECRET: usize = 32;
4747
pub const MLKEM768_NENC: usize = 1088;
48+
49+
pub const MLKEM1024_ID: u16 = 0x0042;
50+
pub const MLKEM1024_NSECRET: usize = 32;
51+
pub const MLKEM1024_NENC: usize = 1568;
4852
}
4953

5054
mod kdf_params {
@@ -89,6 +93,7 @@ pub(crate) enum KEM {
8993
P384,
9094
P521,
9195
MLKEM768,
96+
MLKEM1024,
9297
}
9398

9499
impl KEM {
@@ -153,6 +158,7 @@ impl KEM {
153158
KEM::P384 => kem_params::P384_ID,
154159
KEM::P521 => kem_params::P521_ID,
155160
KEM::MLKEM768 => kem_params::MLKEM768_ID,
161+
KEM::MLKEM1024 => kem_params::MLKEM1024_ID,
156162
}
157163
}
158164

@@ -163,6 +169,7 @@ impl KEM {
163169
KEM::P384 => kem_params::P384_NSECRET,
164170
KEM::P521 => kem_params::P521_NSECRET,
165171
KEM::MLKEM768 => kem_params::MLKEM768_NSECRET,
172+
KEM::MLKEM1024 => kem_params::MLKEM1024_NSECRET,
166173
}
167174
}
168175

@@ -173,6 +180,7 @@ impl KEM {
173180
KEM::P384 => kem_params::P384_NENC,
174181
KEM::P521 => kem_params::P521_NENC,
175182
KEM::MLKEM768 => kem_params::MLKEM768_NENC,
183+
KEM::MLKEM1024 => kem_params::MLKEM1024_NENC,
176184
}
177185
}
178186

@@ -221,6 +229,15 @@ impl KEM {
221229
));
222230
}
223231
}
232+
KEM::MLKEM1024 => {
233+
if !key.is_instance(&types::MLKEM1024_PUBLIC_KEY.get(py)?)? {
234+
return Err(CryptographyError::from(
235+
pyo3::exceptions::PyTypeError::new_err(
236+
"Expected MLKEM1024PublicKey for KEM.MLKEM1024",
237+
),
238+
));
239+
}
240+
}
224241
}
225242
Ok(())
226243
}
@@ -270,6 +287,15 @@ impl KEM {
270287
));
271288
}
272289
}
290+
KEM::MLKEM1024 => {
291+
if !key.is_instance(&types::MLKEM1024_PRIVATE_KEY.get(py)?)? {
292+
return Err(CryptographyError::from(
293+
pyo3::exceptions::PyTypeError::new_err(
294+
"Expected MLKEM1024PrivateKey for KEM.MLKEM1024",
295+
),
296+
));
297+
}
298+
}
273299
}
274300
Ok(())
275301
}
@@ -284,7 +310,7 @@ impl KEM {
284310
pyo3::Bound<'p, pyo3::types::PyBytes>,
285311
)> {
286312
match self {
287-
KEM::MLKEM768 => {
313+
KEM::MLKEM768 | KEM::MLKEM1024 => {
288314
let result = pk_r.call_method0(pyo3::intern!(py, "encapsulate"))?;
289315
Ok(result.extract()?)
290316
}
@@ -302,7 +328,7 @@ impl KEM {
302328
kem_suite_id: &[u8; 5],
303329
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
304330
match self {
305-
KEM::MLKEM768 => {
331+
KEM::MLKEM768 | KEM::MLKEM1024 => {
306332
let enc_bytes = pyo3::types::PyBytes::new(py, enc);
307333
Ok(sk_r
308334
.call_method1(pyo3::intern!(py, "decapsulate"), (enc_bytes,))?
@@ -446,8 +472,8 @@ impl KEM {
446472
.into_any(),
447473
)
448474
}
449-
KEM::MLKEM768 => {
450-
unreachable!("ML-KEM-768 does not generate an ephemeral DH key")
475+
KEM::MLKEM768 | KEM::MLKEM1024 => {
476+
unreachable!("ML-KEM does not generate an ephemeral DH key")
451477
}
452478
}
453479
}
@@ -470,8 +496,8 @@ impl KEM {
470496
),
471497
)?
472498
.extract()?),
473-
KEM::MLKEM768 => {
474-
unreachable!("ML-KEM-768 public keys are not serialized via this path")
499+
KEM::MLKEM768 | KEM::MLKEM1024 => {
500+
unreachable!("ML-KEM public keys are not serialized via this path")
475501
}
476502
}
477503
}
@@ -495,8 +521,8 @@ impl KEM {
495521
let secp521r1 = types::SECP521R1.get(py)?.call0()?;
496522
Ok(pyo3::Bound::new(py, ec::from_public_bytes(py, secp521r1, data)?)?.into_any())
497523
}
498-
KEM::MLKEM768 => {
499-
unreachable!("ML-KEM-768 encapsulated key is a ciphertext, not a public key")
524+
KEM::MLKEM768 | KEM::MLKEM1024 => {
525+
unreachable!("ML-KEM encapsulated key is a ciphertext, not a public key")
500526
}
501527
}
502528
}
@@ -515,8 +541,8 @@ impl KEM {
515541
let ecdh = types::ECDH.get(py)?.call0()?;
516542
Ok(private_key.call_method1(pyo3::intern!(py, "exchange"), (&ecdh, public_key))?)
517543
}
518-
KEM::MLKEM768 => {
519-
unreachable!("ML-KEM-768 does not perform a Diffie-Hellman exchange")
544+
KEM::MLKEM768 | KEM::MLKEM1024 => {
545+
unreachable!("ML-KEM does not perform a Diffie-Hellman exchange")
520546
}
521547
}
522548
}
@@ -529,8 +555,8 @@ impl KEM {
529555
KEM::X25519 | KEM::P256 => Ok(types::SHA256.get(py)?.call0()?),
530556
KEM::P384 => Ok(types::SHA384.get(py)?.call0()?),
531557
KEM::P521 => Ok(types::SHA512.get(py)?.call0()?),
532-
KEM::MLKEM768 => {
533-
unreachable!("ML-KEM-768 does not use a KEM hash algorithm")
558+
KEM::MLKEM768 | KEM::MLKEM1024 => {
559+
unreachable!("ML-KEM does not use a KEM hash algorithm")
534560
}
535561
}
536562
}
@@ -994,7 +1020,15 @@ mod tests {
9941020
}
9951021

9961022
#[test]
997-
#[should_panic(expected = "ML-KEM-768 does not generate an ephemeral DH key")]
1023+
fn test_mlkem1024_secret_length() {
1024+
assert_eq!(
1025+
KEM::MLKEM1024.secret_length(),
1026+
kem_params::MLKEM1024_NSECRET
1027+
);
1028+
}
1029+
1030+
#[test]
1031+
#[should_panic(expected = "ML-KEM does not generate an ephemeral DH key")]
9981032
fn test_mlkem768_generate_key_unreachable() {
9991033
pyo3::Python::initialize();
10001034

@@ -1004,7 +1038,7 @@ mod tests {
10041038
}
10051039

10061040
#[test]
1007-
#[should_panic(expected = "ML-KEM-768 public keys are not serialized via this path")]
1041+
#[should_panic(expected = "ML-KEM public keys are not serialized via this path")]
10081042
fn test_mlkem768_serialize_public_key_unreachable() {
10091043
pyo3::Python::initialize();
10101044

@@ -1015,7 +1049,7 @@ mod tests {
10151049
}
10161050

10171051
#[test]
1018-
#[should_panic(expected = "ML-KEM-768 encapsulated key is a ciphertext, not a public key")]
1052+
#[should_panic(expected = "ML-KEM encapsulated key is a ciphertext, not a public key")]
10191053
fn test_mlkem768_deserialize_public_key_unreachable() {
10201054
pyo3::Python::initialize();
10211055

@@ -1025,7 +1059,7 @@ mod tests {
10251059
}
10261060

10271061
#[test]
1028-
#[should_panic(expected = "ML-KEM-768 does not perform a Diffie-Hellman exchange")]
1062+
#[should_panic(expected = "ML-KEM does not perform a Diffie-Hellman exchange")]
10291063
fn test_mlkem768_exchange_unreachable() {
10301064
pyo3::Python::initialize();
10311065

@@ -1036,7 +1070,7 @@ mod tests {
10361070
}
10371071

10381072
#[test]
1039-
#[should_panic(expected = "ML-KEM-768 does not use a KEM hash algorithm")]
1073+
#[should_panic(expected = "ML-KEM does not use a KEM hash algorithm")]
10401074
fn test_mlkem768_kem_hash_algorithm_unreachable() {
10411075
pyo3::Python::initialize();
10421076

src/rust/src/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,14 @@ pub static MLKEM768_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
423423
"cryptography.hazmat.primitives.asymmetric.mlkem",
424424
&["MLKEM768PrivateKey"],
425425
);
426+
pub static MLKEM1024_PUBLIC_KEY: LazyPyImport = LazyPyImport::new(
427+
"cryptography.hazmat.primitives.asymmetric.mlkem",
428+
&["MLKEM1024PublicKey"],
429+
);
430+
pub static MLKEM1024_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
431+
"cryptography.hazmat.primitives.asymmetric.mlkem",
432+
&["MLKEM1024PrivateKey"],
433+
);
426434

427435
pub static ED25519_PRIVATE_KEY: LazyPyImport = LazyPyImport::new(
428436
"cryptography.hazmat.primitives.asymmetric.ed25519",

0 commit comments

Comments
 (0)