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    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/// # Example
13///
14/// ```ignore
15/// use crypto::mlkem::{SecretKey768, generate_keypair_768};
16///
17/// let (secret_key, public_key) = generate_keypair_768();
18/// let (ciphertext, shared_secret) = public_key.encapsulate();
19/// let decapsulated = secret_key.decapsulate(&ciphertext).unwrap();
20/// assert_eq!(shared_secret, decapsulated);
21/// ```
22#[derive(Clone, Debug, PartialEq, Eq)]
23#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
24pub struct SecretKey768 {
25    bytes: [u8; SECRET_KEY_SIZE_768],
26}
27
28/// ML-KEM-768 encapsulation key (public key) as defined in FIPS 203.
29///
30/// See [`SecretKey768`] for a full usage example.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct PublicKey768 {
33    bytes: [u8; PUBLIC_KEY_SIZE_768],
34}
35
36/// Generate an ML-KEM-768 keypair.
37///
38/// This is a convenience wrapper around [`SecretKey768::generate`].
39///
40/// See [`SecretKey768`] for a usage example.
41#[inline]
42#[cfg(feature = "random")]
43pub fn generate_keypair_768() -> (SecretKey768, PublicKey768) {
44    SecretKey768::generate()
45}
46
47#[inline]
48pub fn generate_keypair_768_derand(coins: &[u8; 64]) -> (SecretKey768, PublicKey768) {
49    SecretKey768::generate_derand(coins)
50}
51
52impl SecretKey768 {
53    pub fn from_bytes(bytes: &[u8; SECRET_KEY_SIZE_768]) -> Self {
54        Self {
55            bytes: *bytes,
56        }
57    }
58
59    pub fn to_bytes(&self) -> [u8; SECRET_KEY_SIZE_768] {
60        self.bytes
61    }
62
63    #[cfg(feature = "random")]
64    pub fn generate() -> (Self, PublicKey768) {
65        let coins: [u8; 64] = crate::random::random_bytes();
66        Self::generate_derand(&coins)
67    }
68
69    pub fn generate_derand(coins: &[u8; 64]) -> (Self, PublicKey768) {
70        let (sk_bytes, pk_bytes) =
71            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, coins);
72        (
73            Self {
74                bytes: sk_bytes,
75            },
76            PublicKey768 {
77                bytes: pk_bytes,
78            },
79        )
80    }
81
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    pub fn from_bytes(bytes: &[u8; PUBLIC_KEY_SIZE_768]) -> Self {
112        Self {
113            bytes: *bytes,
114        }
115    }
116
117    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE_768] {
118        self.bytes
119    }
120
121    #[cfg(feature = "random")]
122    pub fn encapsulate(&self) -> ([u8; CIPHERTEXT_SIZE_768], [u8; SHARED_SECRET_SIZE]) {
123        let coins: [u8; 32] = crate::random::random_bytes();
124        self.encapsulate_derand(&coins)
125    }
126
127    pub(crate) fn encapsulate_derand(&self, coins: &[u8; 32]) -> ([u8; CIPHERTEXT_SIZE_768], [u8; SHARED_SECRET_SIZE]) {
128        crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &self.bytes, coins)
129    }
130}
131
132impl From<&[u8; PUBLIC_KEY_SIZE_768]> for PublicKey768 {
133    fn from(bytes: &[u8; PUBLIC_KEY_SIZE_768]) -> Self {
134        Self::from_bytes(bytes)
135    }
136}
137
138impl TryFrom<&[u8]> for PublicKey768 {
139    type Error = MlKemError;
140
141    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
142        Ok(Self::from_bytes(bytes.try_into().map_err(|_| MlKemError::InvalidKey)?))
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::{
149        super::mlkem::{
150            ML_KEM_768, crypto_kem_dec, crypto_kem_enc_derand, crypto_kem_keypair_derand, decode_hex_array,
151            sha3_256_hex,
152        },
153        *,
154    };
155
156    #[test]
157    fn ml_kem_768_round_trip() {
158        let (private_key, public_key) = generate_keypair_768();
159        let (ciphertext, encapsulated_secret) = public_key.encapsulate();
160        let decapsulated_secret = private_key.decapsulate(&ciphertext).unwrap();
161
162        assert_eq!(encapsulated_secret, decapsulated_secret);
163    }
164
165    #[test]
166    fn ml_kem_768_decapsulation_rejects_tampered_ciphertext() {
167        let (private_key, public_key) = generate_keypair_768();
168        let (mut ciphertext, encapsulated_secret) = public_key.encapsulate();
169
170        ciphertext[0] ^= 0x80;
171
172        let decapsulated_secret = private_key.decapsulate(&ciphertext).unwrap();
173
174        assert_ne!(encapsulated_secret, decapsulated_secret);
175    }
176
177    #[test]
178    fn ml_kem_768_deterministic_derand_vectors_are_stable() {
179        let key_coins = [7u8; 64];
180        let enc_coins = [9u8; 32];
181        let (secret_key, public_key) =
182            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &key_coins);
183        let (ciphertext, shared_secret) =
184            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &public_key, &enc_coins);
185        let decapsulated =
186            crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &secret_key, &ciphertext)
187                .unwrap();
188
189        assert_eq!(shared_secret, decapsulated);
190        assert_eq!(
191            hex::encode(&public_key[..32]),
192            "925a2700ad064ff778b4da4cf51457a48224a52751250a8ee10b251c818bafca"
193        );
194        assert_eq!(
195            hex::encode(&ciphertext[..32]),
196            "766c326c3483444c5b6d917cdddc3c07fbf935295c8f17c92a187a80dc4d15f2"
197        );
198        assert_eq!(
199            hex::encode(shared_secret),
200            "afcf18dfd6b710a09b5cf591d0eb8229d83aa10904934a3ca60a52da5ff36b96"
201        );
202    }
203
204    #[test]
205    fn ml_kem_768_cctv_accumulated_10k() {
206        use crate::{Xof, sha3::Shake128};
207
208        let mut rng = Shake128::new();
209        rng.absorb(&[]);
210
211        let mut acc = Shake128::new();
212
213        for _ in 0..10_000u32 {
214            let mut d = [0u8; 32];
215            let mut z = [0u8; 32];
216            let mut m = [0u8; 32];
217            let mut ct_random = [0u8; CIPHERTEXT_SIZE_768];
218
219            rng.squeeze(&mut d);
220            rng.squeeze(&mut z);
221            rng.squeeze(&mut m);
222            rng.squeeze(&mut ct_random);
223
224            let mut coins = [0u8; 64];
225            coins[..32].copy_from_slice(&d);
226            coins[32..].copy_from_slice(&z);
227
228            let (dk, ek) =
229                crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
230            let (ct, k_encaps) =
231                crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &ek, &m);
232
233            let k_decaps =
234                crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &ct).unwrap();
235            assert_eq!(k_encaps, k_decaps);
236
237            let k_decaps_random =
238                crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &ct_random).unwrap();
239
240            acc.absorb(&ek);
241            acc.absorb(&dk);
242            acc.absorb(&ct);
243            acc.absorb(&k_encaps);
244            acc.absorb(&k_decaps_random);
245        }
246
247        let mut hash = [0u8; 32];
248        acc.squeeze(&mut hash);
249        assert_eq!(
250            hex::encode(hash),
251            "f959d18d3d1180121433bf0e05f11e7908cf9d03edc150b2b07cb90bef5bc1c1",
252            "ML-KEM-768 CCTV accumulated hash mismatch"
253        );
254    }
255
256    #[test]
257    fn ml_kem_768_cctv_intermediate_vector() {
258        let d: [u8; 32] = decode_hex_array("f688563f7c66a5da2d8bdb5a5f3e07bd8dce6f7efcec7f41298d79863459f7cd");
259        let z: [u8; 32] = decode_hex_array("d1d49a515250dbceb9f6e3fcc1c7d5306918964b21ddb22207e03e57f0600da8");
260        let m: [u8; 32] = decode_hex_array("3dc27ca0a6594b0e56320457c45a0f76bb8a213ea4a76d442186a0aefadbcdb9");
261
262        let mut coins = [0u8; 64];
263        coins[..32].copy_from_slice(&d);
264        coins[32..].copy_from_slice(&z);
265
266        let (dk, ek) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
267        let (ct, k) = crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &ek, &m);
268
269        assert_eq!(
270            sha3_256_hex(&ek),
271            "42d930a50dfd1f0541ca45c4598daebb4f51cd10d711a001bd9bb87d5c87a4bf"
272        );
273        assert_eq!(
274            sha3_256_hex(&dk),
275            "db563aebd9fdc875e88563693edad1e5e359cc37b0f685d2d0a3723b37253192"
276        );
277        assert_eq!(
278            sha3_256_hex(&ct),
279            "9d6e358208c4d583050becb319050b7f916de47caad1d589a1d01fea43fe1750"
280        );
281        assert_eq!(
282            hex::encode(k),
283            "ae726da2df66601c6648a7565c02b203a089276ac30f6cc226d048f93fafd78c"
284        );
285    }
286
287    #[test]
288    fn ml_kem_768_decapsulation_with_wrong_key_rejects() {
289        let (_, alice_pk) = generate_keypair_768();
290        let (bob_sk, _bob_pk) = generate_keypair_768();
291        let (ct, _alice_ss) = alice_pk.encapsulate();
292
293        let wrong_ss = bob_sk.decapsulate(&ct).unwrap();
294        assert_ne!(_alice_ss, wrong_ss);
295    }
296
297    #[test]
298    fn ml_kem_768_round_trip_many() {
299        for _ in 0..100 {
300            let (sk, pk) = generate_keypair_768();
301            let (ct, ss_enc) = pk.encapsulate();
302            let ss_dec = sk.decapsulate(&ct).unwrap();
303            assert_eq!(ss_enc, ss_dec);
304        }
305    }
306
307    #[test]
308    fn ml_kem_768_all_zero_ciphertext_does_not_panic() {
309        let (sk, _pk) = generate_keypair_768();
310        let ct = [0u8; CIPHERTEXT_SIZE_768];
311        let _result = sk.decapsulate(&ct);
312    }
313
314    #[test]
315    fn ml_kem_768_all_ones_ciphertext_does_not_panic() {
316        let (sk, _pk) = generate_keypair_768();
317        let ct = [0xffu8; CIPHERTEXT_SIZE_768];
318        let _result = sk.decapsulate(&ct);
319    }
320
321    #[test]
322    fn ml_kem_768_derand_keygen_is_deterministic() {
323        let coins = [7u8; 64];
324        let (sk1, pk1) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
325        let (sk2, pk2) = crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &coins);
326        assert_eq!(sk1, sk2);
327        assert_eq!(pk1, pk2);
328    }
329
330    #[test]
331    fn ml_kem_768_key_sizes_are_correct() {
332        let (sk, pk) = generate_keypair_768();
333        let sk_bytes = sk.to_bytes();
334        let pk_bytes = pk.to_bytes();
335        assert_eq!(sk_bytes.len(), SECRET_KEY_SIZE_768);
336        assert_eq!(pk_bytes.len(), PUBLIC_KEY_SIZE_768);
337        let (ct, _) = pk.encapsulate();
338        assert_eq!(ct.len(), CIPHERTEXT_SIZE_768);
339    }
340
341    #[test]
342    fn ml_kem_768_encaps_is_deterministic_with_same_coins() {
343        let enc_coins = [9u8; 32];
344        let key_coins = [7u8; 64];
345        let (_sk, pk) =
346            crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &key_coins);
347        let (ct1, ss1) =
348            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &pk, &enc_coins);
349        let (ct2, ss2) =
350            crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &pk, &enc_coins);
351        assert_eq!(ct1, ct2);
352        assert_eq!(ss1, ss2);
353    }
354
355    #[test]
356    fn ml_kem_768_decapsulation_with_wrong_key_is_deterministic() {
357        let (_, pk_a) = generate_keypair_768();
358        let (sk_b, _pk_b) = generate_keypair_768();
359        let (ct, _) = pk_a.encapsulate();
360
361        let ss1 = sk_b.decapsulate(&ct).unwrap();
362        let ss2 = sk_b.decapsulate(&ct).unwrap();
363        assert_eq!(ss1, ss2, "implicit rejection must be deterministic");
364    }
365
366    #[test]
367    fn ml_kem_768_wycheproof_keygen() {
368        let data: serde_json::Value = serde_json::from_str(include_str!(
369            "../../testdata/wycheproof/testvectors_v1/mlkem_768_keygen_seed_test.json"
370        ))
371        .unwrap();
372        let mut tested = 0u64;
373        for group in data["testGroups"].as_array().unwrap() {
374            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
375                continue;
376            }
377            for test in group["tests"].as_array().unwrap() {
378                let seed_hex = test["seed"].as_str().unwrap();
379                let expected_ek_hex = test["ek"].as_str().unwrap();
380                let expected_dk_hex = test["dk"].as_str().unwrap();
381                let result = test["result"].as_str().unwrap();
382
383                let seed = hex::decode_array::<64>(seed_hex.as_bytes()).unwrap();
384
385                let (dk, ek) =
386                    crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &seed);
387
388                let ek_hex = hex::encode(ek);
389                let dk_hex = hex::encode(dk);
390
391                if result == "valid" {
392                    assert_eq!(
393                        ek_hex, expected_ek_hex,
394                        "wycheproof keygen KAT tcId={} ek mismatch",
395                        test["tcId"]
396                    );
397                    assert_eq!(
398                        dk_hex, expected_dk_hex,
399                        "wycheproof keygen KAT tcId={} dk mismatch",
400                        test["tcId"]
401                    );
402                }
403                tested += 1;
404            }
405        }
406        assert!(tested > 0, "no ML-KEM-768 keygen tests were run");
407    }
408
409    fn wycheproof_kem_skip_invalid_lengths(seed_hex: &str, c_hex: &str, ct_size: usize) -> bool {
410        seed_hex.len() != 128 || c_hex.len() != ct_size * 2
411    }
412
413    #[test]
414    fn ml_kem_768_wycheproof_kem() {
415        let data: serde_json::Value =
416            serde_json::from_str(include_str!("../../testdata/wycheproof/testvectors_v1/mlkem_768_test.json")).unwrap();
417        let mut tested = 0u64;
418        for group in data["testGroups"].as_array().unwrap() {
419            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
420                continue;
421            }
422            for test in group["tests"].as_array().unwrap() {
423                let seed_hex = test["seed"].as_str().unwrap();
424                let c_hex = test["c"].as_str().unwrap();
425                let expected_k_hex = test["K"].as_str().unwrap();
426                let result = test["result"].as_str().unwrap();
427
428                if wycheproof_kem_skip_invalid_lengths(seed_hex, c_hex, CIPHERTEXT_SIZE_768) {
429                    tested += 1;
430                    continue;
431                }
432
433                let seed = hex::decode_array::<64>(seed_hex.as_bytes()).unwrap();
434
435                let (dk, ek) =
436                    crypto_kem_keypair_derand::<3, SECRET_KEY_SIZE_768, PUBLIC_KEY_SIZE_768>(&ML_KEM_768, &seed);
437
438                if let Some(expected_ek_hex) = test.get("ek").and_then(|v| v.as_str()) {
439                    let ek_hex = hex::encode(ek);
440                    assert_eq!(ek_hex, expected_ek_hex, "wycheproof KEM KAT tcId={} ek mismatch", test["tcId"]);
441                }
442
443                let c = decode_hex_array::<CIPHERTEXT_SIZE_768>(c_hex);
444                let shared_secret = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &c);
445
446                if result == "valid" {
447                    let k = shared_secret.unwrap();
448                    let k_hex = hex::encode(k);
449                    assert_eq!(k_hex, expected_k_hex, "wycheproof KEM KAT tcId={} K mismatch", test["tcId"]);
450                } else {
451                    assert!(
452                        shared_secret.is_ok(),
453                        "wycheproof KEM KAT tcId={} unexpected error",
454                        test["tcId"]
455                    );
456                }
457                tested += 1;
458            }
459        }
460        assert!(tested > 0, "no ML-KEM-768 KEM tests were run");
461    }
462
463    #[test]
464    fn ml_kem_768_wycheproof_encaps() {
465        let data: serde_json::Value = serde_json::from_str(include_str!(
466            "../../testdata/wycheproof/testvectors_v1/mlkem_768_encaps_test.json"
467        ))
468        .unwrap();
469        let mut tested = 0u64;
470        for group in data["testGroups"].as_array().unwrap() {
471            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
472                continue;
473            }
474            for test in group["tests"].as_array().unwrap() {
475                let ek_hex = test["ek"].as_str().unwrap();
476                let m_hex = test["m"].as_str().unwrap();
477                let expected_c_hex = test["c"].as_str().unwrap();
478                let expected_k_hex = test["K"].as_str().unwrap();
479                let result = test["result"].as_str().unwrap();
480
481                if ek_hex.len() != PUBLIC_KEY_SIZE_768 * 2 {
482                    tested += 1;
483                    continue;
484                }
485
486                let ek = decode_hex_array::<PUBLIC_KEY_SIZE_768>(ek_hex);
487
488                if result == "valid" {
489                    let m = decode_hex_array::<32>(m_hex);
490                    let (c, k) =
491                        crypto_kem_enc_derand::<3, PUBLIC_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &ek, &m);
492                    let c_hex_out = hex::encode(c);
493                    let k_hex_out = hex::encode(k);
494                    assert_eq!(
495                        c_hex_out, expected_c_hex,
496                        "wycheproof encaps KAT tcId={} c mismatch",
497                        test["tcId"]
498                    );
499                    assert_eq!(
500                        k_hex_out, expected_k_hex,
501                        "wycheproof encaps KAT tcId={} K mismatch",
502                        test["tcId"]
503                    );
504                }
505                tested += 1;
506            }
507        }
508        assert!(tested > 0, "no ML-KEM-768 encaps tests were run");
509    }
510
511    #[test]
512    fn ml_kem_768_wycheproof_decaps_validation() {
513        let data: serde_json::Value = serde_json::from_str(include_str!(
514            "../../testdata/wycheproof/testvectors_v1/mlkem_768_semi_expanded_decaps_test.json"
515        ))
516        .unwrap();
517        let mut tested = 0u64;
518        for group in data["testGroups"].as_array().unwrap() {
519            if group["parameterSet"].as_str() != Some("ML-KEM-768") {
520                continue;
521            }
522            for test in group["tests"].as_array().unwrap() {
523                let flags: Vec<&str> = test["flags"]
524                    .as_array()
525                    .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
526                    .unwrap_or_default();
527                let dk_hex = test["dk"].as_str().unwrap();
528                let c_hex = test["c"].as_str().unwrap();
529
530                if flags.contains(&"IncorrectDecapsulationKeyLength") || flags.contains(&"IncorrectCiphertextLength") {
531                    tested += 1;
532                    continue;
533                }
534
535                let dk = decode_hex_array::<SECRET_KEY_SIZE_768>(dk_hex);
536                let c = decode_hex_array::<CIPHERTEXT_SIZE_768>(c_hex);
537
538                let result = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &dk, &c);
539
540                assert!(result.is_ok(), "wycheproof decaps tcId={} panicked", test["tcId"]);
541                tested += 1;
542            }
543        }
544        assert!(tested > 0, "no ML-KEM-768 decaps validation tests were run");
545    }
546
547    #[test]
548    fn ml_kem_768_cross_implementation_pqcrypto() {
549        // Cross-implementation validation: decapsulate ciphertexts generated by
550        // the pqcrypto (liboqs) ML-KEM-768 implementation.
551        let data: serde_json::Value =
552            serde_json::from_str(include_str!("../../testdata/mlkem/pqcrypto_768_vectors.json")).unwrap();
553        let vectors = data.as_array().unwrap();
554        assert!(vectors.len() >= 5, "not enough cross-impl vectors");
555
556        for (i, vector) in vectors.iter().enumerate() {
557            let sk_hex = vector["sk"].as_str().unwrap();
558            let ct_hex = vector["ct"].as_str().unwrap();
559            let expected_ss_hex = vector["ss"].as_str().unwrap();
560
561            let sk = decode_hex_array::<SECRET_KEY_SIZE_768>(sk_hex);
562            let ct = decode_hex_array::<CIPHERTEXT_SIZE_768>(ct_hex);
563
564            let ss = crypto_kem_dec::<3, SECRET_KEY_SIZE_768, CIPHERTEXT_SIZE_768>(&ML_KEM_768, &sk, &ct).unwrap();
565            assert_eq!(
566                hex::encode(ss),
567                expected_ss_hex,
568                "cross-impl pqcrypto vector {i} decapsulation mismatch"
569            );
570        }
571    }
572}