From 07802fbdeb9aae9cc11a9e3ea94803e0379fe297 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Fri, 30 Jan 2026 17:34:08 -0700 Subject: [PATCH] ml-kem: replace `EncodedSizeUser` with `ExpandedKeyEncoding` First, this commit completely migrates `EncapsulationKey` to using only the `kem`/`crypto-common` traits: `KeySizeUser`, `TryKeyInit`, `KeyExport`, and changes any remaining uses to use only the new traits. Since `DecapsulationKey` uses those same traits for handling `Seed`s, the only remaining use of the old `EncodedSizeUser` trait is handling the expanded form of `DecapsulationKey`. So this commit repurposes it into an `ExpandedKeyEncoding` trait. Like `DecapsulationKey::from_expanded`, the trait has been marked deprecated with a rationale given in the documentation for `ExpandedKeyEncoding`, namely that the expanded form has only disadvantages when compared to seeds which are significantly smaller, uniformly sized, and avoid the need to do expanded key validation. It also notes several ML-KEM libraries have dropped support entirely. In the `ml-kem` crate, for now, we still need this functionality if only for tests which have been written generically, including but not limited to the ones that run the NIST ACVP vectors. --- ml-kem/benches/mlkem.rs | 2 +- ml-kem/src/decapsulation_key.rs | 96 +++++++++++++++++++++++++-------- ml-kem/src/encapsulation_key.rs | 31 +++++------ ml-kem/src/lib.rs | 73 +++++++++++++------------ ml-kem/src/pkcs8.rs | 4 +- ml-kem/src/traits.rs | 41 -------------- ml-kem/tests/encap-decap.rs | 11 ++-- ml-kem/tests/key-gen.rs | 19 +++---- ml-kem/tests/wycheproof.rs | 11 +++- x-wing/src/lib.rs | 6 +-- 10 files changed, 150 insertions(+), 144 deletions(-) delete mode 100644 ml-kem/src/traits.rs diff --git a/ml-kem/benches/mlkem.rs b/ml-kem/benches/mlkem.rs index e7934f8..fef4e5b 100644 --- a/ml-kem/benches/mlkem.rs +++ b/ml-kem/benches/mlkem.rs @@ -1,4 +1,4 @@ -use ::kem::{Decapsulate, Encapsulate, Kem, KeyExport, KeyInit, TryKeyInit}; +use ::kem::{Decapsulate, Encapsulate, Kem, KeyExport, KeyInit}; use core::hint::black_box; use criterion::{Criterion, criterion_group, criterion_main}; use getrandom::SysRng; diff --git a/ml-kem/src/decapsulation_key.rs b/ml-kem/src/decapsulation_key.rs index 7c8ce65..aa17487 100644 --- a/ml-kem/src/decapsulation_key.rs +++ b/ml-kem/src/decapsulation_key.rs @@ -1,11 +1,14 @@ use crate::{ - B32, EncapsulationKey, Encoded, EncodedSizeUser, ExpandedDecapsulationKey, Seed, SharedKey, + B32, EncapsulationKey, Seed, SharedKey, crypto::{G, J}, - kem::{Generate, InvalidKey, Kem, KeyInit, KeySizeUser}, - param::{DecapsulationKeySize, KemParams}, + kem::{Generate, InvalidKey, Kem, KeyExport, KeyInit, KeySizeUser}, + param::{DecapsulationKeySize, ExpandedDecapsulationKey, KemParams}, pke::{DecryptionKey, EncryptionKey}, }; -use array::sizes::{U32, U64}; +use array::{ + Array, ArraySize, + sizes::{U32, U64}, +}; use kem::{Ciphertext, Decapsulate}; use rand_core::{TryCryptoRng, TryRng}; use subtle::{ConditionallySelectable, ConstantTimeEq}; @@ -41,7 +44,7 @@ where /// Initialize a [`DecapsulationKey`] from the serialized expanded key form. /// /// Note that this form is deprecated in practice; prefer to use - /// [`DecapsulationKey::from_seed`]. + /// [`DecapsulationKey::from_seed`]. See [`ExpandedKeyEncoding`] for more information. /// /// # Errors /// - Returns [`InvalidKey`] in the event the expanded key failed validation @@ -164,24 +167,6 @@ where } } -impl

EncodedSizeUser for DecapsulationKey

