Skip to main content

crypto/mlkem/
mlkem768.rs

1use super::mlkem::{
2    ML_KEM_768, MlKemError, SHARED_SECRET_SIZE, crypto_kem_dec, crypto_kem_enc_derand, crypto_kem_keypair_derand,
3    encapsulation_key_modulus_check, indcpa_secret_key_bytes,
4};
5
6pub const PUBLIC_KEY_SIZE_768: usize = 1184;
7pub const SECRET_KEY_SIZE_768: usize = 2400;
8pub const CIPHERTEXT_SIZE_768: usize = 1088;
9
10/// ML-KEM-768 decapsulation key (secret key) as defined in FIPS 203.
11///
12/// A key is created with [`SecretKey768::generate`] (fresh random key) or
13/// [`SecretKey768::generate_derand`] (deterministic, for test vectors).
14///
15/// # Example
16///
17/// ```
18/// use crypto::mlkem::SecretKey768;
19///
20/// let (secret_key, public_key) = SecretKey768::generate_derand(&[0u8; 64]);
21/// let (shared_secret, ciphertext) = public_key.encapsulate_derand(&[0u8; 32]);
22/// let decapsulated = secret_key.decapsulate(&ciphertext).unwrap();
23/// assert_eq!(shared_secret, decapsulated);
24/// ```
25#[derive(Clone, PartialEq, Eq)]
26#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
27pub struct SecretKey768 {
28    bytes: [u8; SECRET_KEY_SIZE_768],
29}
30
31/// ML-KEM-768 encapsulation key (public key) as defined in FIPS 203.
32///
33/// See [`SecretKey768`] for a full usage example.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct PublicKey768 {
36    bytes: [u8; PUBLIC_KEY_SIZE_768],
37}
38
39impl SecretKey768 {
40    pub fn from_bytes(bytes: &[u8; SECRET_KEY_SIZE_768]) -> Self {
41        Self {
42            bytes: *bytes,
43        }
44    }
45
46    pub fn to_bytes(&self) -> [u8; SECRET_KEY_SIZE_768] {
47        self.bytes
48    }
49
50    /// Generates a fresh random keypair.
51    ///
52    /// Returns [`MlKemError::Random`] when the operating system's random
53    /// number generator is unavailable or fails.
54    #[cfg(feature = "random")]
55    pub fn generate() -> Result<(Self, PublicKey768), MlKemError> {
56        let coins: [u8; 64] = crate::random::bytes()?;
57        Ok(Self::generate_derand(&coins))
58    }
59
60    pub fn generate_derand(coins: &[u8; 64]) -> (Self, PublicKey768) {
61        let (sk_bytes, pk_bytes) =
62            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, coins);
63        (
64            Self {
65                bytes: sk_bytes,
66            },
67            PublicKey768 {
68                bytes: pk_bytes,
69            },
70        )
71    }
72
73    /// Decapsulates `ciphertext`, recovering the shared secret encapsulated in it.
74    ///
75    /// Implements the FIPS 203 implicit-rejection transform: a ciphertext that
76    /// does not re-encrypt to itself does not produce an error, but instead
77    /// yields a pseudorandom shared secret derived from the key and ciphertext.
78    ///
79    /// Returns [`MlKemError::InvalidKey`] if this key fails the FIPS 203
80    /// decapsulation-key check, i.e. the hash of its embedded encapsulation key
81    /// does not match the hash stored in the key.
82    pub fn decapsulate(&self, ciphertext: &[u8; CIPHERTEXT_SIZE_768]) -> Result<[u8; SHARED_SECRET_SIZE], MlKemError> {
83        crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &self.bytes, ciphertext)
84    }
85
86    pub fn public_key(&self) -> PublicKey768 {
87        let offset = indcpa_secret_key_bytes::<3>();
88        let mut pk_bytes = [0u8; PUBLIC_KEY_SIZE_768];
89        pk_bytes.copy_from_slice(&self.bytes[offset..offset + PUBLIC_KEY_SIZE_768]);
90        PublicKey768 {
91            bytes: pk_bytes,
92        }
93    }
94}
95
96impl From<&[u8; SECRET_KEY_SIZE_768]> for SecretKey768 {
97    fn from(bytes: &[u8; SECRET_KEY_SIZE_768]) -> Self {
98        Self::from_bytes(bytes)
99    }
100}
101
102impl TryFrom<&[u8]> for SecretKey768 {
103    type Error = MlKemError;
104
105    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
106        Ok(Self::from_bytes(bytes.try_into().map_err(|_| MlKemError::InvalidKey)?))
107    }
108}
109
110impl PublicKey768 {
111    /// Parses an encapsulation key from its FIPS 203 encoding.
112    ///
113    /// Returns [`MlKemError::InvalidKey`] when the encoded `t˂` is not
114    /// canonical, i.e. when any of its coefficients is not in `[0, q-1]`
115    /// (FIPS 203 Section 7.2 modulus check).
116    pub fn from_bytes(bytes: &[u8; PUBLIC_KEY_SIZE_768]) -> Result<Self, MlKemError> {
117        if !encapsulation_key_modulus_check::<3>(bytes) {
118            return Err(MlKemError::InvalidKey);
119        }
120        Ok(Self {
121            bytes: *bytes,
122        })
123    }
124
125    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE_768] {
126        self.bytes
127    }
128
129    /// Encapsulates a fresh random shared secret against this public key.
130    ///
131    /// Returns [`MlKemError::Random`] when the operating system's random
132    /// number generator is unavailable or fails.
133    #[cfg(feature = "random")]
134    pub fn encapsulate(&self) -> Result<([u8; SHARED_SECRET_SIZE], [u8; CIPHERTEXT_SIZE_768]), MlKemError> {
135        let coins: [u8; 32] = crate::random::bytes()?;
136        Ok(self.encapsulate_derand(&coins))
137    }
138
139    /// Encapsulates a shared secret against this public key using the provided
140    /// 32-byte `coins` as the encapsulation randomness.
141    ///
142    /// This is the deterministic counterpart of [`PublicKey768::encapsulate`],
143    /// useful for reproducing FIPS 203 / ACVP test vectors. It must not be used
144    /// with repeated `coins`.
145    pub fn encapsulate_derand(&self, coins: &[u8; 32]) -> ([u8; SHARED_SECRET_SIZE], [u8; CIPHERTEXT_SIZE_768]) {
146        crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &self.bytes, coins)
147    }
148}
149
150impl TryFrom<&[u8]> for PublicKey768 {
151    type Error = MlKemError;
152
153    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
154        Self::from_bytes(bytes.try_into().map_err(|_| MlKemError::InvalidKey)?)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::{
161        super::mlkem::{
162            ML_KEM_768, Q, crypto_kem_dec, crypto_kem_enc_derand, crypto_kem_keypair_derand, decode_hex_array,
163            sha3_256_hex,
164        },
165        *,
166    };
167
168    #[test]
169    fn ml_kem_768_round_trip() {
170        let (private_key, public_key) = SecretKey768::generate().unwrap();
171        let (encapsulated_secret, ciphertext) = public_key.encapsulate().unwrap();
172        let decapsulated_secret = private_key.decapsulate(&ciphertext).unwrap();
173
174        assert_eq!(encapsulated_secret, decapsulated_secret);
175    }
176
177    #[test]
178    fn ml_kem_768_decapsulation_rejects_tampered_ciphertext() {
179        let (private_key, public_key) = SecretKey768::generate().unwrap();
180        let (encapsulated_secret, mut ciphertext) = public_key.encapsulate().unwrap();
181
182        ciphertext[0] ^= 0x80;
183
184        let decapsulated_secret = private_key.decapsulate(&ciphertext).unwrap();
185
186        assert_ne!(encapsulated_secret, decapsulated_secret);
187    }
188
189    #[test]
190    fn ml_kem_768_deterministic_derand_vectors_are_stable() {
191        let key_coins = [7u8; 64];
192        let enc_coins = [9u8; 32];
193        let (secret_key, public_key) =
194            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &key_coins);
195        let (shared_secret, ciphertext) =
196            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &public_key, &enc_coins);
197        let decapsulated =
198            crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &secret_key, &ciphertext)
199                .unwrap();
200
201        assert_eq!(shared_secret, decapsulated);
202        assert_eq!(
203            hex::encode(&public_key[..32]),
204            "925a2700ad064ff778b4da4cf51457a48224a52751250a8ee10b251c818bafca"
205        );
206        assert_eq!(
207            hex::encode(&ciphertext[..32]),
208            "766c326c3483444c5b6d917cdddc3c07fbf935295c8f17c92a187a80dc4d15f2"
209        );
210        assert_eq!(
211            hex::encode(shared_secret),
212            "afcf18dfd6b710a09b5cf591d0eb8229d83aa10904934a3ca60a52da5ff36b96"
213        );
214    }
215
216    #[test]
217    fn ml_kem_768_cctv_accumulated_10k() {
218        use crate::{Xof, sha3::Shake128};
219
220        let mut rng = Shake128::new();
221        rng.absorb(&[]);
222
223        let mut acc = Shake128::new();
224
225        for _ in 0..10_000u32 {
226            let mut d = [0u8; 32];
227            let mut z = [0u8; 32];
228            let mut m = [0u8; 32];
229            let mut ct_random = [0u8; CIPHERTEXT_SIZE_768];
230
231            rng.squeeze(&mut d);
232            rng.squeeze(&mut z);
233            rng.squeeze(&mut m);
234            rng.squeeze(&mut ct_random);
235
236            let mut coins = [0u8; 64];
237            coins[..32].copy_from_slice(&d);
238            coins[32..].copy_from_slice(&z);
239
240            let (dk, ek) =
241                crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
242            let (k_encaps, ct) =
243                crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &ek, &m);
244
245            let k_decaps =
246                crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &ct).unwrap();
247            assert_eq!(k_encaps, k_decaps);
248
249            let k_decaps_random =
250                crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &ct_random).unwrap();
251
252            acc.absorb(&ek);
253            acc.absorb(&dk);
254            acc.absorb(&ct);
255            acc.absorb(&k_encaps);
256            acc.absorb(&k_decaps_random);
257        }
258
259        let mut hash = [0u8; 32];
260        acc.squeeze(&mut hash);
261        assert_eq!(
262            hex::encode(hash),
263            "f959d18d3d1180121433bf0e05f11e7908cf9d03edc150b2b07cb90bef5bc1c1",
264            "ML-KEM-768 CCTV accumulated hash mismatch"
265        );
266    }
267
268    #[test]
269    fn ml_kem_768_cctv_intermediate_vector() {
270        let d: [u8; 32] = decode_hex_array("f688563f7c66a5da2d8bdb5a5f3e07bd8dce6f7efcec7f41298d79863459f7cd");
271        let z: [u8; 32] = decode_hex_array("d1d49a515250dbceb9f6e3fcc1c7d5306918964b21ddb22207e03e57f0600da8");
272        let m: [u8; 32] = decode_hex_array("3dc27ca0a6594b0e56320457c45a0f76bb8a213ea4a76d442186a0aefadbcdb9");
273
274        let mut coins = [0u8; 64];
275        coins[..32].copy_from_slice(&d);
276        coins[32..].copy_from_slice(&z);
277
278        let (dk, ek) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
279        let (k, ct) = crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &ek, &m);
280
281        assert_eq!(
282            sha3_256_hex(&ek),
283            "42d930a50dfd1f0541ca45c4598daebb4f51cd10d711a001bd9bb87d5c87a4bf"
284        );
285        assert_eq!(
286            sha3_256_hex(&dk),
287            "db563aebd9fdc875e88563693edad1e5e359cc37b0f685d2d0a3723b37253192"
288        );
289        assert_eq!(
290            sha3_256_hex(&ct),
291            "9d6e358208c4d583050becb319050b7f916de47caad1d589a1d01fea43fe1750"
292        );
293        assert_eq!(
294            hex::encode(k),
295            "ae726da2df66601c6648a7565c02b203a089276ac30f6cc226d048f93fafd78c"
296        );
297    }
298
299    #[test]
300    fn ml_kem_768_decapsulation_with_wrong_key_rejects() {
301        let (_, alice_pk) = SecretKey768::generate().unwrap();
302        let (bob_sk, _bob_pk) = SecretKey768::generate().unwrap();
303        let (_alice_ss, ct) = alice_pk.encapsulate().unwrap();
304
305        let wrong_ss = bob_sk.decapsulate(&ct).unwrap();
306        assert_ne!(_alice_ss, wrong_ss);
307    }
308
309    #[test]
310    fn ml_kem_768_round_trip_many() {
311        for _ in 0..100 {
312            let (sk, pk) = SecretKey768::generate().unwrap();
313            let (ss_enc, ct) = pk.encapsulate().unwrap();
314            let ss_dec = sk.decapsulate(&ct).unwrap();
315            assert_eq!(ss_enc, ss_dec);
316        }
317    }
318
319    #[test]
320    fn ml_kem_768_all_zero_ciphertext_does_not_panic() {
321        let (sk, _pk) = SecretKey768::generate().unwrap();
322        let ct = [0u8; CIPHERTEXT_SIZE_768];
323        let _result = sk.decapsulate(&ct);
324    }
325
326    #[test]
327    fn ml_kem_768_all_ones_ciphertext_does_not_panic() {
328        let (sk, _pk) = SecretKey768::generate().unwrap();
329        let ct = [0xffu8; CIPHERTEXT_SIZE_768];
330        let _result = sk.decapsulate(&ct);
331    }
332
333    #[test]
334    fn ml_kem_768_derand_keygen_is_deterministic() {
335        let coins = [7u8; 64];
336        let (sk1, pk1) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
337        let (sk2, pk2) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
338        assert_eq!(sk1, sk2);
339        assert_eq!(pk1, pk2);
340    }
341
342    #[test]
343    fn ml_kem_768_key_sizes_are_correct() {
344        let (sk, pk) = SecretKey768::generate().unwrap();
345        let sk_bytes = sk.to_bytes();
346        let pk_bytes = pk.to_bytes();
347        assert_eq!(sk_bytes.len(), SECRET_KEY_SIZE_768);
348        assert_eq!(pk_bytes.len(), PUBLIC_KEY_SIZE_768);
349        let (_, ct) = pk.encapsulate().unwrap();
350        assert_eq!(ct.len(), CIPHERTEXT_SIZE_768);
351    }
352
353    #[test]
354    fn ml_kem_768_rejects_non_canonical_encapsulation_key() {
355        let (_sk, pk) = SecretKey768::generate().unwrap();
356        let base = pk.to_bytes();
357
358        // A freshly generated key is canonical.
359        assert!(PublicKey768::from_bytes(&base).is_ok());
360
361        // Overwrite the first 12-bit coefficient with `q` (out of range) and
362        // with the in-range boundary `q - 1`.
363        let set_first = |c: u16| {
364            let mut bytes = base;
365            bytes[0] = (c & 0xff) as u8;
366            bytes[1] = (bytes[1] & 0xf0) | ((c >> 8) as u8 & 0x0f);
367            bytes
368        };
369
370        assert!(PublicKey768::from_bytes(&set_first(Q as u16)).is_err());
371        assert!(PublicKey768::from_bytes(&set_first(0x0fff)).is_err());
372        assert!(PublicKey768::from_bytes(&set_first(Q as u16 - 1)).is_ok());
373    }
374
375    #[test]
376    fn ml_kem_768_encaps_is_deterministic_with_same_coins() {
377        let enc_coins = [9u8; 32];
378        let key_coins = [7u8; 64];
379        let (_sk, pk) =
380            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &key_coins);
381        let (ss1, ct1) =
382            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &pk, &enc_coins);
383        let (ss2, ct2) =
384            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &pk, &enc_coins);
385        assert_eq!(ct1, ct2);
386        assert_eq!(ss1, ss2);
387    }
388
389    #[test]
390    fn ml_kem_768_decapsulation_with_wrong_key_is_deterministic() {
391        let (_, pk_a) = SecretKey768::generate().unwrap();
392        let (sk_b, _pk_b) = SecretKey768::generate().unwrap();
393        let (_, ct) = pk_a.encapsulate().unwrap();
394
395        let ss1 = sk_b.decapsulate(&ct).unwrap();
396        let ss2 = sk_b.decapsulate(&ct).unwrap();
397        assert_eq!(ss1, ss2, "implicit rejection must be deterministic");
398    }
399
400    #[test]
401    fn ml_kem_768_wycheproof_keygen() {
402        let data: serde_json::Value = serde_json::from_str(include_str!(
403            "../../testdata/wycheproof/testvectors_v1/mlkem_768_keygen_seed_test.json"
404        ))
405        .unwrap();
406        let mut tested = 0u64;
407        for group in data["testGroups"].as_array().unwrap() {
408            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
409                continue;
410            }
411            for test in group["tests"].as_array().unwrap() {
412                let seed_hex = test["seed"].as_str().unwrap();
413                let expected_ek_hex = test["ek"].as_str().unwrap();
414                let expected_dk_hex = test["dk"].as_str().unwrap();
415                let result = test["result"].as_str().unwrap();
416
417                let seed = hex::decode_array::<64>(seed_hex.as_bytes()).unwrap();
418
419                let (dk, ek) =
420                    crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &seed);
421
422                let ek_hex = hex::encode(ek);
423                let dk_hex = hex::encode(dk);
424
425                if result == "valid" {
426                    assert_eq!(
427                        ek_hex, expected_ek_hex,
428                        "wycheproof keygen KAT tcId={} ek mismatch",
429                        test["tcId"]
430                    );
431                    assert_eq!(
432                        dk_hex, expected_dk_hex,
433                        "wycheproof keygen KAT tcId={} dk mismatch",
434                        test["tcId"]
435                    );
436                }
437                tested += 1;
438            }
439        }
440        assert!(tested > 0, "no ML-KEM-768 keygen tests were run");
441    }
442
443    fn wycheproof_kem_has_invalid_lengths(seed_hex: &str, c_hex: &str, ct_size: usize) -> bool {
444        seed_hex.len() != 128 || c_hex.len() != ct_size * 2
445    }
446
447    #[test]
448    fn ml_kem_768_wycheproof_kem() {
449        let data: serde_json::Value =
450            serde_json::from_str(include_str!("../../testdata/wycheproof/testvectors_v1/mlkem_768_test.json")).unwrap();
451        let mut tested = 0u64;
452        let mut skipped = 0u64;
453        for group in data["testGroups"].as_array().unwrap() {
454            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
455                continue;
456            }
457            for test in group["tests"].as_array().unwrap() {
458                let seed_hex = test["seed"].as_str().unwrap();
459                let c_hex = test["c"].as_str().unwrap();
460                let expected_k_hex = test["K"].as_str().unwrap();
461                let result = test["result"].as_str().unwrap();
462
463                if wycheproof_kem_has_invalid_lengths(seed_hex, c_hex, CIPHERTEXT_SIZE_768) {
464                    // The key-generation seed is a fixed `[u8; 64]` and the
465                    // ciphertext is a fixed `[u8; CIPHERTEXT_SIZE_768]`, so
466                    // wrong-length inputs are unrepresentable through the API.
467                    assert!(
468                        hex::decode(seed_hex).unwrap().len() != 64
469                            || hex::decode(c_hex).unwrap().len() != CIPHERTEXT_SIZE_768,
470                        "wycheproof KEM KAT tcId={} unexpectedly has correct lengths",
471                        test["tcId"]
472                    );
473                    skipped += 1;
474                    continue;
475                }
476
477                let seed = hex::decode_array::<64>(seed_hex.as_bytes()).unwrap();
478
479                let (dk, ek) =
480                    crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &seed);
481
482                if let Some(expected_ek_hex) = test.get("ek").and_then(|v| v.as_str()) {
483                    let ek_hex = hex::encode(ek);
484                    assert_eq!(ek_hex, expected_ek_hex, "wycheproof KEM KAT tcId={} ek mismatch", test["tcId"]);
485                }
486
487                let c = decode_hex_array::<CIPHERTEXT_SIZE_768>(c_hex);
488                let shared_secret = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &c);
489
490                if result == "valid" {
491                    let k = shared_secret.unwrap();
492                    let k_hex = hex::encode(k);
493                    assert_eq!(k_hex, expected_k_hex, "wycheproof KEM KAT tcId={} K mismatch", test["tcId"]);
494                } else {
495                    assert!(
496                        shared_secret.is_ok(),
497                        "wycheproof KEM KAT tcId={} unexpected error",
498                        test["tcId"]
499                    );
500                }
501                tested += 1;
502            }
503        }
504        assert!(tested > 0, "no ML-KEM-768 KEM tests were run");
505        eprintln!("wycheproof ML-KEM-768 KEM: {tested} tested, {skipped} skipped (wrong-length inputs)");
506    }
507
508    #[test]
509    fn ml_kem_768_wycheproof_encaps() {
510        let data: serde_json::Value = serde_json::from_str(include_str!(
511            "../../testdata/wycheproof/testvectors_v1/mlkem_768_encaps_test.json"
512        ))
513        .unwrap();
514        let mut tested = 0u64;
515        let mut skipped = 0u64;
516        for group in data["testGroups"].as_array().unwrap() {
517            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
518                continue;
519            }
520            for test in group["tests"].as_array().unwrap() {
521                let ek_hex = test["ek"].as_str().unwrap();
522                let m_hex = test["m"].as_str().unwrap();
523                let expected_c_hex = test["c"].as_str().unwrap();
524                let expected_k_hex = test["K"].as_str().unwrap();
525                let result = test["result"].as_str().unwrap();
526
527                if ek_hex.len() != PUBLIC_KEY_SIZE_768 * 2 {
528                    let ek_bytes = hex::decode(ek_hex).unwrap();
529                    assert!(
530                        PublicKey768::try_from(ek_bytes.as_slice()).is_err(),
531                        "wycheproof encaps tcId={} flagged wrong-length ek but it parsed",
532                        test["tcId"]
533                    );
534                    skipped += 1;
535                    continue;
536                }
537
538                let ek = decode_hex_array::<PUBLIC_KEY_SIZE_768>(ek_hex);
539
540                if result == "valid" {
541                    let public_key = PublicKey768::from_bytes(&ek)
542                        .unwrap_or_else(|_| panic!("wycheproof encaps tcId={} valid ek rejected", test["tcId"]));
543                    let m = decode_hex_array::<32>(m_hex);
544                    let (k, c) = public_key.encapsulate_derand(&m);
545                    let c_hex_out = hex::encode(c);
546                    let k_hex_out = hex::encode(k);
547                    assert_eq!(
548                        c_hex_out, expected_c_hex,
549                        "wycheproof encaps KAT tcId={} c mismatch",
550                        test["tcId"]
551                    );
552                    assert_eq!(
553                        k_hex_out, expected_k_hex,
554                        "wycheproof encaps KAT tcId={} K mismatch",
555                        test["tcId"]
556                    );
557                } else {
558                    assert!(
559                        PublicKey768::from_bytes(&ek).is_err(),
560                        "wycheproof encaps tcId={} invalid ek accepted",
561                        test["tcId"]
562                    );
563                }
564                tested += 1;
565            }
566        }
567        assert!(tested > 0, "no ML-KEM-768 encaps tests were run");
568        eprintln!("wycheproof ML-KEM-768 encaps: {tested} tested, {skipped} skipped (wrong-length ek)");
569    }
570
571    #[test]
572    fn ml_kem_768_wycheproof_decaps_validation() {
573        let data: serde_json::Value = serde_json::from_str(include_str!(
574            "../../testdata/wycheproof/testvectors_v1/mlkem_768_semi_expanded_decaps_test.json"
575        ))
576        .unwrap();
577        let mut valid = 0u64;
578        let mut invalid = 0u64;
579        let mut skipped = 0u64;
580        for group in data["testGroups"].as_array().unwrap() {
581            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
582                continue;
583            }
584            for test in group["tests"].as_array().unwrap() {
585                let flags: Vec<&str> = test["flags"]
586                    .as_array()
587                    .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
588                    .unwrap_or_default();
589                let tc_id = test["tcId"].clone();
590                let dk_hex = test["dk"].as_str().unwrap();
591                let c_hex = test["c"].as_str().unwrap();
592
593                if flags.contains(&"IncorrectDecapsulationKeyLength") {
594                    let dk_bytes = hex::decode(dk_hex).unwrap();
595                    assert!(
596                        SecretKey768::try_from(dk_bytes.as_slice()).is_err(),
597                        "wycheproof decaps tcId={tc_id} flagged wrong-length dk but it parsed",
598                    );
599                    skipped += 1;
600                    continue;
601                }
602
603                if flags.contains(&"IncorrectCiphertextLength") {
604                    // Ciphertexts are raw fixed-size arrays with no fallible
605                    // decoder, so a wrong-length ciphertext cannot reach the API.
606                    assert!(
607                        hex::decode(c_hex).unwrap().len() != CIPHERTEXT_SIZE_768,
608                        "wycheproof decaps tcId={tc_id} flagged wrong-length ct but length is correct",
609                    );
610                    skipped += 1;
611                    continue;
612                }
613
614                let dk = decode_hex_array::<SECRET_KEY_SIZE_768>(dk_hex);
615                let c = decode_hex_array::<CIPHERTEXT_SIZE_768>(c_hex);
616
617                let result = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &c);
618
619                match test["result"].as_str().unwrap() {
620                    "valid" => {
621                        let k = result
622                            .unwrap_or_else(|_| panic!("wycheproof decaps tcId={tc_id} ({flags:?}): expected success"));
623                        let expected = test["K"].as_str().expect("valid vector without K");
624                        assert_eq!(
625                            hex::encode(k),
626                            expected,
627                            "wycheproof decaps tcId={tc_id} ({flags:?}) shared secret mismatch"
628                        );
629                        valid += 1;
630                    }
631                    "invalid" => {
632                        assert!(
633                            result.is_err(),
634                            "wycheproof decaps tcId={tc_id} ({flags:?}): expected rejection but decapsulation succeeded",
635                        );
636                        invalid += 1;
637                    }
638                    other => panic!("wycheproof decaps tcId={tc_id}: unknown result {other}"),
639                }
640            }
641        }
642        assert!(valid > 0, "no valid ML-KEM-768 decaps tests were run");
643        assert!(invalid > 0, "no invalid ML-KEM-768 decaps tests were run");
644        eprintln!("wycheproof ML-KEM-768 decaps: {valid} valid, {invalid} invalid, {skipped} skipped (wrong lengths)");
645    }
646
647    /// Implicit rejection must be the FIPS 203 value `J(z || c) = SHAKE256(z||c)`
648    /// for *every* ciphertext byte, including the last byte of the ciphertext
649    /// (the tail of `c2`). A truncated re-encryption comparison — the class of
650    /// bug behind CVE-2026-10097/CVE-2026-6330 — would leave that byte unchecked
651    /// and leak the decryption noise.
652    #[test]
653    fn ml_kem_768_implicit_rejection_matches_spec() {
654        use crate::{Xof, sha3::Shake256};
655
656        let (sk, pk) = SecretKey768::generate().unwrap();
657        let sk_bytes = sk.to_bytes();
658        let z = &sk_bytes[SECRET_KEY_SIZE_768 - 32..];
659        let (k_ok, ct) = pk.encapsulate().unwrap();
660
661        for offset in [0, CIPHERTEXT_SIZE_768 / 2, 959, CIPHERTEXT_SIZE_768 - 1] {
662            let mut bad = ct;
663            bad[offset] ^= 0x01;
664            let k_bad = sk.decapsulate(&bad).unwrap();
665            assert_ne!(k_bad, k_ok, "tampered ciphertext byte {offset} returned the valid secret");
666
667            let mut hasher = Shake256::new();
668            hasher.absorb(z);
669            hasher.absorb(&bad);
670            let mut expected = [0u8; SHARED_SECRET_SIZE];
671            hasher.squeeze(&mut expected);
672            assert_eq!(k_bad, expected, "implicit rejection at byte {offset} is not SHAKE256(z||c)");
673        }
674    }
675
676    #[test]
677    fn ml_kem_768_rejects_key_with_wrong_hash() {
678        let (sk, _pk) = SecretKey768::generate().unwrap();
679        let mut bytes = sk.to_bytes();
680        let (_, ct) = sk.public_key().encapsulate().unwrap();
681
682        // A well-formed key decapsulates.
683        assert!(SecretKey768::from_bytes(&bytes).decapsulate(&ct).is_ok());
684
685        // Corrupting the stored H(ek) must be rejected (FIPS 203 Section 7.3).
686        bytes[SECRET_KEY_SIZE_768 - 64] ^= 0x01;
687        assert_eq!(SecretKey768::from_bytes(&bytes).decapsulate(&ct), Err(MlKemError::InvalidKey));
688
689        // Corrupting the embedded encapsulation key must also be rejected,
690        // because it no longer hashes to the stored value.
691        let mut bytes = sk.to_bytes();
692        bytes[SECRET_KEY_SIZE_768 - PUBLIC_KEY_SIZE_768 - 64] ^= 0x01;
693        assert_eq!(SecretKey768::from_bytes(&bytes).decapsulate(&ct), Err(MlKemError::InvalidKey));
694    }
695
696    #[test]
697    fn ml_kem_768_cross_implementation_pqcrypto() {
698        // Cross-implementation validation: decapsulate ciphertexts generated by
699        // the pqcrypto (liboqs) ML-KEM-768 implementation.
700        let data: serde_json::Value =
701            serde_json::from_str(include_str!("../../testdata/mlkem/pqcrypto_768_vectors.json")).unwrap();
702        let vectors = data.as_array().unwrap();
703        assert!(vectors.len() >= 5, "not enough cross-impl vectors");
704
705        for (i, vector) in vectors.iter().enumerate() {
706            let sk_hex = vector["sk"].as_str().unwrap();
707            let ct_hex = vector["ct"].as_str().unwrap();
708            let expected_ss_hex = vector["ss"].as_str().unwrap();
709
710            let sk = decode_hex_array::<SECRET_KEY_SIZE_768>(sk_hex);
711            let ct = decode_hex_array::<CIPHERTEXT_SIZE_768>(ct_hex);
712
713            let ss = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &sk, &ct).unwrap();
714            assert_eq!(
715                hex::encode(ss),
716                expected_ss_hex,
717                "cross-impl pqcrypto vector {i} decapsulation mismatch"
718            );
719        }
720    }
721}