Skip to main content

crypto/mlkem/
mlkem1024.rs

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