Skip to content

Commit 439978c

Browse files
authored
ml-kem: add Wycheproof mlkem_*_decaps_test (#215)
These tests mostly cover length handling which isn't particularly helpful because it's something the caller (or a trait impl) has to do. That said, it includes some nice changes to the test machinery, and we're now set up for further future decapsulation tests.
1 parent d8c7434 commit 439978c

2 files changed

Lines changed: 120 additions & 22 deletions

File tree

ml-kem/src/lib.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub type Seed = Array<u8, U64>;
9898
/// cipher with a 128-bit key.
9999
pub mod ml_kem_512 {
100100
use super::{Debug, ParameterSet, U2, U3, U4, U10, kem};
101+
use crate::param;
101102

102103
/// `MlKem512` is the parameter set for security category 1, corresponding to key search on a
103104
/// block cipher with a 128-bit key.
@@ -119,12 +120,21 @@ pub mod ml_kem_512 {
119120
/// An ML-KEM-512 `EncapsulationKey` provides the ability to encapsulate a shared key so that it
120121
/// can only be decapsulated by the holder of the corresponding decapsulation key.
121122
pub type EncapsulationKey = kem::EncapsulationKey<MlKem512Params>;
123+
124+
/// Encoded ML-KEM-512 ciphertexts.
125+
pub type EncodedCiphertext = param::EncodedCiphertext<MlKem512Params>;
126+
127+
/// Legacy expanded decapsulation keys. Prefer seeds instead.
128+
#[doc(hidden)]
129+
#[deprecated(since = "0.3.0", note = "use `Seed` instead")]
130+
pub type ExpandedDecapsulationKey = param::ExpandedDecapsulationKey<MlKem512Params>;
122131
}
123132

124133
/// ML-KEM-768 is the parameter set for security category 3, corresponding to key search on a block
125134
/// cipher with a 192-bit key.
126135
pub mod ml_kem_768 {
127136
use super::{Debug, ParameterSet, U2, U3, U4, U10, kem};
137+
use crate::param;
128138

129139
/// `MlKem768` is the parameter set for security category 3, corresponding to key search on a
130140
/// block cipher with a 192-bit key.
@@ -146,12 +156,20 @@ pub mod ml_kem_768 {
146156
/// An ML-KEM-768 `EncapsulationKey` provides the ability to encapsulate a shared key so that it
147157
/// can only be decapsulated by the holder of the corresponding decapsulation key.
148158
pub type EncapsulationKey = kem::EncapsulationKey<MlKem768Params>;
159+
160+
/// Encoded ML-KEM-512 ciphertexts.
161+
pub type EncodedCiphertext = param::EncodedCiphertext<MlKem768Params>;
162+
163+
/// Legacy expanded decapsulation keys. Prefer seeds instead.
164+
#[doc(hidden)]
165+
#[deprecated(since = "0.3.0", note = "use `Seed` instead")]
166+
pub type ExpandedDecapsulationKey = param::ExpandedDecapsulationKey<MlKem768Params>;
149167
}
150168

151169
/// ML-KEM-1024 is the parameter set for security category 5, corresponding to key search on a block
152170
/// cipher with a 256-bit key.
153171
pub mod ml_kem_1024 {
154-
use super::{Debug, ParameterSet, U2, U4, U5, U11, kem};
172+
use super::{Debug, ParameterSet, U2, U4, U5, U11, kem, param};
155173

156174
/// `MlKem1024` is the parameter set for security category 5, corresponding to key search on a
157175
/// block cipher with a 256-bit key.
@@ -173,6 +191,14 @@ pub mod ml_kem_1024 {
173191
/// An ML-KEM-1024 `EncapsulationKey` provides the ability to encapsulate a shared key so that
174192
/// it can only be decapsulated by the holder of the corresponding decapsulation key.
175193
pub type EncapsulationKey = kem::EncapsulationKey<MlKem1024Params>;
194+
195+
/// Encoded ML-KEM-512 ciphertexts.
196+
pub type EncodedCiphertext = param::EncodedCiphertext<MlKem1024Params>;
197+
198+
/// Legacy expanded decapsulation keys. Prefer seeds instead.
199+
#[doc(hidden)]
200+
#[deprecated(since = "0.3.0", note = "use `Seed` instead")]
201+
pub type ExpandedDecapsulationKey = param::ExpandedDecapsulationKey<MlKem1024Params>;
176202
}
177203

178204
/// An ML-KEM-512 `DecapsulationKey` which provides the ability to generate a new key pair, and

ml-kem/tests/wycheproof.rs

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! Test against the Wycheproof test vectors.
22
3+
use array::{Array, ArraySize};
34
use ml_kem::{
45
EncodedSizeUser, KemCore, MlKem512, MlKem768, MlKem1024,
5-
kem::{KeyExport, TryKeyInit},
6+
kem::{Decapsulate, KeyExport, TryKeyInit},
67
};
78
use serde::Deserialize;
89
use std::fs::File;
@@ -43,7 +44,6 @@ struct Test {
4344
dk: Option<String>,
4445
#[cfg(feature = "hazmat")]
4546
m: Option<String>,
46-
#[cfg(feature = "hazmat")]
4747
c: Option<String>,
4848
#[cfg(feature = "hazmat")]
4949
#[serde(default, rename(deserialize = "K"))]
@@ -73,13 +73,16 @@ macro_rules! load_json_file {
7373
}};
7474
}
7575

76-
fn decode_optional_hex(opt: &Option<String>, field: &str) -> Vec<u8> {
77-
match opt {
78-
Some(h) => {
79-
hex::decode(h).unwrap_or_else(|e| panic!("invalid hex for field '{field}': {e}"))
80-
}
81-
None => panic!("missing field: {field}"),
82-
}
76+
fn decode_optional_hex<U: ArraySize>(opt: &Option<String>, field: &str) -> Option<Array<u8, U>> {
77+
opt.as_ref().and_then(|h| {
78+
let vec = hex::decode(h).unwrap_or_else(|e| panic!("invalid hex for field '{field}': {e}"));
79+
vec.as_slice().try_into().ok()
80+
})
81+
}
82+
83+
fn decode_expected_hex<U: ArraySize>(opt: &Option<String>, field: &str) -> Array<u8, U> {
84+
decode_optional_hex(opt, field)
85+
.unwrap_or_else(|| panic!("missing or incorrect length field: {field}"))
8386
}
8487

8588
macro_rules! mlkem_keygen_seed_test {
@@ -101,11 +104,11 @@ macro_rules! mlkem_keygen_seed_test {
101104
test.comment.as_ref().unwrap(),
102105
&test.result
103106
);
104-
let test_seed = decode_optional_hex(&test.seed, "seed");
105-
let test_dk = decode_optional_hex(&test.dk, "dk");
107+
let test_seed = decode_expected_hex(&test.seed, "seed");
108+
let test_dk = decode_expected_hex(&test.dk, "dk");
106109

107-
let (dk, ek) = $kem::from_seed(test_seed.as_slice().try_into().unwrap());
108-
assert_eq!(test_dk.as_slice(), dk.to_encoded_bytes().as_slice());
110+
let (dk, ek) = $kem::from_seed(test_seed);
111+
assert_eq!(test_dk, dk.to_encoded_bytes());
109112
assert_eq!(test.ek.as_slice(), ek.to_bytes().as_slice());
110113
}
111114
}
@@ -143,21 +146,74 @@ macro_rules! mlkem_encaps_test {
143146

144147
#[cfg(feature = "hazmat")]
145148
{
146-
let test_m = decode_optional_hex(&test.m, "m");
147-
let test_m = test_m.as_slice().try_into().unwrap();
148-
let (c, k) = ek.encapsulate_deterministic(test_m);
149-
150-
let test_c = decode_optional_hex(&test.c, "c");
151-
let test_k = decode_optional_hex(&test.k, "K");
152-
assert_eq!(test_c.as_slice(), c.as_slice());
153-
assert_eq!(test_k.as_slice(), k.as_slice());
149+
let test_m = decode_expected_hex(&test.m, "m");
150+
let (c, k) = ek.encapsulate_deterministic(&test_m);
151+
152+
let test_c = decode_expected_hex(&test.c, "c");
153+
let test_k = decode_expected_hex(&test.k, "K");
154+
assert_eq!(test_c, c);
155+
assert_eq!(test_k, k);
154156
}
155157
}
156158
}
157159
}
158160
};
159161
}
160162

