diff --git a/docs/hazmat/primitives/hpke.rst b/docs/hazmat/primitives/hpke.rst index 58d83ef5e3d8..60e101003b2d 100644 --- a/docs/hazmat/primitives/hpke.rst +++ b/docs/hazmat/primitives/hpke.rst @@ -98,6 +98,11 @@ specifying auxiliary authenticated information. ML-KEM-768. Post-quantum secure. Only available on backends that support ML-KEM. + .. attribute:: MLKEM1024 + + ML-KEM-1024. Post-quantum secure. Only available on backends that + support ML-KEM. + .. class:: KDF An enumeration of key derivation functions. diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi index 1540c5832fa1..21b01484865f 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/hpke.pyi @@ -11,6 +11,7 @@ class KEM: P384: KEM P521: KEM MLKEM768: KEM + MLKEM1024: KEM class KDF: HKDF_SHA256: KDF @@ -31,7 +32,8 @@ class Suite: plaintext: Buffer, public_key: x25519.X25519PublicKey | ec.EllipticCurvePublicKey - | mlkem.MLKEM768PublicKey, + | mlkem.MLKEM768PublicKey + | mlkem.MLKEM1024PublicKey, info: Buffer | None = None, ) -> bytes: ... def decrypt( @@ -39,7 +41,8 @@ class Suite: ciphertext: Buffer, private_key: x25519.X25519PrivateKey | ec.EllipticCurvePrivateKey - | mlkem.MLKEM768PrivateKey, + | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey, info: Buffer | None = None, ) -> bytes: ... @@ -48,7 +51,8 @@ def _encrypt_with_aad( plaintext: Buffer, public_key: x25519.X25519PublicKey | ec.EllipticCurvePublicKey - | mlkem.MLKEM768PublicKey, + | mlkem.MLKEM768PublicKey + | mlkem.MLKEM1024PublicKey, info: Buffer | None = None, aad: Buffer | None = None, ) -> bytes: ... @@ -57,7 +61,8 @@ def _decrypt_with_aad( ciphertext: Buffer, private_key: x25519.X25519PrivateKey | ec.EllipticCurvePrivateKey - | mlkem.MLKEM768PrivateKey, + | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey, info: Buffer | None = None, aad: Buffer | None = None, ) -> bytes: ... diff --git a/src/rust/src/backend/hpke.rs b/src/rust/src/backend/hpke.rs index f6bb06a1ec91..212420039778 100644 --- a/src/rust/src/backend/hpke.rs +++ b/src/rust/src/backend/hpke.rs @@ -45,6 +45,10 @@ mod kem_params { pub const MLKEM768_ID: u16 = 0x0041; pub const MLKEM768_NSECRET: usize = 32; pub const MLKEM768_NENC: usize = 1088; + + pub const MLKEM1024_ID: u16 = 0x0042; + pub const MLKEM1024_NSECRET: usize = 32; + pub const MLKEM1024_NENC: usize = 1568; } mod kdf_params { @@ -89,6 +93,7 @@ pub(crate) enum KEM { P384, P521, MLKEM768, + MLKEM1024, } impl KEM { @@ -153,6 +158,7 @@ impl KEM { KEM::P384 => kem_params::P384_ID, KEM::P521 => kem_params::P521_ID, KEM::MLKEM768 => kem_params::MLKEM768_ID, + KEM::MLKEM1024 => kem_params::MLKEM1024_ID, } } @@ -163,6 +169,7 @@ impl KEM { KEM::P384 => kem_params::P384_NSECRET, KEM::P521 => kem_params::P521_NSECRET, KEM::MLKEM768 => kem_params::MLKEM768_NSECRET, + KEM::MLKEM1024 => kem_params::MLKEM1024_NSECRET, } } @@ -173,6 +180,7 @@ impl KEM { KEM::P384 => kem_params::P384_NENC, KEM::P521 => kem_params::P521_NENC, KEM::MLKEM768 => kem_params::MLKEM768_NENC, + KEM::MLKEM1024 => kem_params::MLKEM1024_NENC, } } @@ -221,6 +229,15 @@ impl KEM { )); } } + KEM::MLKEM1024 => { + if !key.is_instance(&types::MLKEM1024_PUBLIC_KEY.get(py)?)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "Expected MLKEM1024PublicKey for KEM.MLKEM1024", + ), + )); + } + } } Ok(()) } @@ -270,6 +287,15 @@ impl KEM { )); } } + KEM::MLKEM1024 => { + if !key.is_instance(&types::MLKEM1024_PRIVATE_KEY.get(py)?)? { + return Err(CryptographyError::from( + pyo3::exceptions::PyTypeError::new_err( + "Expected MLKEM1024PrivateKey for KEM.MLKEM1024", + ), + )); + } + } } Ok(()) } @@ -284,7 +310,7 @@ impl KEM { pyo3::Bound<'p, pyo3::types::PyBytes>, )> { match self { - KEM::MLKEM768 => { + KEM::MLKEM768 | KEM::MLKEM1024 => { let result = pk_r.call_method0(pyo3::intern!(py, "encapsulate"))?; Ok(result.extract()?) } @@ -302,7 +328,7 @@ impl KEM { kem_suite_id: &[u8; 5], ) -> CryptographyResult> { match self { - KEM::MLKEM768 => { + KEM::MLKEM768 | KEM::MLKEM1024 => { let enc_bytes = pyo3::types::PyBytes::new(py, enc); Ok(sk_r .call_method1(pyo3::intern!(py, "decapsulate"), (enc_bytes,))? @@ -446,8 +472,8 @@ impl KEM { .into_any(), ) } - KEM::MLKEM768 => { - unreachable!("ML-KEM-768 does not generate an ephemeral DH key") + KEM::MLKEM768 | KEM::MLKEM1024 => { + unreachable!("ML-KEM does not generate an ephemeral DH key") } } } @@ -470,8 +496,8 @@ impl KEM { ), )? .extract()?), - KEM::MLKEM768 => { - unreachable!("ML-KEM-768 public keys are not serialized via this path") + KEM::MLKEM768 | KEM::MLKEM1024 => { + unreachable!("ML-KEM public keys are not serialized via this path") } } } @@ -495,8 +521,8 @@ impl KEM { let secp521r1 = types::SECP521R1.get(py)?.call0()?; Ok(pyo3::Bound::new(py, ec::from_public_bytes(py, secp521r1, data)?)?.into_any()) } - KEM::MLKEM768 => { - unreachable!("ML-KEM-768 encapsulated key is a ciphertext, not a public key") + KEM::MLKEM768 | KEM::MLKEM1024 => { + unreachable!("ML-KEM encapsulated key is a ciphertext, not a public key") } } } @@ -515,8 +541,8 @@ impl KEM { let ecdh = types::ECDH.get(py)?.call0()?; Ok(private_key.call_method1(pyo3::intern!(py, "exchange"), (&ecdh, public_key))?) } - KEM::MLKEM768 => { - unreachable!("ML-KEM-768 does not perform a Diffie-Hellman exchange") + KEM::MLKEM768 | KEM::MLKEM1024 => { + unreachable!("ML-KEM does not perform a Diffie-Hellman exchange") } } } @@ -529,8 +555,8 @@ impl KEM { KEM::X25519 | KEM::P256 => Ok(types::SHA256.get(py)?.call0()?), KEM::P384 => Ok(types::SHA384.get(py)?.call0()?), KEM::P521 => Ok(types::SHA512.get(py)?.call0()?), - KEM::MLKEM768 => { - unreachable!("ML-KEM-768 does not use a KEM hash algorithm") + KEM::MLKEM768 | KEM::MLKEM1024 => { + unreachable!("ML-KEM does not use a KEM hash algorithm") } } } @@ -994,7 +1020,15 @@ mod tests { } #[test] - #[should_panic(expected = "ML-KEM-768 does not generate an ephemeral DH key")] + fn test_mlkem1024_secret_length() { + assert_eq!( + KEM::MLKEM1024.secret_length(), + kem_params::MLKEM1024_NSECRET + ); + } + + #[test] + #[should_panic(expected = "ML-KEM does not generate an ephemeral DH key")] fn test_mlkem768_generate_key_unreachable() { pyo3::Python::initialize(); @@ -1004,7 +1038,7 @@ mod tests { } #[test] - #[should_panic(expected = "ML-KEM-768 public keys are not serialized via this path")] + #[should_panic(expected = "ML-KEM public keys are not serialized via this path")] fn test_mlkem768_serialize_public_key_unreachable() { pyo3::Python::initialize(); @@ -1015,7 +1049,7 @@ mod tests { } #[test] - #[should_panic(expected = "ML-KEM-768 encapsulated key is a ciphertext, not a public key")] + #[should_panic(expected = "ML-KEM encapsulated key is a ciphertext, not a public key")] fn test_mlkem768_deserialize_public_key_unreachable() { pyo3::Python::initialize(); @@ -1025,7 +1059,7 @@ mod tests { } #[test] - #[should_panic(expected = "ML-KEM-768 does not perform a Diffie-Hellman exchange")] + #[should_panic(expected = "ML-KEM does not perform a Diffie-Hellman exchange")] fn test_mlkem768_exchange_unreachable() { pyo3::Python::initialize(); @@ -1036,7 +1070,7 @@ mod tests { } #[test] - #[should_panic(expected = "ML-KEM-768 does not use a KEM hash algorithm")] + #[should_panic(expected = "ML-KEM does not use a KEM hash algorithm")] fn test_mlkem768_kem_hash_algorithm_unreachable() { pyo3::Python::initialize(); diff --git a/src/rust/src/types.rs b/src/rust/src/types.rs index 14c69775f1f1..9badc2511a28 100644 --- a/src/rust/src/types.rs +++ b/src/rust/src/types.rs @@ -423,6 +423,14 @@ pub static MLKEM768_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.asymmetric.mlkem", &["MLKEM768PrivateKey"], ); +pub static MLKEM1024_PUBLIC_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mlkem", + &["MLKEM1024PublicKey"], +); +pub static MLKEM1024_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( + "cryptography.hazmat.primitives.asymmetric.mlkem", + &["MLKEM1024PrivateKey"], +); pub static ED25519_PRIVATE_KEY: LazyPyImport = LazyPyImport::new( "cryptography.hazmat.primitives.asymmetric.ed25519", diff --git a/tests/hazmat/primitives/test_hpke.py b/tests/hazmat/primitives/test_hpke.py index 0cfeaf6d146e..c0888b077e89 100644 --- a/tests/hazmat/primitives/test_hpke.py +++ b/tests/hazmat/primitives/test_hpke.py @@ -28,10 +28,18 @@ P384_ENC_LENGTH = 97 P521_ENC_LENGTH = 133 MLKEM768_ENC_LENGTH = 1088 +MLKEM1024_ENC_LENGTH = 1568 SUPPORTED_SUITES = list( itertools.product( - [KEM.X25519, KEM.P256, KEM.P384, KEM.P521, KEM.MLKEM768], + [ + KEM.X25519, + KEM.P256, + KEM.P384, + KEM.P521, + KEM.MLKEM768, + KEM.MLKEM1024, + ], [ KDF.HKDF_SHA256, KDF.HKDF_SHA384, @@ -71,7 +79,10 @@ def test_roundtrip(self, backend, kem, kdf, aead): hashes.SHAKE256(digest_size=64) ): pytest.skip("SHAKE256 not supported") - if kem == KEM.MLKEM768 and not backend.mlkem_supported(): + if ( + kem in [KEM.MLKEM768, KEM.MLKEM1024] + and not backend.mlkem_supported() + ): pytest.skip("ML-KEM not supported") suite = Suite(kem, kdf, aead) @@ -79,6 +90,7 @@ def test_roundtrip(self, backend, kem, kdf, aead): x25519.X25519PrivateKey | ec.EllipticCurvePrivateKey | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey ) if kem == KEM.X25519: sk_r = x25519.X25519PrivateKey.generate() @@ -88,8 +100,10 @@ def test_roundtrip(self, backend, kem, kdf, aead): sk_r = ec.generate_private_key(ec.SECP384R1()) elif kem == KEM.P521: sk_r = ec.generate_private_key(ec.SECP521R1()) - else: + elif kem == KEM.MLKEM768: sk_r = mlkem.MLKEM768PrivateKey.generate() + else: + sk_r = mlkem.MLKEM1024PrivateKey.generate() pk_r = sk_r.public_key() ciphertext = suite.encrypt(b"Hello, HPKE!", pk_r, info=b"test") @@ -107,7 +121,10 @@ def test_roundtrip_no_info(self, backend, kem, kdf, aead): hashes.SHAKE256(digest_size=64) ): pytest.skip("SHAKE256 not supported") - if kem == KEM.MLKEM768 and not backend.mlkem_supported(): + if ( + kem in [KEM.MLKEM768, KEM.MLKEM1024] + and not backend.mlkem_supported() + ): pytest.skip("ML-KEM not supported") suite = Suite(kem, kdf, aead) @@ -115,6 +132,7 @@ def test_roundtrip_no_info(self, backend, kem, kdf, aead): x25519.X25519PrivateKey | ec.EllipticCurvePrivateKey | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey ) if kem == KEM.X25519: sk_r = x25519.X25519PrivateKey.generate() @@ -124,8 +142,10 @@ def test_roundtrip_no_info(self, backend, kem, kdf, aead): sk_r = ec.generate_private_key(ec.SECP384R1()) elif kem == KEM.P521: sk_r = ec.generate_private_key(ec.SECP521R1()) - else: + elif kem == KEM.MLKEM768: sk_r = mlkem.MLKEM768PrivateKey.generate() + else: + sk_r = mlkem.MLKEM1024PrivateKey.generate() pk_r = sk_r.public_key() ciphertext = suite.encrypt(b"Hello!", pk_r) @@ -337,9 +357,11 @@ def test_ciphertext_format_p521(self): # enc (133 bytes) + ct (4 bytes pt + 16 bytes tag) assert len(ciphertext) == P521_ENC_LENGTH + 4 + 16 - def test_ciphertext_format_mlkem768(self, backend): - if not backend.mlkem_supported(): - pytest.skip("ML-KEM not supported") + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_ciphertext_format_mlkem768(self): suite = Suite(KEM.MLKEM768, KDF.HKDF_SHA256, AEAD.AES_128_GCM) sk_r = mlkem.MLKEM768PrivateKey.generate() @@ -350,9 +372,11 @@ def test_ciphertext_format_mlkem768(self, backend): # enc (1088 bytes) + ct (4 bytes pt + 16 bytes tag) assert len(ciphertext) == MLKEM768_ENC_LENGTH + 4 + 16 - def test_wrong_key_mlkem768(self, backend): - if not backend.mlkem_supported(): - pytest.skip("ML-KEM not supported") + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_wrong_key_mlkem768(self): suite = Suite(KEM.MLKEM768, KDF.HKDF_SHA256, AEAD.AES_128_GCM) sk_r = mlkem.MLKEM768PrivateKey.generate() pk_r = sk_r.public_key() @@ -373,15 +397,77 @@ def test_wrong_key_mlkem768(self, backend): with pytest.raises(TypeError): suite.decrypt(ciphertext, x25519_sk) - def test_mlkem768_wrong_kem_with_ec(self, backend): - if not backend.mlkem_supported(): - pytest.skip("ML-KEM not supported") + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_mlkem768_wrong_kem_with_ec(self): # ML-KEM public key with EC-based KEM suite should fail suite = Suite(KEM.P256, KDF.HKDF_SHA256, AEAD.AES_128_GCM) mlkem_pk = mlkem.MLKEM768PrivateKey.generate().public_key() with pytest.raises(TypeError): suite.encrypt(b"test", mlkem_pk) + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_ciphertext_format_mlkem1024(self): + suite = Suite(KEM.MLKEM1024, KDF.HKDF_SHA256, AEAD.AES_128_GCM) + + sk_r = mlkem.MLKEM1024PrivateKey.generate() + pk_r = sk_r.public_key() + + ciphertext = suite.encrypt(b"test", pk_r) + + # enc (1568 bytes) + ct (4 bytes pt + 16 bytes tag) + assert len(ciphertext) == MLKEM1024_ENC_LENGTH + 4 + 16 + + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_wrong_key_mlkem1024(self): + suite = Suite(KEM.MLKEM1024, KDF.HKDF_SHA256, AEAD.AES_128_GCM) + sk_r = mlkem.MLKEM1024PrivateKey.generate() + pk_r = sk_r.public_key() + ciphertext = suite.encrypt(b"test", pk_r) + + # Wrong key of correct type + sk_wrong = mlkem.MLKEM1024PrivateKey.generate() + with pytest.raises(InvalidTag): + suite.decrypt(ciphertext, sk_wrong) + + # Wrong key type for encrypt + x25519_pk = x25519.X25519PrivateKey.generate().public_key() + with pytest.raises(TypeError): + suite.encrypt(b"test", x25519_pk) + + # Wrong key type for decrypt + x25519_sk = x25519.X25519PrivateKey.generate() + with pytest.raises(TypeError): + suite.decrypt(ciphertext, x25519_sk) + + # ML-KEM-768 key with ML-KEM-1024 suite should fail + mlkem768_pk = mlkem.MLKEM768PrivateKey.generate().public_key() + with pytest.raises(TypeError): + suite.encrypt(b"test", mlkem768_pk) + + mlkem768_sk = mlkem.MLKEM768PrivateKey.generate() + with pytest.raises(TypeError): + suite.decrypt(ciphertext, mlkem768_sk) + + @pytest.mark.supported( + only_if=lambda backend: backend.mlkem_supported(), + skip_message="Requires ML-KEM support", + ) + def test_mlkem1024_wrong_kem_with_ec(self): + # ML-KEM-1024 public key with EC-based KEM suite should fail + suite = Suite(KEM.P256, KDF.HKDF_SHA256, AEAD.AES_128_GCM) + mlkem_pk = mlkem.MLKEM1024PrivateKey.generate().public_key() + with pytest.raises(TypeError): + suite.encrypt(b"test", mlkem_pk) + def test_empty_plaintext(self): suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.AES_128_GCM) @@ -529,6 +615,7 @@ def test_vector_decryption(self, backend, subtests): 0x0012: KEM.P521, 0x0020: KEM.X25519, 0x0041: KEM.MLKEM768, + 0x0042: KEM.MLKEM1024, } kdf_map = { 0x0001: KDF.HKDF_SHA256, @@ -565,7 +652,10 @@ def test_vector_decryption(self, backend, subtests): hashes.SHAKE256(digest_size=64) ): continue - if kem == KEM.MLKEM768 and not backend.mlkem_supported(): + if ( + kem in [KEM.MLKEM768, KEM.MLKEM1024] + and not backend.mlkem_supported() + ): continue suite = Suite(kem, kdf, aead) @@ -575,6 +665,7 @@ def test_vector_decryption(self, backend, subtests): x25519.X25519PrivateKey | ec.EllipticCurvePrivateKey | mlkem.MLKEM768PrivateKey + | mlkem.MLKEM1024PrivateKey ) if kem == KEM.X25519: sk_r = x25519.X25519PrivateKey.from_private_bytes( @@ -589,8 +680,12 @@ def test_vector_decryption(self, backend, subtests): elif kem == KEM.P521: private_value = int.from_bytes(sk_r_bytes, "big") sk_r = ec.derive_private_key(private_value, ec.SECP521R1()) - else: + elif kem == KEM.MLKEM768: sk_r = mlkem.MLKEM768PrivateKey.from_seed_bytes(sk_r_bytes) + else: + sk_r = mlkem.MLKEM1024PrivateKey.from_seed_bytes( + sk_r_bytes + ) enc = bytes.fromhex(vector["enc"]) info = bytes.fromhex(vector["info"])