Skip to main content

crypto/mlkem/
mlkem.rs

1use constant_time_eq::constant_time_eq;
2#[cfg(feature = "zeroize")]
3use zeroize::{Zeroize, ZeroizeOnDrop};
4
5use crate::{
6    RandomError, Xof,
7    sha3::{Sha3_256, Sha3_512, Shake128, Shake256},
8};
9
10/// Size of the shared secret produced by ML-KEM encapsulation/decapsulation (32 bytes).
11pub const SHARED_SECRET_SIZE: usize = 32;
12
13pub(crate) const N: usize = 256;
14pub(crate) const Q: i16 = 3329;
15const SYMBYTES: usize = 32;
16const POLY_BYTES: usize = 384;
17const SHAKE128_RATE: usize = 168;
18const QINV: i16 = -3327;
19const MONT_SQUARED_DIV_N: i16 = 1441;
20const ZETAS: [i16; 128] = [
21    -1044, -758, -359, -1517, 1493, 1422, 287, 202, -171, 622, 1577, 182, 962, -1202, -1474, 1468, 573, -1325, 264,
22    383, -829, 1458, -1602, -130, -681, 1017, 732, 608, -1542, 411, -205, -1571, 1223, 652, -552, 1015, -1293, 1491,
23    -282, -1544, 516, -8, -320, -666, -1618, -1162, 126, 1469, -853, -90, -271, 830, 107, -1421, -247, -951, -398, 961,
24    -1508, -725, 448, -1065, 677, -1275, -1103, 430, 555, 843, -1251, 871, 1550, 105, 422, 587, 177, -235, -291, -460,
25    1574, 1653, -246, 778, 1159, -147, -777, 1483, -602, 1119, -1590, 644, -872, 349, 418, 329, -156, -75, 817, 1097,
26    603, 610, 1322, -1285, -1465, 384, -1215, -136, 1218, -1335, -874, 220, -1187, -1659, -1185, -1530, -1278, 794,
27    -1510, -854, -870, 478, -108, -308, 996, 991, 958, -1460, 1522, 1628,
28];
29
30pub(crate) const ML_KEM_768: MlKemParams<3> = MlKemParams {
31    eta1: 2,
32    polycompressedbytes: 128,
33    polyveccompressedbytes: 960,
34};
35pub(crate) const ML_KEM_1024: MlKemParams<4> = MlKemParams {
36    eta1: 2,
37    polycompressedbytes: 160,
38    polyveccompressedbytes: 1408,
39};
40
41/// ML-KEM error type.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MlKemError {
44    InvalidKey,
45    Random(RandomError),
46}
47
48impl core::fmt::Display for MlKemError {
49    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        match self {
51            MlKemError::InvalidKey => write!(f, "key is not valid"),
52            MlKemError::Random(err) => write!(f, "{err}"),
53        }
54    }
55}
56
57impl From<RandomError> for MlKemError {
58    fn from(err: RandomError) -> Self {
59        MlKemError::Random(err)
60    }
61}
62
63#[derive(Clone, Copy)]
64pub(crate) struct MlKemParams<const K: usize> {
65    pub(crate) eta1: usize,
66    pub(crate) polycompressedbytes: usize,
67    pub(crate) polyveccompressedbytes: usize,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
72pub(crate) struct Poly {
73    pub(crate) coeffs: [i16; N],
74}
75
76impl Default for Poly {
77    #[inline]
78    fn default() -> Self {
79        Self {
80            coeffs: [0; N],
81        }
82    }
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
86#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
87pub(crate) struct PolyVec<const K: usize> {
88    pub(crate) vec: [Poly; K],
89}
90
91impl<const K: usize> Default for PolyVec<K> {
92    #[inline]
93    fn default() -> Self {
94        Self {
95            vec: core::array::from_fn(|_| Poly::default()),
96        }
97    }
98}
99
100#[inline]
101pub(crate) fn crypto_kem_keypair_derand<const K: usize, const SECRET_KEY_SIZE: usize, const PUBLIC_KEY_SIZE: usize>(
102    params: &MlKemParams<K>,
103    coins: &[u8; 64],
104) -> ([u8; SECRET_KEY_SIZE], [u8; PUBLIC_KEY_SIZE]) {
105    let mut public_key = [0u8; PUBLIC_KEY_SIZE];
106    let mut secret_key = [0u8; SECRET_KEY_SIZE];
107
108    indcpa_keypair_derand::<K>(
109        params,
110        &mut public_key,
111        &mut secret_key[..indcpa_secret_key_bytes::<K>()],
112        &coins[..32],
113    );
114    secret_key[indcpa_secret_key_bytes::<K>()..indcpa_secret_key_bytes::<K>() + PUBLIC_KEY_SIZE]
115        .copy_from_slice(&public_key);
116
117    let public_key_hash = hash_h(&public_key);
118    secret_key[SECRET_KEY_SIZE - 64..SECRET_KEY_SIZE - 32].copy_from_slice(&public_key_hash);
119    secret_key[SECRET_KEY_SIZE - 32..].copy_from_slice(&coins[32..]);
120
121    (secret_key, public_key)
122}
123
124#[inline]
125pub(crate) fn crypto_kem_enc_derand<const K: usize, const PUBLIC_KEY_SIZE: usize, const CIPHERTEXT_SIZE: usize>(
126    params: &MlKemParams<K>,
127    public_key: &[u8; PUBLIC_KEY_SIZE],
128    coins: &[u8; 32],
129) -> ([u8; SHARED_SECRET_SIZE], [u8; CIPHERTEXT_SIZE]) {
130    let mut ciphertext = [0u8; CIPHERTEXT_SIZE];
131    let mut buf = [0u8; 64];
132    let mut kr = [0u8; 64];
133
134    buf[..32].copy_from_slice(coins);
135    buf[32..].copy_from_slice(&hash_h(public_key));
136    kr.copy_from_slice(&hash_g(&buf));
137
138    indcpa_enc::<K>(params, &mut ciphertext, &buf[..32], public_key, array_ref_32(&kr[32..64]));
139
140    let mut shared_secret = [0u8; SHARED_SECRET_SIZE];
141    shared_secret.copy_from_slice(&kr[..32]);
142
143    // FIPS 203 Section 3.3: destroy the message and the derived key material.
144    wipe(&mut buf);
145    wipe(&mut kr);
146
147    (shared_secret, ciphertext)
148}
149
150#[inline]
151pub(crate) fn crypto_kem_dec<const K: usize, const SECRET_KEY_SIZE: usize, const CIPHERTEXT_SIZE: usize>(
152    params: &MlKemParams<K>,
153    secret_key: &[u8; SECRET_KEY_SIZE],
154    ciphertext: &[u8; CIPHERTEXT_SIZE],
155) -> Result<[u8; SHARED_SECRET_SIZE], MlKemError> {
156    let public_key_offset = indcpa_secret_key_bytes::<K>();
157    let public_key_size = public_key_bytes::<K>();
158    if SECRET_KEY_SIZE != secret_key_size::<K>() {
159        return Err(MlKemError::InvalidKey);
160    }
161
162    let public_key = &secret_key[public_key_offset..public_key_offset + public_key_size];
163
164    // FIPS 203 Section 7.3 decapsulation-key check: the hash `H(ek)` stored in
165    // the key must match the hash of the encapsulation key embedded in it.
166    // Without this check a corrupted or mismatched key would silently produce a
167    // wrong shared secret instead of being rejected.
168    if !constant_time_eq(&hash_h(public_key), &secret_key[SECRET_KEY_SIZE - 64..SECRET_KEY_SIZE - 32]) {
169        return Err(MlKemError::InvalidKey);
170    }
171
172    let mut message_and_hash = [0u8; 64];
173    let mut kr = [0u8; 64];
174    let mut cmp = [0u8; CIPHERTEXT_SIZE];
175
176    indcpa_dec::<K>(
177        params,
178        &mut message_and_hash[..32],
179        ciphertext,
180        &secret_key[..public_key_offset],
181    );
182    message_and_hash[32..].copy_from_slice(&secret_key[SECRET_KEY_SIZE - 64..SECRET_KEY_SIZE - 32]);
183    kr.copy_from_slice(&hash_g(&message_and_hash));
184
185    indcpa_enc::<K>(params, &mut cmp, &message_and_hash[..32], public_key, array_ref_32(&kr[32..64]));
186
187    let mut shared_secret = rkprf(array_ref_32(&secret_key[SECRET_KEY_SIZE - 32..]), ciphertext);
188    cmov(&mut shared_secret, array_ref_32(&kr[..32]), constant_time_eq(ciphertext, &cmp));
189
190    // FIPS 203 Section 3.3: only the designated output may be retained.
191    wipe(&mut message_and_hash);
192    wipe(&mut kr);
193    wipe(&mut cmp);
194
195    Ok(shared_secret)
196}
197
198#[inline]
199pub(crate) fn indcpa_keypair_derand<const K: usize>(
200    params: &MlKemParams<K>,
201    public_key: &mut [u8],
202    secret_key: &mut [u8],
203    coins: &[u8],
204) {
205    debug_assert_eq!(public_key.len(), public_key_bytes::<K>());
206    debug_assert_eq!(secret_key.len(), indcpa_secret_key_bytes::<K>());
207    debug_assert_eq!(coins.len(), 32);
208
209    let mut g_input = [0u8; 33];
210    g_input[..32].copy_from_slice(coins);
211    g_input[32] = K as u8;
212    let mut seed_output = hash_g(&g_input);
213    let public_seed = array_ref_32(&seed_output[..32]);
214    let noise_seed = array_ref_32(&seed_output[32..64]);
215    let matrix = gen_matrix::<K>(public_seed, false);
216
217    let mut skpv = PolyVec::<K>::default();
218    let mut e = PolyVec::<K>::default();
219    for (index, poly) in skpv.vec.iter_mut().enumerate() {
220        *poly = poly_getnoise(noise_seed, index as u8, params.eta1);
221    }
222    for (index, poly) in e.vec.iter_mut().enumerate() {
223        *poly = poly_getnoise(noise_seed, (K + index) as u8, params.eta1);
224    }
225
226    polyvec_ntt(&mut skpv);
227    polyvec_ntt(&mut e);
228
229    let mut pkpv = PolyVec::<K>::default();
230    for i in 0..K {
231        pkpv.vec[i] = polyvec_basemul_acc_montgomery(&matrix[i], &skpv);
232        poly_tomont(&mut pkpv.vec[i]);
233    }
234
235    polyvec_add(&mut pkpv, &e);
236    polyvec_reduce(&mut pkpv);
237
238    pack_sk(secret_key, &skpv);
239    pack_pk(public_key, &pkpv, public_seed);
240
241    // FIPS 203 Section 3.3: the key-generation seed and noise seed are secret.
242    wipe(&mut g_input);
243    wipe(&mut seed_output);
244}
245
246#[inline]
247pub(crate) fn indcpa_enc<const K: usize>(
248    params: &MlKemParams<K>,
249    ciphertext: &mut [u8],
250    message: &[u8],
251    public_key: &[u8],
252    coins: &[u8; 32],
253) {
254    debug_assert_eq!(ciphertext.len(), ciphertext_bytes(params));
255    debug_assert_eq!(message.len(), 32);
256    debug_assert_eq!(public_key.len(), public_key_bytes::<K>());
257
258    let (pkpv, seed) = unpack_pk::<K>(public_key);
259    let at = gen_matrix::<K>(&seed, true);
260    let k = poly_frommsg(message);
261
262    let mut sp = PolyVec::<K>::default();
263    let mut ep = PolyVec::<K>::default();
264    for (index, poly) in sp.vec.iter_mut().enumerate() {
265        *poly = poly_getnoise(coins, index as u8, params.eta1);
266    }
267    let ep_nonce_offset = sp.vec.len();
268    for (index, poly) in ep.vec.iter_mut().enumerate() {
269        *poly = poly_getnoise(coins, (ep_nonce_offset + index) as u8, 2);
270    }
271    let epp = poly_getnoise(coins, (sp.vec.len() + ep.vec.len()) as u8, 2);
272
273    polyvec_ntt(&mut sp);
274
275    let mut b = PolyVec::<K>::default();
276    for i in 0..K {
277        b.vec[i] = polyvec_basemul_acc_montgomery(&at[i], &sp);
278    }
279    let mut v = polyvec_basemul_acc_montgomery(&pkpv, &sp);
280
281    polyvec_invntt_tomont(&mut b);
282    poly_invntt_tomont(&mut v);
283
284    polyvec_add(&mut b, &ep);
285    poly_add(&mut v, &epp);
286    poly_add(&mut v, &k);
287    polyvec_reduce(&mut b);
288    poly_reduce(&mut v);
289
290    pack_ciphertext(params, ciphertext, &b, &v);
291}
292
293#[inline]
294pub(crate) fn indcpa_dec<const K: usize>(
295    params: &MlKemParams<K>,
296    message: &mut [u8],
297    ciphertext: &[u8],
298    secret_key: &[u8],
299) {
300    debug_assert_eq!(message.len(), 32);
301    debug_assert_eq!(ciphertext.len(), ciphertext_bytes(params));
302    debug_assert_eq!(secret_key.len(), indcpa_secret_key_bytes::<K>());
303
304    let (mut b, v) = unpack_ciphertext::<K>(params, ciphertext);
305    let skpv = unpack_sk::<K>(secret_key);
306
307    polyvec_ntt(&mut b);
308    let mut mp = polyvec_basemul_acc_montgomery(&skpv, &b);
309    poly_invntt_tomont(&mut mp);
310    let product = mp.clone();
311    poly_sub(&mut mp, &v, &product);
312    poly_reduce(&mut mp);
313
314    message.copy_from_slice(&poly_tomsg(&mp));
315}
316
317#[inline]
318fn pack_pk<const K: usize>(out: &mut [u8], pk: &PolyVec<K>, seed: &[u8; 32]) {
319    let polyvec_bytes = polyvec_bytes::<K>();
320    polyvec_tobytes(&mut out[..polyvec_bytes], pk);
321    out[polyvec_bytes..polyvec_bytes + 32].copy_from_slice(seed);
322}
323
324#[inline]
325fn unpack_pk<const K: usize>(packed: &[u8]) -> (PolyVec<K>, [u8; 32]) {
326    let polyvec_bytes = polyvec_bytes::<K>();
327    let pk = polyvec_frombytes::<K>(&packed[..polyvec_bytes]);
328    let mut seed = [0u8; 32];
329    seed.copy_from_slice(&packed[polyvec_bytes..polyvec_bytes + 32]);
330    (pk, seed)
331}
332
333#[inline]
334fn pack_sk<const K: usize>(out: &mut [u8], sk: &PolyVec<K>) {
335    polyvec_tobytes(out, sk);
336}
337
338#[inline]
339fn unpack_sk<const K: usize>(packed: &[u8]) -> PolyVec<K> {
340    polyvec_frombytes(packed)
341}
342
343#[inline]
344fn pack_ciphertext<const K: usize>(params: &MlKemParams<K>, out: &mut [u8], b: &PolyVec<K>, v: &Poly) {
345    let split = params.polyveccompressedbytes;
346    polyvec_compress(params, &mut out[..split], b);
347    poly_compress(params, &mut out[split..split + params.polycompressedbytes], v);
348}
349
350#[inline]
351fn unpack_ciphertext<const K: usize>(params: &MlKemParams<K>, packed: &[u8]) -> (PolyVec<K>, Poly) {
352    let split = params.polyveccompressedbytes;
353    (
354        polyvec_decompress(params, &packed[..split]),
355        poly_decompress(params, &packed[split..split + params.polycompressedbytes]),
356    )
357}
358
359#[inline]
360pub(crate) fn gen_matrix<const K: usize>(seed: &[u8; 32], transpose: bool) -> [PolyVec<K>; K] {
361    let mut matrix = core::array::from_fn(|_| PolyVec::<K>::default());
362    for i in 0..K {
363        for j in 0..K {
364            let (x, y) = if transpose {
365                (i as u8, j as u8)
366            } else {
367                (j as u8, i as u8)
368            };
369            matrix[i].vec[j] = uniform_poly(seed, x, y);
370        }
371    }
372    matrix
373}
374
375#[inline]
376fn uniform_poly(seed: &[u8; 32], x: u8, y: u8) -> Poly {
377    let mut shake = Shake128::new();
378    shake.absorb(seed);
379    shake.absorb(&[x, y]);
380
381    let mut poly = Poly::default();
382    let mut ctr = 0usize;
383    let mut block = [0u8; SHAKE128_RATE];
384    while ctr < N {
385        shake.squeeze(&mut block);
386        ctr += rej_uniform(&mut poly.coeffs[ctr..], &block);
387    }
388    poly
389}
390
391#[inline]
392fn rej_uniform(out: &mut [i16], buf: &[u8]) -> usize {
393    let mut ctr = 0usize;
394    let mut pos = 0usize;
395    while ctr < out.len() && pos + 3 <= buf.len() {
396        let val0 = (((buf[pos] as u16) | ((buf[pos + 1] as u16) << 8)) & 0x0fff) as i16;
397        let val1 = ((((buf[pos + 1] as u16) >> 4) | ((buf[pos + 2] as u16) << 4)) & 0x0fff) as i16;
398        pos += 3;
399
400        if val0 < Q {
401            out[ctr] = val0;
402            ctr += 1;
403        }
404        if ctr < out.len() && val1 < Q {
405            out[ctr] = val1;
406            ctr += 1;
407        }
408    }
409    ctr
410}
411
412#[inline]
413pub(crate) fn poly_getnoise(seed: &[u8; 32], nonce: u8, eta: usize) -> Poly {
414    debug_assert_eq!(eta, 2);
415    let mut input = [0u8; 33];
416    input[..32].copy_from_slice(seed);
417    input[32] = nonce;
418    let mut buf = [0u8; 128];
419    let mut shake256 = Shake256::new();
420    shake256.absorb(&input);
421    shake256.squeeze(&mut buf);
422    cbd2(&buf)
423}
424
425#[inline]
426fn cbd2(buf: &[u8; 128]) -> Poly {
427    let mut poly = Poly::default();
428    for i in 0..(N / 8) {
429        let t = load32(&buf[4 * i..4 * i + 4]);
430        let mut d = t & 0x5555_5555;
431        d += (t >> 1) & 0x5555_5555;
432        for j in 0..8 {
433            let a = ((d >> (4 * j)) & 0x3) as i16;
434            let b = ((d >> (4 * j + 2)) & 0x3) as i16;
435            poly.coeffs[8 * i + j] = a - b;
436        }
437    }
438    poly
439}
440
441#[inline]
442pub(crate) fn polyvec_compress<const K: usize>(params: &MlKemParams<K>, out: &mut [u8], a: &PolyVec<K>) {
443    match params.polyveccompressedbytes {
444        960 => {
445            let mut offset = 0usize;
446            for poly in &a.vec {
447                for chunk in poly.coeffs.as_chunks::<4>().0 {
448                    let mut t = [0u16; 4];
449                    for (dst, coeff) in t.iter_mut().zip(chunk.iter()) {
450                        let mut u = *coeff as i32;
451                        u += (u >> 15) & Q as i32;
452                        let mut d0 = u as u64;
453                        d0 <<= 10;
454                        d0 += 1665;
455                        d0 *= 1_290_167;
456                        d0 >>= 32;
457                        *dst = (d0 as u16) & 0x03ff;
458                    }
459                    out[offset] = t[0] as u8;
460                    out[offset + 1] = ((t[0] >> 8) as u8) | ((t[1] << 2) as u8);
461                    out[offset + 2] = ((t[1] >> 6) as u8) | ((t[2] << 4) as u8);
462                    out[offset + 3] = ((t[2] >> 4) as u8) | ((t[3] << 6) as u8);
463                    out[offset + 4] = (t[3] >> 2) as u8;
464                    offset += 5;
465                }
466            }
467        }
468        1408 => {
469            let mut offset = 0usize;
470            for poly in &a.vec {
471                for chunk in poly.coeffs.as_chunks::<8>().0 {
472                    let mut t = [0u16; 8];
473                    for (dst, coeff) in t.iter_mut().zip(chunk.iter()) {
474                        let mut u = *coeff as i32;
475                        u += (u >> 15) & Q as i32;
476                        let mut d0 = u as u64;
477                        d0 <<= 11;
478                        d0 += 1664;
479                        d0 *= 645_084;
480                        d0 >>= 31;
481                        *dst = (d0 as u16) & 0x07ff;
482                    }
483                    out[offset] = t[0] as u8;
484                    out[offset + 1] = ((t[0] >> 8) as u8) | ((t[1] << 3) as u8);
485                    out[offset + 2] = ((t[1] >> 5) as u8) | ((t[2] << 6) as u8);
486                    out[offset + 3] = (t[2] >> 2) as u8;
487                    out[offset + 4] = ((t[2] >> 10) as u8) | ((t[3] << 1) as u8);
488                    out[offset + 5] = ((t[3] >> 7) as u8) | ((t[4] << 4) as u8);
489                    out[offset + 6] = ((t[4] >> 4) as u8) | ((t[5] << 7) as u8);
490                    out[offset + 7] = (t[5] >> 1) as u8;
491                    out[offset + 8] = ((t[5] >> 9) as u8) | ((t[6] << 2) as u8);
492                    out[offset + 9] = ((t[6] >> 6) as u8) | ((t[7] << 5) as u8);
493                    out[offset + 10] = (t[7] >> 3) as u8;
494                    offset += 11;
495                }
496            }
497        }
498        _ => unreachable!(),
499    }
500}
501
502#[inline]
503pub(crate) fn polyvec_decompress<const K: usize>(params: &MlKemParams<K>, input: &[u8]) -> PolyVec<K> {
504    let mut out = PolyVec::<K>::default();
505    match params.polyveccompressedbytes {
506        960 => {
507            let mut offset = 0usize;
508            for poly in &mut out.vec {
509                for j in 0..(N / 4) {
510                    let t0 = (input[offset] as u16) | ((input[offset + 1] as u16) << 8);
511                    let t1 = ((input[offset + 1] as u16) >> 2) | ((input[offset + 2] as u16) << 6);
512                    let t2 = ((input[offset + 2] as u16) >> 4) | ((input[offset + 3] as u16) << 4);
513                    let t3 = ((input[offset + 3] as u16) >> 6) | ((input[offset + 4] as u16) << 2);
514                    offset += 5;
515                    poly.coeffs[4 * j] = ((((t0 & 0x03ff) as u32) * Q as u32 + 512) >> 10) as i16;
516                    poly.coeffs[4 * j + 1] = ((((t1 & 0x03ff) as u32) * Q as u32 + 512) >> 10) as i16;
517                    poly.coeffs[4 * j + 2] = ((((t2 & 0x03ff) as u32) * Q as u32 + 512) >> 10) as i16;
518                    poly.coeffs[4 * j + 3] = ((((t3 & 0x03ff) as u32) * Q as u32 + 512) >> 10) as i16;
519                }
520            }
521        }
522        1408 => {
523            let mut offset = 0usize;
524            for poly in &mut out.vec {
525                for j in 0..(N / 8) {
526                    let t0 = (input[offset] as u16) | ((input[offset + 1] as u16) << 8);
527                    let t1 = ((input[offset + 1] as u16) >> 3) | ((input[offset + 2] as u16) << 5);
528                    let t2 = ((input[offset + 2] as u16) >> 6)
529                        | ((input[offset + 3] as u16) << 2)
530                        | ((input[offset + 4] as u16) << 10);
531                    let t3 = ((input[offset + 4] as u16) >> 1) | ((input[offset + 5] as u16) << 7);
532                    let t4 = ((input[offset + 5] as u16) >> 4) | ((input[offset + 6] as u16) << 4);
533                    let t5 = ((input[offset + 6] as u16) >> 7)
534                        | ((input[offset + 7] as u16) << 1)
535                        | ((input[offset + 8] as u16) << 9);
536                    let t6 = ((input[offset + 8] as u16) >> 2) | ((input[offset + 9] as u16) << 6);
537                    let t7 = ((input[offset + 9] as u16) >> 5) | ((input[offset + 10] as u16) << 3);
538                    offset += 11;
539                    let values = [t0, t1, t2, t3, t4, t5, t6, t7];
540                    for (k, value) in values.into_iter().enumerate() {
541                        poly.coeffs[8 * j + k] = ((((value & 0x07ff) as u32) * Q as u32 + 1024) >> 11) as i16;
542                    }
543                }
544            }
545        }
546        _ => unreachable!(),
547    }
548    out
549}
550
551#[inline]
552fn polyvec_tobytes<const K: usize>(out: &mut [u8], polyvec: &PolyVec<K>) {
553    for (i, poly) in polyvec.vec.iter().enumerate() {
554        poly_tobytes(&mut out[i * POLY_BYTES..(i + 1) * POLY_BYTES], poly);
555    }
556}
557
558#[inline]
559fn polyvec_frombytes<const K: usize>(input: &[u8]) -> PolyVec<K> {
560    let mut out = PolyVec::<K>::default();
561    for (i, poly) in out.vec.iter_mut().enumerate() {
562        *poly = poly_frombytes(&input[i * POLY_BYTES..(i + 1) * POLY_BYTES]);
563    }
564    out
565}
566
567/// FIPS 203 Section 7.2 modulus check for an ML-KEM encapsulation key.
568///
569/// Decodes the `ByteDecode₁₂` coefficients of the encoded `t̂` (the leading
570/// `polyvec_bytes::<K>()` bytes of the encapsulation key) and returns `false`
571/// when any coefficient is not in `[0, q-1]`. The trailing `ρ` seed is not
572/// inspected.
573#[inline]
574pub(crate) fn encapsulation_key_modulus_check<const K: usize>(public_key: &[u8]) -> bool {
575    let encoded = &public_key[..polyvec_bytes::<K>()];
576    for chunk in encoded.chunks(3) {
577        let b0 = chunk[0] as u16;
578        let b1 = chunk[1] as u16;
579        let b2 = chunk[2] as u16;
580        let c0 = (b0 | (b1 << 8)) & 0x0fff;
581        let c1 = ((b1 >> 4) | (b2 << 4)) & 0x0fff;
582        if c0 >= Q as u16 || c1 >= Q as u16 {
583            return false;
584        }
585    }
586    true
587}
588
589#[inline]
590fn polyvec_ntt<const K: usize>(polyvec: &mut PolyVec<K>) {
591    for poly in &mut polyvec.vec {
592        poly_ntt(poly);
593    }
594}
595
596#[inline]
597fn polyvec_invntt_tomont<const K: usize>(polyvec: &mut PolyVec<K>) {
598    for poly in &mut polyvec.vec {
599        poly_invntt_tomont(poly);
600    }
601}
602
603#[inline]
604fn polyvec_basemul_acc_montgomery<const K: usize>(a: &PolyVec<K>, b: &PolyVec<K>) -> Poly {
605    let mut out = poly_basemul_montgomery(&a.vec[0], &b.vec[0]);
606    for i in 1..K {
607        let t = poly_basemul_montgomery(&a.vec[i], &b.vec[i]);
608        poly_add(&mut out, &t);
609    }
610    poly_reduce(&mut out);
611    out
612}
613
614#[inline]
615fn polyvec_reduce<const K: usize>(polyvec: &mut PolyVec<K>) {
616    for poly in &mut polyvec.vec {
617        poly_reduce(poly);
618    }
619}
620
621#[inline]
622fn polyvec_add<const K: usize>(left: &mut PolyVec<K>, right: &PolyVec<K>) {
623    for i in 0..K {
624        poly_add(&mut left.vec[i], &right.vec[i]);
625    }
626}
627
628#[inline]
629fn poly_compress<const K: usize>(params: &MlKemParams<K>, out: &mut [u8], poly: &Poly) {
630    match params.polycompressedbytes {
631        128 => {
632            let mut offset = 0usize;
633            for chunk in poly.coeffs.as_chunks::<8>().0 {
634                let mut t = [0u8; 8];
635                for (dst, coeff) in t.iter_mut().zip(chunk.iter()) {
636                    let mut u = *coeff as i32;
637                    u += (u >> 15) & Q as i32;
638                    let mut d0 = ((u as u32) << 4) as u64;
639                    d0 += 1665;
640                    d0 *= 80_635;
641                    d0 >>= 28;
642                    *dst = (d0 as u8) & 0x0f;
643                }
644                out[offset] = t[0] | (t[1] << 4);
645                out[offset + 1] = t[2] | (t[3] << 4);
646                out[offset + 2] = t[4] | (t[5] << 4);
647                out[offset + 3] = t[6] | (t[7] << 4);
648                offset += 4;
649            }
650        }
651        160 => {
652            let mut offset = 0usize;
653            for chunk in poly.coeffs.as_chunks::<8>().0 {
654                let mut t = [0u8; 8];
655                for (dst, coeff) in t.iter_mut().zip(chunk.iter()) {
656                    let mut u = *coeff as i32;
657                    u += (u >> 15) & Q as i32;
658                    let mut d0 = ((u as u32) << 5) as u64;
659                    d0 += 1664;
660                    d0 *= 40_318;
661                    d0 >>= 27;
662                    *dst = (d0 as u8) & 0x1f;
663                }
664                out[offset] = t[0] | (t[1] << 5);
665                out[offset + 1] = (t[1] >> 3) | (t[2] << 2) | (t[3] << 7);
666                out[offset + 2] = (t[3] >> 1) | (t[4] << 4);
667                out[offset + 3] = (t[4] >> 4) | (t[5] << 1) | (t[6] << 6);
668                out[offset + 4] = (t[6] >> 2) | (t[7] << 3);
669                offset += 5;
670            }
671        }
672        _ => unreachable!(),
673    }
674}
675
676#[inline]
677fn poly_decompress<const K: usize>(params: &MlKemParams<K>, input: &[u8]) -> Poly {
678    let mut out = Poly::default();
679    match params.polycompressedbytes {
680        128 => {
681            for i in 0..(N / 2) {
682                out.coeffs[2 * i] = ((((input[i] & 0x0f) as u16) * Q as u16 + 8) >> 4) as i16;
683                out.coeffs[2 * i + 1] = ((((input[i] >> 4) as u16) * Q as u16 + 8) >> 4) as i16;
684            }
685        }
686        160 => {
687            let mut offset = 0usize;
688            for i in 0..(N / 8) {
689                let t0 = input[offset] >> 0;
690                let t1 = (input[offset] >> 5) | (input[offset + 1] << 3);
691                let t2 = input[offset + 1] >> 2;
692                let t3 = (input[offset + 1] >> 7) | (input[offset + 2] << 1);
693                let t4 = (input[offset + 2] >> 4) | (input[offset + 3] << 4);
694                let t5 = input[offset + 3] >> 1;
695                let t6 = (input[offset + 3] >> 6) | (input[offset + 4] << 2);
696                let t7 = input[offset + 4] >> 3;
697                offset += 5;
698                let values = [t0, t1, t2, t3, t4, t5, t6, t7];
699                for (j, value) in values.into_iter().enumerate() {
700                    out.coeffs[8 * i + j] = (((value as u32 & 31) * Q as u32 + 16) >> 5) as i16;
701                }
702            }
703        }
704        _ => unreachable!(),
705    }
706    out
707}
708
709#[inline]
710fn poly_tobytes(out: &mut [u8], poly: &Poly) {
711    for i in 0..(N / 2) {
712        let mut t0 = poly.coeffs[2 * i] as i32;
713        t0 += (t0 >> 15) & Q as i32;
714        let mut t1 = poly.coeffs[2 * i + 1] as i32;
715        t1 += (t1 >> 15) & Q as i32;
716        out[3 * i] = t0 as u8;
717        out[3 * i + 1] = ((t0 >> 8) as u8) | ((t1 << 4) as u8);
718        out[3 * i + 2] = (t1 >> 4) as u8;
719    }
720}
721
722#[inline]
723fn poly_frombytes(input: &[u8]) -> Poly {
724    let mut out = Poly::default();
725    for i in 0..(N / 2) {
726        out.coeffs[2 * i] = (((input[3 * i] as u16) | ((input[3 * i + 1] as u16) << 8)) & 0x0fff) as i16;
727        out.coeffs[2 * i + 1] = ((((input[3 * i + 1] as u16) >> 4) | ((input[3 * i + 2] as u16) << 4)) & 0x0fff) as i16;
728    }
729    out
730}
731
732#[inline]
733pub(crate) fn poly_frommsg(msg: &[u8]) -> Poly {
734    let mut out = Poly::default();
735    let half_q: i16 = ((Q + 1) / 2) as i16;
736    for i in 0..(N / 8) {
737        for j in 0..8 {
738            let bit = ((msg[i] >> j) & 1) as i16;
739            out.coeffs[8 * i + j] = (-bit) & half_q;
740        }
741    }
742    out
743}
744
745#[inline]
746pub(crate) fn poly_tomsg(poly: &Poly) -> [u8; 32] {
747    let mut msg = [0u8; 32];
748    for i in 0..(N / 8) {
749        for j in 0..8 {
750            let mut t = poly.coeffs[8 * i + j] as i32;
751            t <<= 1;
752            t += 1665;
753            t *= 80_635;
754            t >>= 28;
755            msg[i] |= ((t & 1) as u8) << j;
756        }
757    }
758    msg
759}
760
761#[inline]
762fn poly_ntt(poly: &mut Poly) {
763    ntt(&mut poly.coeffs);
764    poly_reduce(poly);
765}
766
767#[inline]
768fn poly_invntt_tomont(poly: &mut Poly) {
769    invntt(&mut poly.coeffs);
770}
771
772#[inline]
773fn poly_basemul_montgomery(a: &Poly, b: &Poly) -> Poly {
774    let mut out = Poly::default();
775    for i in 0..(N / 4) {
776        let r0 = basemul(
777            [a.coeffs[4 * i], a.coeffs[4 * i + 1]],
778            [b.coeffs[4 * i], b.coeffs[4 * i + 1]],
779            ZETAS[64 + i],
780        );
781        out.coeffs[4 * i] = r0[0];
782        out.coeffs[4 * i + 1] = r0[1];
783
784        let r1 = basemul(
785            [a.coeffs[4 * i + 2], a.coeffs[4 * i + 3]],
786            [b.coeffs[4 * i + 2], b.coeffs[4 * i + 3]],
787            -ZETAS[64 + i],
788        );
789        out.coeffs[4 * i + 2] = r1[0];
790        out.coeffs[4 * i + 3] = r1[1];
791    }
792    out
793}
794
795#[inline]
796fn poly_tomont(poly: &mut Poly) {
797    for coeff in &mut poly.coeffs {
798        *coeff = montgomery_reduce(*coeff as i32 * 1353);
799    }
800}
801
802#[inline]
803fn poly_reduce(poly: &mut Poly) {
804    for coeff in &mut poly.coeffs {
805        *coeff = barrett_reduce(*coeff);
806    }
807}
808
809#[inline]
810fn poly_add(left: &mut Poly, right: &Poly) {
811    for i in 0..N {
812        left.coeffs[i] = (left.coeffs[i] as i32 + right.coeffs[i] as i32) as i16;
813    }
814}
815
816#[inline]
817fn poly_sub(out: &mut Poly, left: &Poly, right: &Poly) {
818    for i in 0..N {
819        out.coeffs[i] = (left.coeffs[i] as i32 - right.coeffs[i] as i32) as i16;
820    }
821}
822
823#[inline]
824fn ntt(r: &mut [i16; N]) {
825    let mut k = 1usize;
826    let mut len = 128usize;
827    while len >= 2 {
828        let mut start = 0usize;
829        while start < N {
830            let zeta = ZETAS[k];
831            k += 1;
832            for j in start..start + len {
833                let t = fqmul(zeta, r[j + len]);
834                let rj = r[j] as i32;
835                r[j + len] = (rj - t as i32) as i16;
836                r[j] = (rj + t as i32) as i16;
837            }
838            start += 2 * len;
839        }
840        len >>= 1;
841    }
842}
843
844#[inline]
845fn invntt(r: &mut [i16; N]) {
846    let mut k = 127usize;
847    let mut len = 2usize;
848    while len <= 128 {
849        let mut start = 0usize;
850        while start < N {
851            let zeta = ZETAS[k];
852            k -= 1;
853            for j in start..start + len {
854                let t = r[j];
855                r[j] = barrett_reduce((t as i32 + r[j + len] as i32) as i16);
856                r[j + len] = fqmul(zeta, (r[j + len] as i32 - t as i32) as i16);
857            }
858            start += 2 * len;
859        }
860        len <<= 1;
861    }
862
863    for coeff in r.iter_mut() {
864        *coeff = fqmul(*coeff, MONT_SQUARED_DIV_N);
865    }
866}
867
868#[inline]
869fn basemul(a: [i16; 2], b: [i16; 2], zeta: i16) -> [i16; 2] {
870    let mut out = [0i16; 2];
871    out[0] = fqmul(a[1], b[1]);
872    out[0] = fqmul(out[0], zeta);
873    out[0] = (out[0] as i32 + fqmul(a[0], b[0]) as i32) as i16;
874    out[1] = (fqmul(a[0], b[1]) as i32 + fqmul(a[1], b[0]) as i32) as i16;
875    out
876}
877
878#[inline]
879fn fqmul(a: i16, b: i16) -> i16 {
880    montgomery_reduce(a as i32 * b as i32)
881}
882
883#[inline]
884fn montgomery_reduce(a: i32) -> i16 {
885    let t = (a as i16).wrapping_mul(QINV) as i32;
886    ((a - t * Q as i32) >> 16) as i16
887}
888
889#[inline]
890fn barrett_reduce(a: i16) -> i16 {
891    const V: i32 = ((1 << 26) + (Q as i32 / 2)) / Q as i32;
892    let t = ((V * a as i32 + (1 << 25)) >> 26) * Q as i32;
893    (a as i32 - t) as i16
894}
895
896#[inline]
897fn hash_h(data: &[u8]) -> [u8; 32] {
898    use crate::Hasher;
899    let mut hasher = Sha3_256::new();
900    hasher.update(data);
901    hasher.sum().as_ref().try_into().unwrap()
902}
903
904#[inline]
905fn hash_g(data: &[u8]) -> [u8; 64] {
906    use crate::Hasher;
907    let mut hasher = Sha3_512::new();
908    hasher.update(data);
909    hasher.sum().as_ref().try_into().unwrap()
910}
911
912#[inline]
913fn rkprf(cipher_key: &[u8; 32], ciphertext: &[u8]) -> [u8; 32] {
914    let mut shake = Shake256::new();
915    shake.absorb(cipher_key);
916    shake.absorb(ciphertext);
917    let mut out = [0u8; 32];
918    shake.squeeze(&mut out);
919    out
920}
921
922/// Constant-time conditional move: if `cond` is true, copies `value` into `out`.
923/// Uses a compiler barrier on the mask to prevent the optimizer from turning this
924/// into a branch (which would leak timing information in the FO transform).
925#[inline]
926fn cmov(out: &mut [u8; 32], value: &[u8; 32], cond: bool) {
927    let mask = ct_mask_u8(cond);
928    for i in 0..32 {
929        out[i] ^= mask & (out[i] ^ value[i]);
930    }
931}
932
933/// Converts a boolean condition to a constant-time mask (0x00 or 0xFF) with a compiler
934/// barrier to prevent optimization into a branch.
935#[inline]
936fn ct_mask_u8(cond: bool) -> u8 {
937    let mask = 0u8.wrapping_sub(cond as u8);
938    // Prevent the compiler from reasoning about the mask value and potentially
939    // converting downstream code into a conditional branch.
940    ct_barrier_u8(mask)
941}
942
943#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
944#[inline]
945fn ct_barrier_u8(mut value: u8) -> u8 {
946    // SAFETY: the inline asm is a no-op that forces the compiler to treat `value`
947    // as an opaque value, preventing branch-based optimizations.
948    unsafe {
949        core::arch::asm!("/* {0} */", inout(reg_byte) value, options(pure, nomem, nostack, preserves_flags));
950    }
951    value
952}
953
954#[cfg(any(
955    target_arch = "aarch64",
956    target_arch = "arm",
957    target_arch = "riscv32",
958    target_arch = "riscv64"
959))]
960#[inline]
961#[allow(asm_sub_register)]
962fn ct_barrier_u8(mut value: u8) -> u8 {
963    unsafe {
964        core::arch::asm!("/* {0} */", inout(reg) value, options(pure, nomem, nostack, preserves_flags));
965    }
966    value
967}
968
969#[cfg(not(any(
970    target_arch = "x86",
971    target_arch = "x86_64",
972    target_arch = "aarch64",
973    target_arch = "arm",
974    target_arch = "riscv32",
975    target_arch = "riscv64"
976)))]
977#[inline(never)]
978fn ct_barrier_u8(value: u8) -> u8 {
979    core::hint::black_box(value)
980}
981
982#[inline]
983fn load32(input: &[u8]) -> u32 {
984    (input[0] as u32) | ((input[1] as u32) << 8) | ((input[2] as u32) << 16) | ((input[3] as u32) << 24)
985}
986
987#[inline]
988pub(crate) fn public_key_bytes<const K: usize>() -> usize {
989    polyvec_bytes::<K>() + SYMBYTES
990}
991
992#[inline]
993pub(crate) fn indcpa_secret_key_bytes<const K: usize>() -> usize {
994    polyvec_bytes::<K>()
995}
996
997#[inline]
998fn polyvec_bytes<const K: usize>() -> usize {
999    K * POLY_BYTES
1000}
1001
1002#[inline]
1003pub(crate) fn secret_key_size<const K: usize>() -> usize {
1004    indcpa_secret_key_bytes::<K>() + public_key_bytes::<K>() + 2 * SYMBYTES
1005}
1006
1007#[inline]
1008fn ciphertext_bytes<const K: usize>(params: &MlKemParams<K>) -> usize {
1009    params.polyveccompressedbytes + params.polycompressedbytes
1010}
1011
1012#[inline]
1013fn array_ref_32(input: &[u8]) -> &[u8; 32] {
1014    input.try_into().expect("slice length should be 32")
1015}
1016
1017/// Best-effort zeroization of an intermediate secret buffer.
1018///
1019/// When the `zeroize` feature is enabled the bytes are overwritten with zeros;
1020/// otherwise this is a no-op. Secret heap/stack buffers that hold intermediate
1021/// values (FIPS 203 Section 3.3) are wiped through this helper before returning.
1022#[inline]
1023fn wipe(bytes: &mut [u8]) {
1024    #[cfg(feature = "zeroize")]
1025    {
1026        use zeroize::Zeroize;
1027        bytes.zeroize();
1028    }
1029    #[cfg(not(feature = "zeroize"))]
1030    {
1031        let _ = bytes;
1032    }
1033}
1034
1035#[cfg(test)]
1036pub(crate) fn decode_hex_array<const N: usize>(s: &str) -> [u8; N] {
1037    let bytes = hex::decode(s).expect("valid hex");
1038    assert_eq!(bytes.len(), N);
1039    let mut out = [0u8; N];
1040    out.copy_from_slice(&bytes);
1041    out
1042}
1043
1044#[cfg(test)]
1045pub(crate) fn sha3_256_hex(data: &[u8]) -> String {
1046    use crate::Hasher;
1047    let mut hasher = Sha3_256::new();
1048    hasher.update(data);
1049    hex::encode(hasher.sum().as_ref())
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055
1056    #[test]
1057    fn poly_frommsg_tomsg_roundtrip() {
1058        for pattern in 0..=255u16 {
1059            let mut msg = [0u8; 32];
1060            msg[0] = pattern as u8;
1061            msg[1] = (pattern >> 8) as u8;
1062            let poly = poly_frommsg(&msg);
1063            let recovered = poly_tomsg(&poly);
1064            assert_eq!(msg, recovered, "roundtrip failed for pattern {pattern:#06x}");
1065        }
1066    }
1067
1068    #[test]
1069    fn poly_frommsg_constant_time_produces_expected_values() {
1070        let half_q = ((Q + 1) / 2) as i16;
1071        let mut msg = [0u8; 32];
1072        msg[0] = 0b1010_1010;
1073        msg[1] = 0b0101_0101;
1074        let poly = poly_frommsg(&msg);
1075        assert_eq!(poly.coeffs[0], 0);
1076        assert_eq!(poly.coeffs[1], half_q);
1077        assert_eq!(poly.coeffs[2], 0);
1078        assert_eq!(poly.coeffs[3], half_q);
1079        assert_eq!(poly.coeffs[8], half_q);
1080        assert_eq!(poly.coeffs[9], 0);
1081        assert_eq!(poly.coeffs[10], half_q);
1082        assert_eq!(poly.coeffs[11], 0);
1083    }
1084
1085    #[test]
1086    fn cmov_selects_correctly() {
1087        let mut out = [0xAAu8; 32];
1088        let value = [0xBBu8; 32];
1089        cmov(&mut out, &value, false);
1090        assert_eq!(out, [0xAAu8; 32], "cmov with false should not modify output");
1091
1092        cmov(&mut out, &value, true);
1093        assert_eq!(out, [0xBBu8; 32], "cmov with true should copy value");
1094    }
1095
1096    #[test]
1097    fn cmov_is_idempotent() {
1098        let mut out = [0x42u8; 32];
1099        let value = [0x42u8; 32];
1100        cmov(&mut out, &value, true);
1101        assert_eq!(out, [0x42u8; 32]);
1102        cmov(&mut out, &value, false);
1103        assert_eq!(out, [0x42u8; 32]);
1104    }
1105
1106    #[test]
1107    fn barrett_reduce_produces_values_in_range() {
1108        // Barrett reduce should map any i16 to the range [-(Q-1)/2, (Q-1)/2] approximately
1109        for val in [0i16, 1, -1, Q - 1, -(Q - 1), Q, -Q, 3000, -3000, i16::MAX, i16::MIN] {
1110            let reduced = barrett_reduce(val);
1111            // The reduced value should be congruent to val mod Q
1112            let diff = (val as i32 - reduced as i32).rem_euclid(Q as i32);
1113            assert!(diff == 0, "barrett_reduce({val}) = {reduced} not congruent mod Q");
1114        }
1115    }
1116
1117    #[test]
1118    fn montgomery_reduce_correctness() {
1119        // Montgomery reduce: given a, return a * R^(-1) mod Q where R = 2^16
1120        // Verify: montgomery_reduce(a * R) == a mod Q for small a
1121        let r_mod_q: i32 = (1i32 << 16) % Q as i32; // R mod Q = 65536 mod 3329 = 2285
1122        for val in [0i16, 1, -1, 100, -100, Q - 1, -(Q - 1)] {
1123            let product = val as i32 * r_mod_q;
1124            let result = montgomery_reduce(product);
1125            // result should be congruent to val mod Q
1126            let diff = (val as i32 - result as i32).rem_euclid(Q as i32);
1127            assert!(
1128                diff == 0,
1129                "montgomery_reduce({val} * R) = {result}, expected congruent to {val} mod Q"
1130            );
1131        }
1132    }
1133
1134    #[test]
1135    fn ntt_invntt_preserves_polynomial_structure() {
1136        // NTT->InvNTT roundtrip preserves polynomial relationships.
1137        // The full KEM roundtrip tests already validate NTT correctness,
1138        // but this verifies that two distinct inputs remain distinct after transform.
1139        let mut poly_a = Poly::default();
1140        let mut poly_b = Poly::default();
1141        for i in 0..N {
1142            poly_a.coeffs[i] = (i as i16 * 7 + 3) % Q;
1143            poly_b.coeffs[i] = (i as i16 * 11 + 5) % Q;
1144        }
1145        poly_ntt(&mut poly_a);
1146        poly_ntt(&mut poly_b);
1147        // NTT outputs should be different for different inputs
1148        assert_ne!(poly_a.coeffs, poly_b.coeffs);
1149
1150        poly_invntt_tomont(&mut poly_a);
1151        poly_invntt_tomont(&mut poly_b);
1152        // After roundtrip, they should still be different
1153        assert_ne!(poly_a.coeffs, poly_b.coeffs);
1154    }
1155
1156    #[test]
1157    fn poly_compress_decompress_roundtrip_4bit() {
1158        // For 4-bit compression (ML-KEM-768)
1159        let params = &ML_KEM_768;
1160        let mut poly = Poly::default();
1161        for i in 0..N {
1162            poly.coeffs[i] = ((i * 13) % Q as usize) as i16;
1163        }
1164        let mut compressed = [0u8; 128];
1165        poly_compress::<3>(params, &mut compressed, &poly);
1166        let decompressed = poly_decompress::<3>(params, &compressed);
1167        // Compression is lossy but within rounding error
1168        for i in 0..N {
1169            let orig = poly.coeffs[i] as i32;
1170            let dec = decompressed.coeffs[i] as i32;
1171            // Maximum rounding error for d-bit compression: Q / (2^(d+1))
1172            // For 4 bits: Q/32 ≈ 104
1173            let error = ((orig - dec).rem_euclid(Q as i32)).min((dec - orig).rem_euclid(Q as i32));
1174            assert!(
1175                error <= Q as i32 / 32 + 1,
1176                "4-bit compress/decompress error too large at index {i}: orig={orig}, dec={dec}, error={error}"
1177            );
1178        }
1179    }
1180
1181    #[test]
1182    fn poly_compress_decompress_roundtrip_5bit() {
1183        // For 5-bit compression (ML-KEM-1024)
1184        let params = &ML_KEM_1024;
1185        let mut poly = Poly::default();
1186        for i in 0..N {
1187            poly.coeffs[i] = ((i * 13) % Q as usize) as i16;
1188        }
1189        let mut compressed = [0u8; 160];
1190        poly_compress::<4>(params, &mut compressed, &poly);
1191        let decompressed = poly_decompress::<4>(params, &compressed);
1192        for i in 0..N {
1193            let orig = poly.coeffs[i] as i32;
1194            let dec = decompressed.coeffs[i] as i32;
1195            let error = ((orig - dec).rem_euclid(Q as i32)).min((dec - orig).rem_euclid(Q as i32));
1196            assert!(
1197                error <= Q as i32 / 64 + 1,
1198                "5-bit compress/decompress error too large at index {i}: orig={orig}, dec={dec}, error={error}"
1199            );
1200        }
1201    }
1202
1203    #[test]
1204    fn polyvec_compress_decompress_roundtrip_10bit() {
1205        let params = &ML_KEM_768;
1206        let mut pv = PolyVec::<3>::default();
1207        for k in 0..3 {
1208            for i in 0..N {
1209                pv.vec[k].coeffs[i] = ((k * 97 + i * 13) % Q as usize) as i16;
1210            }
1211        }
1212        let mut compressed = [0u8; 960];
1213        polyvec_compress(params, &mut compressed, &pv);
1214        let decompressed = polyvec_decompress::<3>(params, &compressed);
1215        for k in 0..3 {
1216            for i in 0..N {
1217                let orig = pv.vec[k].coeffs[i] as i32;
1218                let dec = decompressed.vec[k].coeffs[i] as i32;
1219                let error = ((orig - dec).rem_euclid(Q as i32)).min((dec - orig).rem_euclid(Q as i32));
1220                assert!(
1221                    error <= Q as i32 / 2048 + 1,
1222                    "10-bit compress/decompress error at [{k}][{i}]: orig={orig}, dec={dec}, error={error}"
1223                );
1224            }
1225        }
1226    }
1227
1228    #[test]
1229    fn polyvec_compress_decompress_roundtrip_11bit() {
1230        let params = &ML_KEM_1024;
1231        let mut pv = PolyVec::<4>::default();
1232        for k in 0..4 {
1233            for i in 0..N {
1234                pv.vec[k].coeffs[i] = ((k * 97 + i * 13) % Q as usize) as i16;
1235            }
1236        }
1237        let mut compressed = [0u8; 1408];
1238        polyvec_compress(params, &mut compressed, &pv);
1239        let decompressed = polyvec_decompress::<4>(params, &compressed);
1240        for k in 0..4 {
1241            for i in 0..N {
1242                let orig = pv.vec[k].coeffs[i] as i32;
1243                let dec = decompressed.vec[k].coeffs[i] as i32;
1244                let error = ((orig - dec).rem_euclid(Q as i32)).min((dec - orig).rem_euclid(Q as i32));
1245                assert!(
1246                    error <= Q as i32 / 4096 + 1,
1247                    "11-bit compress/decompress error at [{k}][{i}]: orig={orig}, dec={dec}, error={error}"
1248                );
1249            }
1250        }
1251    }
1252
1253    #[test]
1254    fn poly_tobytes_frombytes_roundtrip() {
1255        let mut poly = Poly::default();
1256        for i in 0..N {
1257            poly.coeffs[i] = (i as i16 * 13) % Q;
1258        }
1259        let mut buf = [0u8; POLY_BYTES];
1260        poly_tobytes(&mut buf, &poly);
1261        let recovered = poly_frombytes(&buf);
1262        assert_eq!(poly.coeffs, recovered.coeffs);
1263    }
1264
1265    #[test]
1266    fn gen_matrix_transpose_relationship() {
1267        let seed = [42u8; 32];
1268        let matrix = gen_matrix::<3>(&seed, false);
1269        let transposed = gen_matrix::<3>(&seed, true);
1270        for i in 0..3 {
1271            for j in 0..3 {
1272                assert_eq!(
1273                    matrix[i].vec[j].coeffs, transposed[j].vec[i].coeffs,
1274                    "A[{i}][{j}] != A^T[{j}][{i}]"
1275                );
1276            }
1277        }
1278    }
1279
1280    #[test]
1281    fn cbd2_produces_values_in_correct_range() {
1282        // CBD with eta=2 should produce coefficients in [-2, 2]
1283        let mut buf = [0u8; 128];
1284        for i in 0..128 {
1285            buf[i] = (i as u8).wrapping_mul(0x37);
1286        }
1287        let poly = cbd2(&buf);
1288        for (i, &coeff) in poly.coeffs.iter().enumerate() {
1289            assert!((-2..=2).contains(&coeff), "CBD2 coeff[{i}] = {coeff} out of range [-2, 2]");
1290        }
1291    }
1292
1293    #[test]
1294    fn rej_uniform_only_accepts_values_less_than_q() {
1295        // Craft input where val0 = Q (3329 = 0xD01) should be rejected
1296        // rej_uniform parses 3 bytes into 2 12-bit values:
1297        // val0 = (buf[0] | buf[1]<<8) & 0x0fff
1298        // val1 = ((buf[1]>>4) | buf[2]<<4) & 0x0fff
1299        let buf = [
1300            0x01, 0x0D,
1301            0x00, // val0 = 0xD01 = 3329 = Q (rejected), val1 = (0x0D>>4 | 0x00<<4) & 0xfff = 0 (accepted)
1302            0x00, 0x0D,
1303            0xD0, // val0 = 0xD00 = 3328 (accepted), val1 = (0x0D>>4 | 0xD0<<4) & 0xfff = 0xD00 = 3328 (accepted)
1304        ];
1305        let mut out = [0i16; 256];
1306        let count = rej_uniform(&mut out, &buf);
1307        // val0=Q rejected, val1=0 accepted, val0=3328 accepted, val1=3328 accepted
1308        assert_eq!(count, 3);
1309        assert_eq!(out[0], 0); // first accepted: val1 from first triple
1310        assert_eq!(out[1], 3328); // second accepted: val0 from second triple
1311        assert_eq!(out[2], 3328); // third accepted: val1 from second triple
1312    }
1313
1314    #[test]
1315    fn nist_acvp_ml_kem_768_full_vector() {
1316        // Verify against NIST FIPS 203 intermediate test vector (ML-KEM-768.txt)
1317        // These values come from the NIST test file and are validated by the CCTV tests
1318        let d: [u8; 32] = decode_hex_array("f688563f7c66a5da2d8bdb5a5f3e07bd8dce6f7efcec7f41298d79863459f7cd");
1319        let z: [u8; 32] = decode_hex_array("d1d49a515250dbceb9f6e3fcc1c7d5306918964b21ddb22207e03e57f0600da8");
1320        let m: [u8; 32] = decode_hex_array("3dc27ca0a6594b0e56320457c45a0f76bb8a213ea4a76d442186a0aefadbcdb9");
1321
1322        let mut coins = [0u8; 64];
1323        coins[..32].copy_from_slice(&d);
1324        coins[32..].copy_from_slice(&z);
1325
1326        let (dk, ek) = crypto_kem_keypair_derand::<3, 2400, 1184>(&ML_KEM_768, &coins);
1327        let (k, ct) = crypto_kem_enc_derand::<3, 1184, 1088>(&ML_KEM_768, &ek, &m);
1328
1329        // Verify public key hash matches NIST vector
1330        assert_eq!(
1331            sha3_256_hex(&ek),
1332            "42d930a50dfd1f0541ca45c4598daebb4f51cd10d711a001bd9bb87d5c87a4bf"
1333        );
1334        // Verify secret key hash
1335        assert_eq!(
1336            sha3_256_hex(&dk),
1337            "db563aebd9fdc875e88563693edad1e5e359cc37b0f685d2d0a3723b37253192"
1338        );
1339        // Verify ciphertext hash
1340        assert_eq!(
1341            sha3_256_hex(&ct),
1342            "9d6e358208c4d583050becb319050b7f916de47caad1d589a1d01fea43fe1750"
1343        );
1344        // Verify shared secret
1345        assert_eq!(
1346            hex::encode(k),
1347            "ae726da2df66601c6648a7565c02b203a089276ac30f6cc226d048f93fafd78c"
1348        );
1349
1350        // Verify decapsulation produces the same shared secret
1351        let k_dec = crypto_kem_dec::<3, 2400, 1088>(&ML_KEM_768, &dk, &ct).unwrap();
1352        assert_eq!(k, k_dec, "decapsulation mismatch against NIST vector");
1353    }
1354
1355    #[test]
1356    fn nist_acvp_ml_kem_1024_full_vector() {
1357        // Verify against NIST FIPS 203 intermediate test vector (ML-KEM-1024.txt)
1358        let d: [u8; 32] = decode_hex_array("2a62c39ef4fc499f2d132716f480bb7521a49558ae84ee80d9352e66daf1e3a8");
1359        let z: [u8; 32] = decode_hex_array("5f574ef7f013d4336801fed022178c3ed91d0b6d51325315fc1dcabf4770a2ea");
1360        let m: [u8; 32] = decode_hex_array("e07d685ed308e609c9c7842026e35732f6ffc6e2fee10f0afd348f2b42a8acb4");
1361
1362        let mut coins = [0u8; 64];
1363        coins[..32].copy_from_slice(&d);
1364        coins[32..].copy_from_slice(&z);
1365
1366        let (dk, ek) = crypto_kem_keypair_derand::<4, 3168, 1568>(&ML_KEM_1024, &coins);
1367        let (k, ct) = crypto_kem_enc_derand::<4, 1568, 1568>(&ML_KEM_1024, &ek, &m);
1368
1369        assert_eq!(
1370            sha3_256_hex(&ek),
1371            "3b308d1344ed70366b84d790acb705b86cd3dfd471fff171969aaa338f26dca5"
1372        );
1373        assert_eq!(
1374            sha3_256_hex(&dk),
1375            "aa63a9e0c035ada6635e7938b71856b24917ff9b3ebca1a4d205a83b502a415a"
1376        );
1377        assert_eq!(
1378            sha3_256_hex(&ct),
1379            "8caba02733421f12a7ba9a2bcbe4de7c9853156a0637df5a7a0f9127c81da943"
1380        );
1381        assert_eq!(
1382            hex::encode(k),
1383            "d53825c3ff666bb2881215dbec04a8bdce9099b2a3680938c2f199b54d505953"
1384        );
1385
1386        let k_dec = crypto_kem_dec::<4, 3168, 1568>(&ML_KEM_1024, &dk, &ct).unwrap();
1387        assert_eq!(k, k_dec, "decapsulation mismatch against NIST vector");
1388    }
1389
1390    #[test]
1391    fn compression_constant_time_no_division() {
1392        // Verify that the compression constants avoid division at runtime.
1393        // This test exercises boundary values where a naive division would
1394        // produce different rounding behavior than the multiplication trick.
1395        let params_768 = &ML_KEM_768;
1396        let params_1024 = &ML_KEM_1024;
1397
1398        // Test boundary values for poly_compress (4-bit)
1399        let mut poly = Poly::default();
1400        poly.coeffs[0] = 0;
1401        poly.coeffs[1] = (Q - 1) as i16;
1402        poly.coeffs[2] = (Q / 2) as i16;
1403        poly.coeffs[3] = (Q / 2 + 1) as i16;
1404        let mut buf4 = [0u8; 128];
1405        poly_compress::<3>(params_768, &mut buf4, &poly);
1406        let dec = poly_decompress::<3>(params_768, &buf4);
1407        // Verify round-trip for boundary values
1408        assert_eq!(dec.coeffs[0], 0); // 0 should compress/decompress to 0
1409
1410        // Test boundary values for poly_compress (5-bit)
1411        let mut buf5 = [0u8; 160];
1412        poly_compress::<4>(params_1024, &mut buf5, &poly);
1413        let dec5 = poly_decompress::<4>(params_1024, &buf5);
1414        assert_eq!(dec5.coeffs[0], 0);
1415    }
1416
1417    #[test]
1418    fn poly_tomsg_boundary_values() {
1419        // Test poly_tomsg at the decision boundary: Q/4 and 3Q/4
1420        let mut poly = Poly::default();
1421        // Value 0 should produce bit 0
1422        poly.coeffs[0] = 0;
1423        // Value Q/2 (1665) should produce bit 1
1424        poly.coeffs[1] = (Q / 2) as i16;
1425        // Value Q/4 (832) is at the boundary
1426        poly.coeffs[2] = (Q / 4) as i16;
1427        // Value 3Q/4 (2497) is at the other boundary
1428        poly.coeffs[3] = (3 * Q as i32 / 4) as i16;
1429
1430        let msg = poly_tomsg(&poly);
1431        // bit 0: value 0 -> 0
1432        assert_eq!(msg[0] & 1, 0);
1433        // bit 1: value Q/2 -> 1
1434        assert_eq!((msg[0] >> 1) & 1, 1);
1435    }
1436}