163+
macro_rules! mlkem_decaps_test {
164+
($name:ident, $json_file:expr, $kem_module:ident) => {
165+
#[test]
166+
fn $name() {
167+
let tests = load_json_file!($json_file);
168+
169+
for group in tests.groups {
170+
println!(
171+
"Parameter set: {} ({} v{})\n",
172+
&group.parameter_set, &group.source.name, &group.source.version
173+
);
174+
175+
for test in &group.tests {
176+
println!("Test #{} ({:?})", test.id, &test.result);
177+
178+
#[allow(deprecated)]
179+
use ml_kem::$kem_module::{
180+
DecapsulationKey, EncodedCiphertext, ExpandedDecapsulationKey,
181+
};
182+
183+
#[allow(deprecated)]
184+
let test_dk: ExpandedDecapsulationKey =
185+
match decode_optional_hex(&test.dk, "dk") {
186+
Some(dk) => dk,
187+
None => {
188+
if test.result == ExpectedResult::Invalid {
189+
continue;
190+
} else {
191+
panic!("failed to decode expanded decapsulation key!")
192+
}
193+
}
194+
};
195+
196+
#[allow(deprecated)]
197+
let dk = DecapsulationKey::from_expanded(&test_dk).expect("should be valid");
198+
199+
let test_c: EncodedCiphertext = match decode_optional_hex(&test.c, "c") {
200+
Some(dk) => dk,
201+
None => {
202+
if test.result == ExpectedResult::Invalid {
203+
continue;
204+
} else {
205+
panic!("failed to decode ciphertext!")
206+
}
207+
}
208+
};
209+
210+
let _ss = dk.decapsulate(&test_c);
211+
}
212+
}
213+
}
214+
};
215+
}
216+
161217
mlkem_keygen_seed_test!(
162218
mlkem_512_keygen_seed_test,
163219
"mlkem_512_keygen_seed_test.json",
@@ -174,6 +230,22 @@ mlkem_keygen_seed_test!(
174230
MlKem1024
175231
);
176232

233+
mlkem_decaps_test!(
234+
mlkem_512_semi_expanded_decaps_test,
235+
"mlkem_512_semi_expanded_decaps_test.json",
236+
ml_kem_512
237+
);
238+
mlkem_decaps_test!(
239+
mlkem_768_semi_expanded_decaps_test,
240+
"mlkem_768_semi_expanded_decaps_test.json",
241+
ml_kem_768
242+
);
243+
mlkem_decaps_test!(
244+
mlkem_1024_semi_expanded_decaps_test,
245+
"mlkem_1024_semi_expanded_decaps_test.json",
246+
ml_kem_1024
247+
);
248+
177249
mlkem_encaps_test!(
178250
mlkem_512_encaps_test,
179251
"mlkem_512_encaps_test.json",

0 commit comments

Comments
 (0)