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