-where - P: KemParams, -{ - type EncodedSize = DecapsulationKeySize

; - - fn from_encoded_bytes(expanded: &Encoded) -> Result { - #[allow(deprecated)] - Self::from_expanded(expanded) - } - - fn to_encoded_bytes(&self) -> Encoded { - let dk_pke = self.dk_pke.to_bytes(); - let ek = self.ek.to_encoded_bytes(); - P::concat_dk(dk_pke, ek, self.ek.h(), self.z.clone()) - } -} - impl

Generate for DecapsulationKey

where P: KemParams, @@ -210,3 +195,68 @@ where Self::from_seed(*seed) } } + +/// DEPRECATED: support for encoding and decoding [`DecapsulationKey`]s in the legacy expanded form, +/// as opposed to the more widely adopted [`Seed`] form. +/// +/// The expanded encoding format is problematic for several reasons, notably they need to validated +/// whereas generation from seeds is always correct, meaning there is no performance advantage to +/// using them, only additional complexity. +/// +/// They are significantly larger than seeds (which are 64-bytes) and their sizes vary depending on +/// security level whereas the size of a seed is constant: +/// - ML-KEM-512: 1632 bytes +/// - ML-KEM-768: 2400 bytes +/// - ML-KEM-1024: 3168 bytes +/// +/// Many ML-KEM libraries have dropped support for this format entirely. +#[deprecated(since = "0.3.0", note = "use `DecapsulationKey::from_seed` instead")] +pub trait ExpandedKeyEncoding: Sized { + /// The size of an expanded decapsulation key. + type EncodedSize: ArraySize; + + /// Parse a [`DecapsulationKey`] from its legacy expanded form. + /// + /// # Errors + /// - If the key fails to validate successfully. + fn from_expanded_bytes(enc: &Array) -> Result; + + /// Serialize a [`DecapsulationKey`] to its legacy expanded form. + fn to_expanded_bytes(&self) -> Array; +} + +#[allow(deprecated)] +impl

ExpandedKeyEncoding for DecapsulationKey

+where + P: KemParams, +{ + type EncodedSize = DecapsulationKeySize

; + + fn from_expanded_bytes(expanded: &ExpandedDecapsulationKey

) -> Result { + Self::from_expanded(expanded) + } + + fn to_expanded_bytes(&self) -> ExpandedDecapsulationKey

{ + let dk_pke = self.dk_pke.to_bytes(); + let ek = self.ek.to_bytes(); + P::concat_dk(dk_pke, ek, self.ek.h(), self.z.clone()) + } +} + +/// Initialize a KEM from a seed. +pub trait FromSeed: Kem { + /// Using the provided [`Seed`] value, create a KEM keypair. + fn from_seed(seed: &Seed) -> (Self::DecapsulationKey, Self::EncapsulationKey); +} + +impl FromSeed for K +where + K: Kem, + K::DecapsulationKey: KeyInit + KeySizeUser, +{ + fn from_seed(seed: &Seed) -> (K::DecapsulationKey, K::EncapsulationKey) { + let dk = K::DecapsulationKey::new(seed); + let ek = dk.as_ref().clone(); + (dk, ek) + } +} diff --git a/ml-kem/src/encapsulation_key.rs b/ml-kem/src/encapsulation_key.rs index 8d2d2ca..58ee4e9 100644 --- a/ml-kem/src/encapsulation_key.rs +++ b/ml-kem/src/encapsulation_key.rs @@ -1,5 +1,5 @@ use crate::{ - B32, Encoded, EncodedSizeUser, SharedKey, + B32, SharedKey, crypto::{G, H}, kem::{InvalidKey, Kem, Key, KeyExport, KeySizeUser, TryKeyInit}, param::{EncapsulationKeySize, KemParams}, @@ -24,6 +24,16 @@ impl

EncapsulationKey

where P: Kem + KemParams, { + /// Create a new [`EncapsulationKey`] from its serialized form. + /// + /// # Errors + /// If the key failed validation during decoding. + pub fn new(encapsulation_key: &Key) -> Result { + EncryptionKey::from_bytes(encapsulation_key) + .map(Self::from_encryption_key) + .map_err(|_| InvalidKey) + } + /// Encapsulates with the given randomness. This is useful for testing against known vectors. /// /// # Warning @@ -66,21 +76,6 @@ where } } -impl

EncodedSizeUser for EncapsulationKey

-where - P: KemParams, -{ - type EncodedSize = EncapsulationKeySize

; - - fn from_encoded_bytes(enc: &Encoded) -> Result { - Ok(Self::from_encryption_key(EncryptionKey::from_bytes(enc)?)) - } - - fn to_encoded_bytes(&self) -> Encoded { - self.ek_pke.to_bytes() - } -} - impl

KeyExport for EncapsulationKey

where P: KemParams, @@ -102,9 +97,7 @@ where P: KemParams, { fn new(encapsulation_key: &Key) -> Result { - EncryptionKey::from_bytes(encapsulation_key) - .map(Self::from_encryption_key) - .map_err(|_| InvalidKey) + Self::new(encapsulation_key) } } diff --git a/ml-kem/src/lib.rs b/ml-kem/src/lib.rs index a08bea1..728a1f2 100644 --- a/ml-kem/src/lib.rs +++ b/ml-kem/src/lib.rs @@ -68,13 +68,13 @@ mod pke; /// Section 7. Parameter Sets mod param; +// PKCS#8 key encoding support (doc comments in module) pub mod pkcs8; -/// Trait definitions -mod traits; - pub use array; -pub use decapsulation_key::DecapsulationKey; +#[allow(deprecated)] +pub use decapsulation_key::ExpandedKeyEncoding; +pub use decapsulation_key::{DecapsulationKey, FromSeed}; pub use encapsulation_key::EncapsulationKey; pub use kem::{ self, Ciphertext, Decapsulate, Encapsulate, Generate, InvalidKey, Kem, Key, KeyExport, KeyInit, @@ -85,7 +85,6 @@ pub use ml_kem_768::MlKem768; pub use ml_kem_1024::MlKem1024; pub use module_lattice::encoding::ArraySize; pub use param::{ExpandedDecapsulationKey, ParameterSet}; -pub use traits::*; use array::{ Array, @@ -297,21 +296,41 @@ mod test { round_trip_test::(); } + fn seed_test

() + where + P: KemParams, + { + let mut rng = UnwrapErr(SysRng); + let mut seed = Seed::default(); + rng.try_fill_bytes(&mut seed).unwrap(); + + let dk = DecapsulationKey::

::from_seed(seed.clone()); + let seed_encoded = dk.to_seed().unwrap(); + assert_eq!(seed, seed_encoded); + + let ek_original = dk.encapsulation_key(); + let ek_encoded = ek_original.to_bytes(); + let ek_decoded = EncapsulationKey::new(&ek_encoded).unwrap(); + assert_eq!(ek_original, &ek_decoded); + } + + #[test] + fn seed() { + seed_test::(); + seed_test::(); + seed_test::(); + } + + #[allow(deprecated)] fn expanded_key_test

() where P: KemParams, { let mut rng = UnwrapErr(SysRng); let dk_original = DecapsulationKey::

::generate_from_rng(&mut rng); - let ek_original = dk_original.encapsulation_key().clone(); - - let dk_encoded = dk_original.to_encoded_bytes(); - let dk_decoded = DecapsulationKey::from_encoded_bytes(&dk_encoded).unwrap(); + let dk_encoded = dk_original.to_expanded_bytes(); + let dk_decoded = DecapsulationKey::from_expanded_bytes(&dk_encoded).unwrap(); assert_eq!(dk_original, dk_decoded); - - let ek_encoded = ek_original.to_encoded_bytes(); - let ek_decoded = EncapsulationKey::from_encoded_bytes(&ek_encoded).unwrap(); - assert_eq!(ek_original, ek_decoded); } #[test] @@ -328,13 +347,17 @@ mod test { let mut rng = UnwrapErr(SysRng); let dk_original = DecapsulationKey::

::generate_from_rng(&mut rng); - let mut dk_encoded = dk_original.to_encoded_bytes(); + #[allow(deprecated)] + let mut dk_encoded = dk_original.to_expanded_bytes(); + // Corrupt the hash value let hash_offset = P::NttVectorSize::USIZE + P::EncryptionKeySize::USIZE; dk_encoded[hash_offset] ^= 0xFF; + #[allow(deprecated)] let dk_decoded: Result, InvalidKey> = - DecapsulationKey::from_encoded_bytes(&dk_encoded); + DecapsulationKey::from_expanded_bytes(&dk_encoded); + assert!(dk_decoded.is_err()); } @@ -345,26 +368,6 @@ mod test { invalid_hash_expanded_key_test::(); } - fn seed_test

() - where - P: KemParams, - { - let mut rng = UnwrapErr(SysRng); - let mut seed = Seed::default(); - rng.try_fill_bytes(&mut seed).unwrap(); - - let dk = DecapsulationKey::

::from_seed(seed.clone()); - let seed_encoded = dk.to_seed().unwrap(); - assert_eq!(seed, seed_encoded); - } - - #[test] - fn seed() { - seed_test::(); - seed_test::(); - seed_test::(); - } - fn key_inequality_test

() where P: KemParams, diff --git a/ml-kem/src/pkcs8.rs b/ml-kem/src/pkcs8.rs index 1f3c992..4fd7646 100644 --- a/ml-kem/src/pkcs8.rs +++ b/ml-kem/src/pkcs8.rs @@ -31,7 +31,7 @@ use array::Array; #[cfg(feature = "alloc")] use { - crate::EncodedSizeUser, + ::kem::KeyExport, ::pkcs8::der::{Encode, TagMode, asn1::BitStringRef}, }; @@ -100,7 +100,7 @@ where /// Serialize the given `EncapsulationKey` into DER format. /// Returns a `Document` which wraps the DER document in case of success. fn to_public_key_der(&self) -> spki::Result { - let public_key = self.to_encoded_bytes(); + let public_key = self.to_bytes(); let subject_public_key = BitStringRef::new(0, &public_key)?; ::pkcs8::SubjectPublicKeyInfo { diff --git a/ml-kem/src/traits.rs b/ml-kem/src/traits.rs deleted file mode 100644 index 5be5bb7..0000000 --- a/ml-kem/src/traits.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Trait definitions - -use crate::{ArraySize, Seed}; -use array::{Array, sizes::U64}; -use kem::{InvalidKey, Kem, KeyInit, KeySizeUser}; - -/// An object that knows what size it is -pub trait EncodedSizeUser: Sized { - /// The size of an encoded object - type EncodedSize: ArraySize; - - /// Parse an object from its encoded form - /// - /// # Errors - /// - If the object failed to decode successfully - fn from_encoded_bytes(enc: &Encoded) -> Result; - - /// Serialize an object to its encoded form - fn to_encoded_bytes(&self) -> Encoded; -} - -/// A byte array encoding a value the indicated size -pub type Encoded = Array::EncodedSize>; - -/// Initialize a KEM from a seed. -pub trait FromSeed: Kem { - /// Using the provided [`Seed`] value, create a KEM keypair. - fn from_seed(seed: &Seed) -> (Self::DecapsulationKey, Self::EncapsulationKey); -} - -impl FromSeed for K -where - K: Kem, - K::DecapsulationKey: KeyInit + KeySizeUser, -{ - fn from_seed(seed: &Seed) -> (K::DecapsulationKey, K::EncapsulationKey) { - let dk = K::DecapsulationKey::new(seed); - let ek = dk.as_ref().clone(); - (dk, ek) - } -} diff --git a/ml-kem/tests/encap-decap.rs b/ml-kem/tests/encap-decap.rs index 0ec9dcb..f3f9eab 100644 --- a/ml-kem/tests/encap-decap.rs +++ b/ml-kem/tests/encap-decap.rs @@ -63,11 +63,10 @@ fn verify_encap_group(tg: &acvp::EncapTestGroup) { fn verify_encap(tc: &acvp::EncapTestCase) where K: Kem, - K::EncapsulationKey: EncapsulateDeterministic + EncodedSizeUser, + K::EncapsulationKey: EncapsulateDeterministic + TryKeyInit, { let m = Array::try_from(tc.m.as_slice()).unwrap(); - let ek_bytes = Encoded::::try_from(tc.ek.as_slice()).unwrap(); - let ek = K::EncapsulationKey::from_encoded_bytes(&ek_bytes).unwrap(); + let ek = K::EncapsulationKey::new_from_slice(&tc.ek).unwrap(); let (c, k) = ek.encapsulate_deterministic(&m); @@ -85,13 +84,13 @@ fn verify_decap_group(tg: &acvp::DecapTestGroup) { } } +#[allow(deprecated)] fn verify_decap(tc: &acvp::DecapTestCase, dk_slice: &[u8]) where K: Kem, - K::DecapsulationKey: Decapsulate + EncodedSizeUser, + K::DecapsulationKey: Decapsulate + ExpandedKeyEncoding, { - let dk_bytes = Encoded::::try_from(dk_slice).unwrap(); - let dk = K::DecapsulationKey::from_encoded_bytes(&dk_bytes).unwrap(); + let dk = K::DecapsulationKey::from_expanded_bytes(dk_slice.try_into().unwrap()).unwrap(); let c = ::kem::Ciphertext::::try_from(tc.c.as_slice()).unwrap(); let k = dk.decapsulate(&c); diff --git a/ml-kem/tests/key-gen.rs b/ml-kem/tests/key-gen.rs index 2ffbc91..5945975 100644 --- a/ml-kem/tests/key-gen.rs +++ b/ml-kem/tests/key-gen.rs @@ -25,33 +25,28 @@ fn acvp_key_gen() { } } +#[allow(deprecated)] fn verify(tc: &acvp::TestCase) where K: Kem + FromSeed, - K::DecapsulationKey: EncodedSizeUser + Debug + PartialEq, - K::EncapsulationKey: EncodedSizeUser, + K::DecapsulationKey: ExpandedKeyEncoding + Debug + PartialEq, + K::EncapsulationKey: KeySizeUser, { // Import test data into the relevant array structures let d = ArrayN::::try_from(tc.d.as_slice()).unwrap(); let z = ArrayN::::try_from(tc.z.as_slice()).unwrap(); - let dk_bytes = Encoded::::try_from(tc.dk.as_slice()).unwrap(); - let ek_bytes = Encoded::::try_from(tc.ek.as_slice()).unwrap(); - let (dk, ek) = K::from_seed(&d.concat(z)); // Verify correctness via serialization - assert_eq!(dk.to_encoded_bytes().as_slice(), tc.dk.as_slice()); - assert_eq!(ek.to_encoded_bytes().as_slice(), tc.ek.as_slice()); + assert_eq!(dk.to_expanded_bytes().as_slice(), tc.dk.as_slice()); + assert_eq!(ek.to_bytes().as_slice(), tc.ek.as_slice()); // Verify correctness via deserialization assert_eq!( dk, - K::DecapsulationKey::from_encoded_bytes(&dk_bytes).unwrap() - ); - assert_eq!( - ek, - K::EncapsulationKey::from_encoded_bytes(&ek_bytes).unwrap() + K::DecapsulationKey::from_expanded_bytes(tc.dk.as_slice().try_into().unwrap()).unwrap() ); + assert_eq!(ek, K::EncapsulationKey::new_from_slice(&tc.ek).unwrap()); } mod acvp { diff --git a/ml-kem/tests/wycheproof.rs b/ml-kem/tests/wycheproof.rs index 0e48191..8882bae 100644 --- a/ml-kem/tests/wycheproof.rs +++ b/ml-kem/tests/wycheproof.rs @@ -1,8 +1,10 @@ //! Test against the Wycheproof test vectors. use array::{Array, ArraySize}; +#[allow(deprecated)] +use ml_kem::ExpandedKeyEncoding; use ml_kem::{ - EncodedSizeUser, FromSeed, MlKem512, MlKem768, MlKem1024, + FromSeed, MlKem512, MlKem768, MlKem1024, kem::{Decapsulate, KeyExport, TryKeyInit}, }; use serde::Deserialize; @@ -149,7 +151,12 @@ macro_rules! mlkem_keygen_seed_test { let test_dk = decode_expected_hex(&test.dk, "dk"); let (dk, ek) = $kem::from_seed(&test_seed); - assert_eq!(test_dk, dk.to_encoded_bytes()); + + #[allow(deprecated)] + { + assert_eq!(test_dk, dk.to_expanded_bytes()); + } + assert_eq!(test.ek.as_slice(), ek.to_bytes().as_slice()); } } diff --git a/x-wing/src/lib.rs b/x-wing/src/lib.rs index aa8bee1..98b2220 100644 --- a/x-wing/src/lib.rs +++ b/x-wing/src/lib.rs @@ -34,7 +34,7 @@ pub use kem::{ }; use ml_kem::{ - EncodedSizeUser, FromSeed, MlKem768, + FromSeed, MlKem768, array::{ Array, ArrayN, AsArrayRef, sizes::{U32, U1120, U1184, U1216}, @@ -154,7 +154,7 @@ impl KeyExport for EncapsulationKey { fn to_bytes(&self) -> Key { let mut key_bytes = Key::::default(); let (m, x) = key_bytes.split_at_mut(1184); - m.copy_from_slice(&self.pk_m.to_encoded_bytes()); + m.copy_from_slice(&self.pk_m.to_bytes()); x.copy_from_slice(self.pk_x.as_bytes()); key_bytes } @@ -164,7 +164,7 @@ impl TryKeyInit for EncapsulationKey { fn new(key_bytes: &Key) -> Result { let (m_bytes, x_bytes) = key_bytes.split_ref::(); - let pk_m = MlKem768EncapsulationKey::from_encoded_bytes(m_bytes).map_err(|_| InvalidKey)?; + let pk_m = MlKem768EncapsulationKey::new(m_bytes)?; let pk_x = PublicKey::from(x_bytes.0); Ok(EncapsulationKey { pk_m, pk_x })