Skip to main content

crypto/mldsa/
mldsa.rs

1//! Shared ML-DSA (FIPS 204) core, parameterized over the ML-DSA-44, ML-DSA-65
2//! and ML-DSA-87 parameter sets.
3//!
4//! This module is an implementation detail of [`crate::mldsa`]; the public API
5//! lives in the per-parameter-set modules (`mldsa44`, `mldsa65`, `mldsa87`).
6
7use constant_time_eq::constant_time_eq;
8#[cfg(feature = "zeroize")]
9use zeroize::{Zeroize, ZeroizeOnDrop};
10
11use crate::{
12    RandomError, Xof,
13    sha3::{Shake128, Shake256},
14};
15
16/// Number of coefficients in a polynomial.
17pub(crate) const N: usize = 256;
18/// Size in bytes of a seed / private key.
19pub(crate) const SEED_SIZE: usize = 32;
20/// Maximum length in bytes of a context string.
21pub(crate) const CONTEXT_MAX_LEN: usize = 255;
22
23const Q: u32 = 8380417;
24const D: u32 = 13;
25const ONE: u32 = 4193792;
26const MINUS_ONE: u32 = 4186625;
27const RR: u32 = 2365951;
28const QINV: u32 = 4236238847;
29const N_INV: u32 = 16382;
30
31const MAX_LAMBDA_OVER_4: usize = 64;
32const MAX_W1_BYTES: usize = 8 * N * 6 / 8;
33const MAX_POLYZ_BYTES: usize = 20 * N / 8;
34/// Maximum number of ML-DSA.Sign_internal rejection-loop iterations.
35///
36/// FIPS 204 Table 3 permits implementations to bound the loop at 814
37/// iterations (and forbids lower bounds). The loop is expected to terminate
38/// after 3.85-5.1 iterations, so reaching this bound has probability below
39/// 2^-256 and signals a fault or broken randomness.
40const MAX_SIGN_ITERATIONS: usize = 814;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MlDsaError {
44    ContextTooLong,
45    InvalidSignature,
46    InvalidPublicKey,
47    InvalidSignatureLength,
48    /// The signing rejection loop exceeded the FIPS 204 Table 3 limit of 814
49    /// iterations. This is statistically impossible in practice and indicates a
50    /// fault or a broken random number generator rather than a valid signature.
51    RetryLimitExceeded,
52    Random(RandomError),
53}
54
55#[cfg(feature = "alloc")]
56impl core::fmt::Display for MlDsaError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            MlDsaError::ContextTooLong => write!(f, "context length exceeds 255 bytes"),
60            MlDsaError::InvalidSignature => write!(f, "signature is not valid"),
61            MlDsaError::InvalidPublicKey => write!(f, "public key is not valid"),
62            MlDsaError::InvalidSignatureLength => write!(f, "signature length is not valid"),
63            MlDsaError::RetryLimitExceeded => write!(f, "signing retry limit exceeded"),
64            MlDsaError::Random(err) => write!(f, "{err}"),
65        }
66    }
67}
68
69impl From<RandomError> for MlDsaError {
70    fn from(err: RandomError) -> Self {
71        MlDsaError::Random(err)
72    }
73}
74
75/// The parameter set constants that differ between ML-DSA-44, ML-DSA-65 and
76/// ML-DSA-87.
77///
78/// The dimensions `k` and `l` are carried by const generics on the functions
79/// that need them; everything else is stored here.
80#[derive(Clone, Copy)]
81pub(crate) struct MlDsaParams {
82    /// Bound for the secret coefficients (2 or 4).
83    pub(crate) eta: u32,
84    /// Number of non-zero coefficients in the challenge polynomial (39/49/60).
85    pub(crate) tau: usize,
86    /// `tau * eta`.
87    pub(crate) beta: u32,
88    /// `1 << gamma1_bits`.
89    pub(crate) gamma1: u32,
90    /// `log2(gamma1)` (17 or 19).
91    pub(crate) gamma1_bits: usize,
92    /// `(q - 1) / gamma2_den`.
93    pub(crate) gamma2: u32,
94    /// Denominator of `gamma2` (32 or 88).
95    pub(crate) gamma2_den: u32,
96    /// Maximum number of hints (80/55/75).
97    pub(crate) omega: usize,
98    /// `lambda / 4` (32/48/64).
99    pub(crate) lambda_over_4: usize,
100    /// Encoded size of one `z` polynomial: `(gamma1_bits + 1) * N / 8`.
101    pub(crate) polyz_bytes: usize,
102    /// Encoded public key size in bytes.
103    pub(crate) public_key_size: usize,
104    /// Signature size in bytes.
105    pub(crate) signature_size: usize,
106}
107
108pub(crate) const PARAMS_44: MlDsaParams = MlDsaParams {
109    eta: 2,
110    tau: 39,
111    beta: 78,
112    gamma1: 1 << 17,
113    gamma1_bits: 17,
114    gamma2: (Q - 1) / 88,
115    gamma2_den: 88,
116    omega: 80,
117    lambda_over_4: 32,
118    polyz_bytes: 18 * N / 8,
119    public_key_size: 1312,
120    signature_size: 2420,
121};
122
123pub(crate) const PARAMS_65: MlDsaParams = MlDsaParams {
124    eta: 4,
125    tau: 49,
126    beta: 196,
127    gamma1: 1 << 19,
128    gamma1_bits: 19,
129    gamma2: (Q - 1) / 32,
130    gamma2_den: 32,
131    omega: 55,
132    lambda_over_4: 48,
133    polyz_bytes: 20 * N / 8,
134    public_key_size: 1952,
135    signature_size: 3309,
136};
137
138pub(crate) const PARAMS_87: MlDsaParams = MlDsaParams {
139    eta: 2,
140    tau: 60,
141    beta: 120,
142    gamma1: 1 << 19,
143    gamma1_bits: 19,
144    gamma2: (Q - 1) / 32,
145    gamma2_den: 32,
146    omega: 75,
147    lambda_over_4: 64,
148    polyz_bytes: 20 * N / 8,
149    public_key_size: 2592,
150    signature_size: 4627,
151};
152
153type FieldElement = u32;
154
155/// Best-effort zeroization of an intermediate secret buffer.
156///
157/// When the `zeroize` feature is enabled the bytes are overwritten with zeros;
158/// otherwise this is a no-op. FIPS 204 requires intermediate values to be
159/// destroyed when signing fails or when the retry limit is exceeded.
160#[inline]
161fn wipe(bytes: &mut [u8]) {
162    #[cfg(feature = "zeroize")]
163    {
164        use zeroize::Zeroize;
165        bytes.zeroize();
166    }
167    #[cfg(not(feature = "zeroize"))]
168    {
169        let _ = bytes;
170    }
171}
172
173fn field_to_montgomery(a: u32) -> FieldElement {
174    debug_assert!(a < Q);
175    field_montgomery_mul(a, RR)
176}
177
178fn field_from_montgomery(a: FieldElement) -> u32 {
179    field_montgomery_reduce(a as u64)
180}
181
182fn field_montgomery_reduce(x: u64) -> u32 {
183    let t = (x as u32).wrapping_mul(QINV);
184    let u = (x + (t as u64) * (Q as u64)) >> 32;
185    field_reduce_once(u as u32)
186}
187
188fn field_montgomery_mul(a: FieldElement, b: FieldElement) -> FieldElement {
189    field_montgomery_reduce(a as u64 * b as u64)
190}
191
192fn field_reduce_once(x: u32) -> FieldElement {
193    let t = x.wrapping_sub(Q);
194    let mask = ((t as i32) >> 31) as u32;
195    t.wrapping_add(Q & mask)
196}
197
198fn field_add(a: FieldElement, b: FieldElement) -> FieldElement {
199    field_reduce_once(a.wrapping_add(b))
200}
201
202fn field_sub(a: FieldElement, b: FieldElement) -> FieldElement {
203    field_reduce_once(a.wrapping_sub(b).wrapping_add(Q))
204}
205
206fn field_sub_to_montgomery(a: u32, b: u32) -> FieldElement {
207    let x = a.wrapping_sub(b).wrapping_add(Q);
208    field_montgomery_mul(x, RR)
209}
210
211fn field_infinity_norm(r: FieldElement) -> u32 {
212    let x = field_from_montgomery(r);
213    let q_minus_x = Q - x;
214    let half_q = Q / 2;
215    let mask = ((half_q.wrapping_sub(x)) as i32 >> 31) as u32;
216    (mask & q_minus_x) | (!mask & x)
217}
218
219fn field_centered_mod(r: FieldElement) -> i32 {
220    let x = field_from_montgomery(r);
221    let x = x as i32;
222    let half_q = (Q / 2) as i32;
223    let mask = ((half_q - x) >> 31) as i32;
224    (mask & (x - Q as i32)) | (!mask & x)
225}
226
227fn power2round(r: FieldElement) -> (u16, FieldElement) {
228    let rr = field_from_montgomery(r);
229    let r1 = (rr + (1 << 12) - 1) >> 13;
230    let r0 = field_sub_to_montgomery(rr, r1 << 13);
231    (r1 as u16, r0)
232}
233
234fn highbits32(x: u32) -> u8 {
235    let r1 = (x + 127) >> 7;
236    let r1 = (r1 * 1025 + (1 << 21)) >> 22;
237    (r1 & 0b1111) as u8
238}
239
240fn highbits88(x: u32) -> u8 {
241    let r1 = (x + 127) >> 7;
242    let r1 = (r1 * 11275 + (1 << 23)) >> 24;
243    // r1 == 44 must map to 0; do it without a data-dependent branch.
244    let d = r1 ^ 44;
245    let not_eq = (d | d.wrapping_neg()) >> 31;
246    (r1 * not_eq) as u8
247}
248
249fn highbits(x: u32, gamma2_den: u32) -> u8 {
250    match gamma2_den {
251        32 => highbits32(x),
252        88 => highbits88(x),
253        _ => unreachable!(),
254    }
255}
256
257fn decompose32(r: FieldElement) -> (u8, i32) {
258    let x = field_from_montgomery(r) as i32;
259    let r1 = highbits32(x as u32);
260    let r0 = x - (r1 as i32) * 2 * (Q as i32 - 1) / 32;
261    let half_q = (Q / 2) as i32;
262    let mask = ((half_q - r0) >> 31) as i32;
263    let r0 = (mask & (r0 - Q as i32)) | (!mask & r0);
264    (r1, r0)
265}
266
267fn decompose88(r: FieldElement) -> (u8, i32) {
268    let x = field_from_montgomery(r) as i32;
269    let r1 = highbits88(x as u32);
270    let r0 = x - (r1 as i32) * 2 * (Q as i32 - 1) / 88;
271    let half_q = (Q / 2) as i32;
272    let mask = ((half_q - r0) >> 31) as i32;
273    let r0 = (mask & (r0 - Q as i32)) | (!mask & r0);
274    (r1, r0)
275}
276
277fn decompose(r: FieldElement, gamma2_den: u32) -> (u8, i32) {
278    match gamma2_den {
279        32 => decompose32(r),
280        88 => decompose88(r),
281        _ => unreachable!(),
282    }
283}
284
285fn make_hint32(ct0: FieldElement, w: FieldElement, cs2: FieldElement) -> u8 {
286    let r_plus_z = field_sub(w, cs2);
287    let v1 = highbits32(field_from_montgomery(r_plus_z));
288    let r = field_add(r_plus_z, ct0);
289    let r1 = highbits32(field_from_montgomery(r));
290    (v1 != r1) as u8
291}
292
293fn make_hint88(ct0: FieldElement, w: FieldElement, cs2: FieldElement) -> u8 {
294    let r_plus_z = field_sub(w, cs2);
295    let v1 = highbits88(field_from_montgomery(r_plus_z));
296    let r = field_add(r_plus_z, ct0);
297    let r1 = highbits88(field_from_montgomery(r));
298    (v1 != r1) as u8
299}
300
301fn make_hint(ct0: FieldElement, w: FieldElement, cs2: FieldElement, gamma2_den: u32) -> u8 {
302    match gamma2_den {
303        32 => make_hint32(ct0, w, cs2),
304        88 => make_hint88(ct0, w, cs2),
305        _ => unreachable!(),
306    }
307}
308
309fn use_hint32(r: FieldElement, hint: u8) -> u8 {
310    let (r1, r0) = decompose32(r);
311    if hint == 0 {
312        return r1;
313    }
314    let r0_gt_0 = !(r0.wrapping_sub(1) >> 31) as u8;
315    let r1_plus = r1.wrapping_add(1) & 0x0F;
316    let r1_minus = r1.wrapping_sub(1) & 0x0F;
317    (r0_gt_0 & r1_plus) | ((!r0_gt_0) & r1_minus)
318}
319
320fn use_hint88(r: FieldElement, hint: u8) -> u8 {
321    const M: u8 = 44;
322    let (mut r1, r0) = decompose88(r);
323    if hint == 0 {
324        return r1;
325    }
326    if r0 > 0 {
327        if r1 == M - 1 {
328            r1 = 0;
329        } else {
330            r1 += 1;
331        }
332    } else if r1 == 0 {
333        r1 = M - 1;
334    } else {
335        r1 -= 1;
336    }
337    r1
338}
339
340fn use_hint(r: FieldElement, hint: u8, gamma2_den: u32) -> u8 {
341    match gamma2_den {
342        32 => use_hint32(r, hint),
343        88 => use_hint88(r, hint),
344        _ => unreachable!(),
345    }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq)]
349#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
350struct Poly {
351    coeffs: [FieldElement; N],
352}
353
354impl Default for Poly {
355    fn default() -> Self {
356        Self {
357            coeffs: [0u32; N],
358        }
359    }
360}
361
362#[derive(Clone, Debug, PartialEq, Eq)]
363#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
364struct NttPoly {
365    coeffs: [FieldElement; N],
366}
367
368impl Default for NttPoly {
369    fn default() -> Self {
370        Self {
371            coeffs: [0u32; N],
372        }
373    }
374}
375
376fn poly_add(a: &Poly, b: &Poly) -> Poly {
377    let mut r = Poly::default();
378    for i in 0..N {
379        r.coeffs[i] = field_add(a.coeffs[i], b.coeffs[i]);
380    }
381    r
382}
383
384fn poly_sub(a: &Poly, b: &Poly) -> Poly {
385    let mut r = Poly::default();
386    for i in 0..N {
387        r.coeffs[i] = field_sub(a.coeffs[i], b.coeffs[i]);
388    }
389    r
390}
391
392fn ntt_add(a: &NttPoly, b: &NttPoly) -> NttPoly {
393    let mut r = NttPoly::default();
394    for i in 0..N {
395        r.coeffs[i] = field_add(a.coeffs[i], b.coeffs[i]);
396    }
397    r
398}
399
400fn ntt_sub(a: &NttPoly, b: &NttPoly) -> NttPoly {
401    let mut r = NttPoly::default();
402    for i in 0..N {
403        r.coeffs[i] = field_sub(a.coeffs[i], b.coeffs[i]);
404    }
405    r
406}
407
408fn ntt_mul(a: &NttPoly, b: &NttPoly) -> NttPoly {
409    #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
410    {
411        neon::ntt_mul(a, b)
412    }
413    #[cfg(all(target_arch = "x86_64", feature = "std"))]
414    {
415        if is_x86_feature_detected!("avx2") {
416            // SAFETY: AVX2 availability was just checked.
417            unsafe { avx2::ntt_mul(a, b) }
418        } else {
419            ntt_mul_scalar(a, b)
420        }
421    }
422    #[cfg(not(any(
423        all(target_arch = "aarch64", target_feature = "neon"),
424        all(target_arch = "x86_64", feature = "std"),
425    )))]
426    {
427        ntt_mul_scalar(a, b)
428    }
429}
430
431#[cfg(any(test, not(all(target_arch = "aarch64", target_feature = "neon"))))]
432fn ntt_mul_scalar(a: &NttPoly, b: &NttPoly) -> NttPoly {
433    let mut r = NttPoly::default();
434    for i in 0..N {
435        r.coeffs[i] = field_montgomery_mul(a.coeffs[i], b.coeffs[i]);
436    }
437    r
438}
439
440const ZETAS: [FieldElement; 256] = [
441    4193792, 25847, 5771523, 7861508, 237124, 7602457, 7504169, 466468, 1826347, 2353451, 8021166, 6288512, 3119733,
442    5495562, 3111497, 2680103, 2725464, 1024112, 7300517, 3585928, 7830929, 7260833, 2619752, 6271868, 6262231,
443    4520680, 6980856, 5102745, 1757237, 8360995, 4010497, 280005, 2706023, 95776, 3077325, 3530437, 6718724, 4788269,
444    5842901, 3915439, 4519302, 5336701, 3574422, 5512770, 3539968, 8079950, 2348700, 7841118, 6681150, 6736599,
445    3505694, 4558682, 3507263, 6239768, 6779997, 3699596, 811944, 531354, 954230, 3881043, 3900724, 5823537, 2071892,
446    5582638, 4450022, 6851714, 4702672, 5339162, 6927966, 3475950, 2176455, 6795196, 7122806, 1939314, 4296819,
447    7380215, 5190273, 5223087, 4747489, 126922, 3412210, 7396998, 2147896, 2715295, 5412772, 4686924, 7969390, 5903370,
448    7709315, 7151892, 8357436, 7072248, 7998430, 1349076, 1852771, 6949987, 5037034, 264944, 508951, 3097992, 44288,
449    7280319, 904516, 3958618, 4656075, 8371839, 1653064, 5130689, 2389356, 8169440, 759969, 7063561, 189548, 4827145,
450    3159746, 6529015, 5971092, 8202977, 1315589, 1341330, 1285669, 6795489, 7567685, 6940675, 5361315, 4499357,
451    4751448, 3839961, 2091667, 3407706, 2316500, 3817976, 5037939, 2244091, 5933984, 4817955, 266997, 2434439, 7144689,
452    3513181, 4860065, 4621053, 7183191, 5187039, 900702, 1859098, 909542, 819034, 495491, 6767243, 8337157, 7857917,
453    7725090, 5257975, 2031748, 3207046, 4823422, 7855319, 7611795, 4784579, 342297, 286988, 5942594, 4108315, 3437287,
454    5038140, 1735879, 203044, 2842341, 2691481, 5790267, 1265009, 4055324, 1247620, 2486353, 1595974, 4613401, 1250494,
455    2635921, 4832145, 5386378, 1869119, 1903435, 7329447, 7047359, 1237275, 5062207, 6950192, 7929317, 1312455,
456    3306115, 6417775, 7100756, 1917081, 5834105, 7005614, 1500165, 777191, 2235880, 3406031, 7838005, 5548557, 6709241,
457    6533464, 5796124, 4656147, 594136, 4603424, 6366809, 2432395, 2454455, 8215696, 1957272, 3369112, 185531, 7173032,
458    5196991, 162844, 1616392, 3014001, 810149, 1652634, 4686184, 6581310, 5341501, 3523897, 3866901, 269760, 2213111,
459    7404533, 1717735, 472078, 7953734, 1723600, 6577327, 1910376, 6712985, 7276084, 8119771, 4546524, 5441381, 6144432,
460    7959518, 6094090, 183443, 7403526, 1612842, 4834730, 7826001, 3919660, 8332111, 7018208, 3937738, 1400424, 7534263,
461    1976782,
462];
463
464fn ntt(f: &Poly) -> NttPoly {
465    #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
466    {
467        neon::ntt(f)
468    }
469    #[cfg(all(target_arch = "x86_64", feature = "std"))]
470    {
471        if is_x86_feature_detected!("avx2") {
472            // SAFETY: AVX2 availability was just checked.
473            unsafe { avx2::ntt(f) }
474        } else {
475            ntt_scalar(f)
476        }
477    }
478    #[cfg(not(any(
479        all(target_arch = "aarch64", target_feature = "neon"),
480        all(target_arch = "x86_64", feature = "std"),
481    )))]
482    {
483        ntt_scalar(f)
484    }
485}
486
487#[cfg(any(test, not(all(target_arch = "aarch64", target_feature = "neon"))))]
488fn ntt_scalar(f: &Poly) -> NttPoly {
489    let mut f = NttPoly {
490        coeffs: f.coeffs,
491    };
492    let mut m: usize = 0;
493
494    let mut len: usize = 128;
495    while len >= 8 {
496        let mut start: usize = 0;
497        while start < N {
498            m += 1;
499            let zeta = ZETAS[m];
500            let mid = start + len;
501            for j in (start..mid).step_by(2) {
502                let t = field_montgomery_mul(zeta, f.coeffs[j + len]);
503                f.coeffs[j + len] = field_sub(f.coeffs[j], t);
504                f.coeffs[j] = field_add(f.coeffs[j], t);
505                let t = field_montgomery_mul(zeta, f.coeffs[j + len + 1]);
506                f.coeffs[j + len + 1] = field_sub(f.coeffs[j + 1], t);
507                f.coeffs[j + 1] = field_add(f.coeffs[j + 1], t);
508            }
509            start += 2 * len;
510        }
511        len /= 2;
512    }
513
514    let mut start: usize = 0;
515    while start < N {
516        m += 1;
517        let zeta = ZETAS[m];
518        let t = field_montgomery_mul(zeta, f.coeffs[start + 4]);
519        f.coeffs[start + 4] = field_sub(f.coeffs[start], t);
520        f.coeffs[start] = field_add(f.coeffs[start], t);
521        let t = field_montgomery_mul(zeta, f.coeffs[start + 5]);
522        f.coeffs[start + 5] = field_sub(f.coeffs[start + 1], t);
523        f.coeffs[start + 1] = field_add(f.coeffs[start + 1], t);
524        let t = field_montgomery_mul(zeta, f.coeffs[start + 6]);
525        f.coeffs[start + 6] = field_sub(f.coeffs[start + 2], t);
526        f.coeffs[start + 2] = field_add(f.coeffs[start + 2], t);
527        let t = field_montgomery_mul(zeta, f.coeffs[start + 7]);
528        f.coeffs[start + 7] = field_sub(f.coeffs[start + 3], t);
529        f.coeffs[start + 3] = field_add(f.coeffs[start + 3], t);
530        start += 8;
531    }
532
533    start = 0;
534    while start < N {
535        m += 1;
536        let zeta = ZETAS[m];
537        let t = field_montgomery_mul(zeta, f.coeffs[start + 2]);
538        f.coeffs[start + 2] = field_sub(f.coeffs[start], t);
539        f.coeffs[start] = field_add(f.coeffs[start], t);
540        let t = field_montgomery_mul(zeta, f.coeffs[start + 3]);
541        f.coeffs[start + 3] = field_sub(f.coeffs[start + 1], t);
542        f.coeffs[start + 1] = field_add(f.coeffs[start + 1], t);
543        start += 4;
544    }
545
546    start = 0;
547    while start < N {
548        m += 1;
549        let zeta = ZETAS[m];
550        let t = field_montgomery_mul(zeta, f.coeffs[start + 1]);
551        f.coeffs[start + 1] = field_sub(f.coeffs[start], t);
552        f.coeffs[start] = field_add(f.coeffs[start], t);
553        start += 2;
554    }
555
556    f
557}
558
559fn invntt(f: &NttPoly) -> Poly {
560    #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
561    {
562        neon::invntt(f)
563    }
564    #[cfg(all(target_arch = "x86_64", feature = "std"))]
565    {
566        if is_x86_feature_detected!("avx2") {
567            // SAFETY: AVX2 availability was just checked.
568            unsafe { avx2::invntt(f) }
569        } else {
570            invntt_scalar(f)
571        }
572    }
573    #[cfg(not(any(
574        all(target_arch = "aarch64", target_feature = "neon"),
575        all(target_arch = "x86_64", feature = "std"),
576    )))]
577    {
578        invntt_scalar(f)
579    }
580}
581
582#[cfg(any(test, not(all(target_arch = "aarch64", target_feature = "neon"))))]
583fn invntt_scalar(f: &NttPoly) -> Poly {
584    let mut f = NttPoly {
585        coeffs: f.coeffs,
586    };
587    let mut m: usize = 255;
588
589    let mut start: usize = 0;
590    while start < N {
591        let zeta = ZETAS[m];
592        m -= 1;
593        let t = f.coeffs[start];
594        f.coeffs[start] = field_add(t, f.coeffs[start + 1]);
595        f.coeffs[start + 1] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 1], t));
596        start += 2;
597    }
598
599    start = 0;
600    while start < N {
601        let zeta = ZETAS[m];
602        m -= 1;
603        let t = f.coeffs[start];
604        f.coeffs[start] = field_add(t, f.coeffs[start + 2]);
605        f.coeffs[start + 2] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 2], t));
606        let t = f.coeffs[start + 1];
607        f.coeffs[start + 1] = field_add(t, f.coeffs[start + 3]);
608        f.coeffs[start + 3] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 3], t));
609        start += 4;
610    }
611
612    start = 0;
613    while start < N {
614        let zeta = ZETAS[m];
615        m -= 1;
616        let t = f.coeffs[start];
617        f.coeffs[start] = field_add(t, f.coeffs[start + 4]);
618        f.coeffs[start + 4] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 4], t));
619        let t = f.coeffs[start + 1];
620        f.coeffs[start + 1] = field_add(t, f.coeffs[start + 5]);
621        f.coeffs[start + 5] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 5], t));
622        let t = f.coeffs[start + 2];
623        f.coeffs[start + 2] = field_add(t, f.coeffs[start + 6]);
624        f.coeffs[start + 6] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 6], t));
625        let t = f.coeffs[start + 3];
626        f.coeffs[start + 3] = field_add(t, f.coeffs[start + 7]);
627        f.coeffs[start + 7] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 7], t));
628        start += 8;
629    }
630
631    let mut len: usize = 8;
632    while len < N {
633        let mut start: usize = 0;
634        while start < N {
635            let zeta = ZETAS[m];
636            m -= 1;
637            let mid = start + len;
638            for j in (start..mid).step_by(2) {
639                let t = f.coeffs[j];
640                f.coeffs[j] = field_add(t, f.coeffs[j + len]);
641                let diff = field_sub(f.coeffs[j + len], t);
642                f.coeffs[j + len] = field_montgomery_mul(zeta, diff);
643                let t = f.coeffs[j + 1];
644                f.coeffs[j + 1] = field_add(t, f.coeffs[j + len + 1]);
645                let diff = field_sub(f.coeffs[j + len + 1], t);
646                f.coeffs[j + len + 1] = field_montgomery_mul(zeta, diff);
647            }
648            start += 2 * len;
649        }
650        len *= 2;
651    }
652
653    let mut r = Poly::default();
654    for i in 0..N {
655        r.coeffs[i] = field_montgomery_mul(f.coeffs[i], N_INV);
656    }
657    r
658}
659
660fn sample_ntt(rho: &[u8; 32], s: u8, r: u8) -> NttPoly {
661    let mut shake = Shake128::new();
662    shake.absorb(rho);
663    shake.absorb(&[s, r]);
664
665    let mut a = NttPoly::default();
666    let mut j: usize = 0;
667    let mut buf = [0u8; 168];
668    let mut off: usize = 168;
669
670    loop {
671        if off >= 168 {
672            shake.squeeze(&mut buf);
673            off = 0;
674        }
675        let v = (buf[off] as u32) | ((buf[off + 1] as u32) << 8) | ((buf[off + 2] as u32) << 16);
676        off += 3;
677        let v = v & 0x7FFFFF;
678        if v < Q {
679            a.coeffs[j] = field_to_montgomery(v);
680            j += 1;
681            if j >= N {
682                break;
683            }
684        }
685    }
686    a
687}
688
689fn coeff_from_half_byte(b: u8, eta: u32) -> Option<FieldElement> {
690    match eta {
691        2 => {
692            if b > 14 {
693                None
694            } else {
695                Some(field_sub_to_montgomery(2, (b % 5) as u32))
696            }
697        }
698        4 => {
699            if b > 8 {
700                None
701            } else {
702                Some(field_sub_to_montgomery(4, b as u32))
703            }
704        }
705        _ => unreachable!(),
706    }
707}
708
709fn sample_bounded_poly(rho: &[u8], r: u8, eta: u32) -> Poly {
710    let mut shake = Shake256::new();
711    shake.absorb(rho);
712    shake.absorb(&[r, 0]);
713
714    let mut a = Poly::default();
715    let mut j: usize = 0;
716    let mut buf = [0u8; 136];
717    let mut off: usize = 136;
718
719    loop {
720        if off >= 136 {
721            shake.squeeze(&mut buf);
722            off = 0;
723        }
724        let z0 = buf[off] & 0x0F;
725        let z1 = buf[off] >> 4;
726        off += 1;
727
728        if let Some(c) = coeff_from_half_byte(z0, eta) {
729            a.coeffs[j] = c;
730            j += 1;
731            if j >= N {
732                break;
733            }
734        }
735        if let Some(c) = coeff_from_half_byte(z1, eta) {
736            a.coeffs[j] = c;
737            j += 1;
738            if j >= N {
739                break;
740            }
741        }
742    }
743    a
744}
745
746fn sample_in_ball(rho: &[u8], tau: usize) -> Poly {
747    let mut shake = Shake256::new();
748    shake.absorb(rho);
749    let mut s = [0u8; 8];
750    shake.squeeze(&mut s);
751
752    let mut c = Poly::default();
753    let mut signs: u64 = u64::from_le_bytes(s);
754
755    for i in (N - tau)..N {
756        let mut jb = [0u8; 1];
757        loop {
758            shake.squeeze(&mut jb);
759            if jb[0] as usize <= i {
760                break;
761            }
762        }
763        let j = jb[0] as usize;
764        c.coeffs[i] = c.coeffs[j];
765        if (signs & 1) == 0 {
766            c.coeffs[j] = ONE;
767        } else {
768            c.coeffs[j] = MINUS_ONE;
769        }
770        signs >>= 1;
771    }
772
773    c
774}
775
776fn expand_mask(nonce: &[u8; 64], kappa: usize, params: &MlDsaParams) -> Poly {
777    let mut shake = Shake256::new();
778    shake.absorb(nonce);
779    shake.absorb(&(kappa as u16).to_le_bytes());
780
781    let mut buf = [0u8; MAX_POLYZ_BYTES];
782    shake.squeeze(&mut buf[..params.polyz_bytes]);
783    bitunpack(&buf[..params.polyz_bytes], params.gamma1_bits)
784}
785
786fn highbits_poly(w: &Poly, params: &MlDsaParams) -> [u8; N] {
787    let mut r = [0u8; N];
788    for i in 0..N {
789        r[i] = highbits(field_from_montgomery(w.coeffs[i]), params.gamma2_den);
790    }
791    r
792}
793
794fn make_hint_poly(ct0: &Poly, w: &Poly, cs2: &Poly, params: &MlDsaParams) -> ([u8; N], usize) {
795    let mut h = [0u8; N];
796    let mut count = 0usize;
797    for i in 0..N {
798        h[i] = make_hint(ct0.coeffs[i], w.coeffs[i], cs2.coeffs[i], params.gamma2_den);
799        count += h[i] as usize;
800    }
801    (h, count)
802}
803
804fn use_hint_poly(r: &Poly, h: &[u8; N], params: &MlDsaParams) -> [u8; N] {
805    let mut w = [0u8; N];
806    for i in 0..N {
807        w[i] = use_hint(r.coeffs[i], h[i], params.gamma2_den);
808    }
809    w
810}
811
812/// Returns `true` if any coefficient of `w` has centered infinity norm `>= bound`.
813///
814/// All `N` coefficients are always inspected: the result is accumulated with a
815/// branchless OR rather than returning at the first failing index, so the number
816/// of iterations does not depend on the (potentially secret) coefficient values.
817fn coefficients_exceed_bound(w: &Poly, bound: u32) -> bool {
818    let mut exceeded = 0u32;
819    for &c in w.coeffs.iter() {
820        exceeded |= (field_infinity_norm(c) >= bound) as u32;
821    }
822    exceeded != 0
823}
824
825/// Returns `true` if any low-order part `r0` of `w` (per [`decompose`]) satisfies
826/// `|r0| >= bound`.
827///
828/// All `N` coefficients are always inspected: the result is accumulated with a
829/// branchless OR rather than returning at the first failing index, so the number
830/// of iterations does not depend on the (potentially secret) coefficient values.
831fn lowbits_exceed_bound(w: &Poly, bound: u32, gamma2_den: u32) -> bool {
832    let mut exceeded = 0u32;
833    for &c in w.coeffs.iter() {
834        let (_, r0) = decompose(c, gamma2_den);
835        let abs_r0 = (r0 ^ (r0 >> 31)).wrapping_sub(r0 >> 31) as u32;
836        exceeded |= (abs_r0 >= bound) as u32;
837    }
838    exceeded != 0
839}
840
841fn pk_encode<const K: usize>(rho: &[u8; 32], t1: &[[u16; N]; K], out: &mut [u8]) {
842    debug_assert_eq!(out.len(), 32 + K * N * 10 / 8);
843    out[..32].copy_from_slice(rho);
844    let mut pos = 32;
845
846    for w in t1.iter() {
847        for i in (0..N).step_by(4) {
848            let c0 = w[i] as u32;
849            let c1 = w[i + 1] as u32;
850            let c2 = w[i + 2] as u32;
851            let c3 = w[i + 3] as u32;
852            out[pos] = (c0 & 0xFF) as u8;
853            out[pos + 1] = ((c0 >> 8) | (c1 << 2)) as u8;
854            out[pos + 2] = ((c1 >> 6) | (c2 << 4)) as u8;
855            out[pos + 3] = ((c2 >> 4) | (c3 << 6)) as u8;
856            out[pos + 4] = (c3 >> 2) as u8;
857            pos += 5;
858        }
859    }
860}
861
862fn pk_decode<const K: usize>(params: &MlDsaParams, pk: &[u8]) -> Result<([u8; 32], [[u16; N]; K]), MlDsaError> {
863    if pk.len() != params.public_key_size {
864        return Err(MlDsaError::InvalidPublicKey);
865    }
866    let mut rho = [0u8; 32];
867    rho.copy_from_slice(&pk[..32]);
868    let mut t1 = [[0u16; N]; K];
869    let mut pos = 32;
870
871    for r in 0..K {
872        for i in (0..N).step_by(4) {
873            let b0 = pk[pos] as u16;
874            let b1 = pk[pos + 1] as u16;
875            let b2 = pk[pos + 2] as u16;
876            let b3 = pk[pos + 3] as u16;
877            let b4 = pk[pos + 4] as u16;
878            t1[r][i] = b0 | ((b1 & 0b0000_0011) << 8);
879            t1[r][i + 1] = (b1 >> 2) | ((b2 & 0b0000_1111) << 6);
880            t1[r][i + 2] = (b2 >> 4) | ((b3 & 0b0011_1111) << 4);
881            t1[r][i + 3] = (b3 >> 6) | ((b4 & 0b1111_1111) << 2);
882            pos += 5;
883        }
884    }
885    Ok((rho, t1))
886}
887
888fn bitpack_18(z: &Poly, out: &mut [u8]) {
889    const B: u32 = 1 << 17;
890    let mut q = 0usize;
891
892    for i in (0..N).step_by(4) {
893        let w0 = (B as i32 - field_centered_mod(z.coeffs[i])) as u32;
894        out[q] = w0 as u8;
895        out[q + 1] = (w0 >> 8) as u8;
896        out[q + 2] = (w0 >> 16) as u8;
897        let w1 = (B as i32 - field_centered_mod(z.coeffs[i + 1])) as u32;
898        out[q + 2] |= (w1 << 2) as u8;
899        out[q + 3] = (w1 >> 6) as u8;
900        out[q + 4] = (w1 >> 14) as u8;
901        let w2 = (B as i32 - field_centered_mod(z.coeffs[i + 2])) as u32;
902        out[q + 4] |= (w2 << 4) as u8;
903        out[q + 5] = (w2 >> 4) as u8;
904        out[q + 6] = (w2 >> 12) as u8;
905        let w3 = (B as i32 - field_centered_mod(z.coeffs[i + 3])) as u32;
906        out[q + 6] |= (w3 << 6) as u8;
907        out[q + 7] = (w3 >> 2) as u8;
908        out[q + 8] = (w3 >> 10) as u8;
909        q += 9;
910    }
911}
912
913fn bitpack_20(z: &Poly, out: &mut [u8]) {
914    let b = 1u32 << 19;
915    let mut q = 0usize;
916
917    for i in (0..N).step_by(2) {
918        let w0 = (b as i32 - field_centered_mod(z.coeffs[i])) as u32;
919        out[q] = w0 as u8;
920        out[q + 1] = (w0 >> 8) as u8;
921        out[q + 2] = (w0 >> 16) as u8;
922        let w1 = (b as i32 - field_centered_mod(z.coeffs[i + 1])) as u32;
923        out[q + 2] |= ((w1 & 0x0F) << 4) as u8;
924        out[q + 3] = (w1 >> 4) as u8;
925        out[q + 4] = (w1 >> 12) as u8;
926        q += 5;
927    }
928}
929
930fn bitpack(z: &Poly, gamma1_bits: usize, out: &mut [u8]) {
931    match gamma1_bits {
932        17 => bitpack_18(z, out),
933        19 => bitpack_20(z, out),
934        _ => unreachable!(),
935    }
936}
937
938fn bitunpack_18(v: &[u8]) -> Poly {
939    const B: u32 = 1 << 17;
940    const MASK18: u32 = (1 << 18) - 1;
941    let mut r = Poly::default();
942    let mut p = v;
943
944    for i in (0..N).step_by(4) {
945        let w0 = (p[0] as u32) | ((p[1] as u32) << 8) | ((p[2] as u32) << 16);
946        r.coeffs[i] = field_sub_to_montgomery(B, w0 & MASK18);
947        let w1 = ((p[2] as u32) >> 2) | ((p[3] as u32) << 6) | ((p[4] as u32) << 14);
948        r.coeffs[i + 1] = field_sub_to_montgomery(B, w1 & MASK18);
949        let w2 = ((p[4] as u32) >> 4) | ((p[5] as u32) << 4) | ((p[6] as u32) << 12);
950        r.coeffs[i + 2] = field_sub_to_montgomery(B, w2 & MASK18);
951        let w3 = ((p[6] as u32) >> 6) | ((p[7] as u32) << 2) | ((p[8] as u32) << 10);
952        r.coeffs[i + 3] = field_sub_to_montgomery(B, w3 & MASK18);
953        p = &p[9..];
954    }
955    r
956}
957
958fn bitunpack_20(v: &[u8]) -> Poly {
959    let b = 1u32 << 19;
960    let mask20 = (1u32 << 20) - 1;
961    let mut r = Poly::default();
962    let mut p = v;
963
964    for i in (0..N).step_by(2) {
965        let w0 = (p[0] as u32) | ((p[1] as u32) << 8) | ((p[2] as u32) << 16);
966        r.coeffs[i] = field_sub_to_montgomery(b, w0 & mask20);
967        let w1 = ((p[2] as u32) >> 4) | ((p[3] as u32) << 4) | ((p[4] as u32) << 12);
968        r.coeffs[i + 1] = field_sub_to_montgomery(b, w1 & mask20);
969        p = &p[5..];
970    }
971    r
972}
973
974fn bitunpack(v: &[u8], gamma1_bits: usize) -> Poly {
975    match gamma1_bits {
976        17 => bitunpack_18(v),
977        19 => bitunpack_20(v),
978        _ => unreachable!(),
979    }
980}
981
982fn hint_encode<const K: usize>(params: &MlDsaParams, h: &[[u8; N]; K], out: &mut [u8]) {
983    let omega = params.omega;
984    debug_assert_eq!(out.len(), omega + K);
985    out.fill(0);
986    let mut idx: usize = 0;
987
988    for i in 0..K {
989        for j in 0..N {
990            if h[i][j] != 0 {
991                out[idx] = j as u8;
992                idx += 1;
993            }
994        }
995        out[omega + i] = idx as u8;
996    }
997}
998
999fn hint_decode<const K: usize>(params: &MlDsaParams, sig: &[u8]) -> Result<[[u8; N]; K], MlDsaError> {
1000    let omega = params.omega;
1001    debug_assert_eq!(sig.len(), omega + K);
1002    let mut h = [[0u8; N]; K];
1003    let mut idx: usize = 0;
1004
1005    for i in 0..K {
1006        let limit = sig[omega + i] as usize;
1007        if limit < idx || limit > omega {
1008            return Err(MlDsaError::InvalidSignature);
1009        }
1010        // Track polynomial start so the ordering check doesn't fire across polynomial boundaries.
1011        let poly_start = idx;
1012        while idx < limit {
1013            let j = sig[idx] as usize;
1014            // FIPS 204 §6.2 Algorithm 24: indices within a polynomial must be strictly increasing.
1015            if idx > poly_start && sig[idx - 1] as usize >= j {
1016                return Err(MlDsaError::InvalidSignature);
1017            }
1018            if j >= N {
1019                return Err(MlDsaError::InvalidSignature);
1020            }
1021            h[i][j] = 1;
1022            idx += 1;
1023        }
1024    }
1025    for k in idx..omega {
1026        if sig[k] != 0 {
1027            return Err(MlDsaError::InvalidSignature);
1028        }
1029    }
1030    Ok(h)
1031}
1032
1033fn sig_encode<const K: usize, const L: usize>(
1034    params: &MlDsaParams,
1035    ch: &[u8],
1036    z: &[Poly; L],
1037    h: &[[u8; N]; K],
1038    out: &mut [u8],
1039) {
1040    debug_assert_eq!(out.len(), params.signature_size);
1041    let lo4 = params.lambda_over_4;
1042    out[..lo4].copy_from_slice(ch);
1043
1044    let mut pos = lo4;
1045    for poly in z.iter() {
1046        let pz = params.polyz_bytes;
1047        bitpack(poly, params.gamma1_bits, &mut out[pos..pos + pz]);
1048        pos += pz;
1049    }
1050
1051    hint_encode::<K>(params, h, &mut out[pos..]);
1052}
1053
1054fn sig_decode<const K: usize, const L: usize>(
1055    params: &MlDsaParams,
1056    sig: &[u8],
1057) -> Result<([u8; MAX_LAMBDA_OVER_4], [Poly; L], [[u8; N]; K]), MlDsaError> {
1058    if sig.len() != params.signature_size {
1059        return Err(MlDsaError::InvalidSignatureLength);
1060    }
1061    let lo4 = params.lambda_over_4;
1062    let mut ch = [0u8; MAX_LAMBDA_OVER_4];
1063    ch[..lo4].copy_from_slice(&sig[..lo4]);
1064
1065    let mut z: [Poly; L] = core::array::from_fn(|_| Poly::default());
1066    let mut pos = lo4;
1067    for poly in z.iter_mut() {
1068        let pz = params.polyz_bytes;
1069        *poly = bitunpack(&sig[pos..pos + pz], params.gamma1_bits);
1070        pos += pz;
1071    }
1072
1073    let h = hint_decode::<K>(params, &sig[pos..])?;
1074
1075    Ok((ch, z, h))
1076}
1077
1078fn w1_encode<const K: usize>(params: &MlDsaParams, w1: &[[u8; N]; K]) -> ([u8; MAX_W1_BYTES], usize) {
1079    let mut buf = [0u8; MAX_W1_BYTES];
1080    let mut pos = 0usize;
1081
1082    match params.gamma2_den {
1083        32 => {
1084            // Coefficients are <= 15, four bits each.
1085            for w in w1.iter() {
1086                for i in (0..N).step_by(2) {
1087                    buf[pos] = w[i] | (w[i + 1] << 4);
1088                    pos += 1;
1089                }
1090            }
1091        }
1092        88 => {
1093            // Coefficients are <= 43, six bits each.
1094            for w in w1.iter() {
1095                for i in (0..N).step_by(4) {
1096                    let (b0, b1, b2, b3) = (w[i], w[i + 1], w[i + 2], w[i + 3]);
1097                    buf[pos] = b0 | (b1 << 6);
1098                    buf[pos + 1] = (b1 >> 2) | (b2 << 4);
1099                    buf[pos + 2] = (b2 >> 4) | (b3 << 2);
1100                    pos += 3;
1101                }
1102            }
1103        }
1104        _ => unreachable!(),
1105    }
1106
1107    (buf, pos)
1108}
1109
1110fn compute_matrix_a<const K: usize, const L: usize>(rho: &[u8; 32]) -> [[NttPoly; L]; K] {
1111    core::array::from_fn(|r| core::array::from_fn(|s| sample_ntt(rho, s as u8, r as u8)))
1112}
1113
1114fn compute_matrix_a_into<const K: usize, const L: usize>(a: &mut [[NttPoly; L]; K], rho: &[u8; 32]) {
1115    for r in 0..K {
1116        for s in 0..L {
1117            a[r][s] = sample_ntt(rho, s as u8, r as u8);
1118        }
1119    }
1120}
1121
1122pub(crate) fn compute_pubkey_hash(pk: &[u8]) -> [u8; 64] {
1123    let mut shake = Shake256::new();
1124    shake.absorb(pk);
1125    let mut tr = [0u8; 64];
1126    shake.squeeze(&mut tr);
1127    tr
1128}
1129
1130fn compute_message_hash(tr: &[u8; 64], message: &[u8], ctx: &[u8]) -> Result<[u8; 64], MlDsaError> {
1131    if ctx.len() > CONTEXT_MAX_LEN {
1132        return Err(MlDsaError::ContextTooLong);
1133    }
1134    let mut shake = Shake256::new();
1135    shake.absorb(tr);
1136    shake.absorb(&[0u8]);
1137    shake.absorb(&[ctx.len() as u8]);
1138    shake.absorb(ctx);
1139    shake.absorb(message);
1140    let mut mu = [0u8; 64];
1141    shake.squeeze(&mut mu);
1142    Ok(mu)
1143}
1144
1145fn compute_t1_hat<const K: usize>(t1: &[[u16; N]; K]) -> [NttPoly; K] {
1146    core::array::from_fn(|i| {
1147        let mut w = Poly::default();
1148        for j in 0..N {
1149            w.coeffs[j] = field_to_montgomery((t1[i][j] as u32) << D);
1150        }
1151        ntt(&w)
1152    })
1153}
1154
1155/// Expanded key material shared by all ML-DSA parameter sets.
1156///
1157/// This is the only way to sign: key generation runs once when the key is
1158/// initialized and the resulting matrix `A` and secret vectors in the NTT
1159/// domain are cached, so signing does not repeat the expensive key generation.
1160///
1161/// The value is a plain fixed-size type that never allocates. It is large
1162/// (tens of kilobytes, up to about 80 KiB for ML-DSA-87), so constructing it
1163/// uses a correspondingly large amount of stack; callers on memory-constrained
1164/// targets should keep it in long-lived storage rather than creating it in a
1165/// deeply nested call.
1166#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
1167pub(crate) struct MlDsaKeyMaterial<const K: usize, const L: usize, const PK_SIZE: usize> {
1168    seed: [u8; SEED_SIZE],
1169    key_bytes: [u8; 32],
1170    pk: [u8; PK_SIZE],
1171    tr: [u8; 64],
1172    a: [[NttPoly; L]; K],
1173    s1_hat: [NttPoly; L],
1174    s2_hat: [NttPoly; K],
1175    t0_hat: [NttPoly; K],
1176}
1177
1178impl<const K: usize, const L: usize, const PK_SIZE: usize> MlDsaKeyMaterial<K, L, PK_SIZE> {
1179    /// Creates a zeroed, uninitialized key material value.
1180    ///
1181    /// SAFETY: all fields are plain arrays of integers and polynomials whose
1182    /// all-zero bit pattern is a valid value, so `zeroed` is sound here. The
1183    /// key must be initialized before signing.
1184    pub(crate) const fn new() -> Self {
1185        // SAFETY: see above.
1186        unsafe { core::mem::zeroed() }
1187    }
1188
1189    /// Expands `seed` into a fresh signing key.
1190    pub(crate) fn from_seed(params: &MlDsaParams, seed: &[u8; SEED_SIZE]) -> Self {
1191        let mut this = Self::new();
1192        this.init(params, seed);
1193        this
1194    }
1195
1196    /// Generates a fresh random signing key.
1197    #[cfg(feature = "random")]
1198    pub(crate) fn from_random(params: &MlDsaParams) -> Result<Self, MlDsaError> {
1199        let mut this = Self::new();
1200        this.generate(params)?;
1201        Ok(this)
1202    }
1203
1204    /// Expands `seed` into a signing key, overwriting any previous state.
1205    pub(crate) fn init(&mut self, params: &MlDsaParams, seed: &[u8; SEED_SIZE]) {
1206        debug_assert_eq!(PK_SIZE, params.public_key_size);
1207        let mut shake = Shake256::new();
1208        shake.absorb(seed);
1209        shake.absorb(&[K as u8, L as u8]);
1210        let mut rho = [0u8; 32];
1211        let mut rhos = [0u8; 64];
1212        shake.squeeze(&mut rho);
1213        shake.squeeze(&mut rhos);
1214        shake.squeeze(&mut self.key_bytes);
1215
1216        compute_matrix_a_into::<K, L>(&mut self.a, &rho);
1217
1218        for r in 0..L {
1219            let s1 = sample_bounded_poly(&rhos, r as u8, params.eta);
1220            self.s1_hat[r] = ntt(&s1);
1221        }
1222        for r in 0..K {
1223            let s2 = sample_bounded_poly(&rhos, (L + r) as u8, params.eta);
1224            self.s2_hat[r] = ntt(&s2);
1225        }
1226
1227        let mut t1 = [[0u16; N]; K];
1228        for i in 0..K {
1229            let mut t_hat = self.s2_hat[i].clone();
1230            for j in 0..L {
1231                t_hat = ntt_add(&t_hat, &ntt_mul(&self.a[i][j], &self.s1_hat[j]));
1232            }
1233            let t = invntt(&t_hat);
1234            let mut t0 = Poly::default();
1235            for j in 0..N {
1236                (t1[i][j], t0.coeffs[j]) = power2round(t.coeffs[j]);
1237            }
1238            self.t0_hat[i] = ntt(&t0);
1239        }
1240
1241        pk_encode::<K>(&rho, &t1, &mut self.pk);
1242        self.tr = compute_pubkey_hash(&self.pk);
1243        self.seed = *seed;
1244    }
1245
1246    #[cfg(feature = "random")]
1247    pub(crate) fn generate(&mut self, params: &MlDsaParams) -> Result<[u8; SEED_SIZE], MlDsaError> {
1248        let seed: [u8; SEED_SIZE] = crate::random::bytes()?;
1249        self.init(params, &seed);
1250        Ok(seed)
1251    }
1252
1253    pub(crate) fn public_key(&self) -> &[u8; PK_SIZE] {
1254        &self.pk
1255    }
1256
1257    pub(crate) fn seed(&self) -> &[u8; SEED_SIZE] {
1258        &self.seed
1259    }
1260
1261    pub(crate) fn sign_derand_into(
1262        &self,
1263        params: &MlDsaParams,
1264        message: &[u8],
1265        ctx: &[u8],
1266        rnd: &[u8; 32],
1267        sig_out: &mut [u8],
1268    ) -> Result<(), MlDsaError> {
1269        let mut mu = compute_message_hash(&self.tr, message, ctx)?;
1270        let result = self.sign_internal(params, &mu, rnd, sig_out);
1271        // FIPS 204 Section 3.6.2: destroy the message representative.
1272        wipe(&mut mu);
1273        result
1274    }
1275
1276    pub(crate) fn sign_external_mu_derand_into(
1277        &self,
1278        params: &MlDsaParams,
1279        mu: &[u8; 64],
1280        rnd: &[u8; 32],
1281        sig_out: &mut [u8],
1282    ) -> Result<(), MlDsaError> {
1283        self.sign_internal(params, mu, rnd, sig_out)
1284    }
1285
1286    fn sign_internal(
1287        &self,
1288        params: &MlDsaParams,
1289        mu: &[u8; 64],
1290        rnd: &[u8; 32],
1291        sig_out: &mut [u8],
1292    ) -> Result<(), MlDsaError> {
1293        debug_assert_eq!(sig_out.len(), params.signature_size);
1294        let a = &self.a;
1295        let s1_hat = &self.s1_hat;
1296        let s2_hat = &self.s2_hat;
1297        let t0_hat = &self.t0_hat;
1298
1299        let gamma1beta = params.gamma1 - params.beta;
1300        let gamma2 = params.gamma2;
1301        let gamma2beta = gamma2 - params.beta;
1302        let lo4 = params.lambda_over_4;
1303
1304        let mut h_shake = Shake256::new();
1305        h_shake.absorb(&self.key_bytes);
1306        h_shake.absorb(rnd);
1307        h_shake.absorb(mu);
1308        let mut nonce = [0u8; 64];
1309        h_shake.squeeze(&mut nonce);
1310
1311        let mut kappa: usize = 0;
1312        let mut iterations: usize = 0;
1313
1314        loop {
1315            if iterations >= MAX_SIGN_ITERATIONS {
1316                // FIPS 204 Table 3: destroy intermediate results and fail.
1317                wipe(&mut nonce);
1318                return Err(MlDsaError::RetryLimitExceeded);
1319            }
1320            iterations += 1;
1321
1322            let mut y: [Poly; L] = core::array::from_fn(|_| Poly::default());
1323            for item in y.iter_mut() {
1324                *item = expand_mask(&nonce, kappa, params);
1325                kappa += 1;
1326            }
1327
1328            let mut y_hat: [NttPoly; L] = core::array::from_fn(|_| NttPoly::default());
1329            for i in 0..L {
1330                y_hat[i] = ntt(&y[i]);
1331            }
1332
1333            let mut w: [Poly; K] = core::array::from_fn(|_| Poly::default());
1334            for i in 0..K {
1335                let mut w_hat = NttPoly::default();
1336                for j in 0..L {
1337                    w_hat = ntt_add(&w_hat, &ntt_mul(&a[i][j], &y_hat[j]));
1338                }
1339                w[i] = invntt(&w_hat);
1340            }
1341
1342            let mut w1 = [[0u8; N]; K];
1343            for i in 0..K {
1344                w1[i] = highbits_poly(&w[i], params);
1345            }
1346
1347            let mut ch_shake = Shake256::new();
1348            ch_shake.absorb(mu);
1349            let (w1_bytes, w1_len) = w1_encode::<K>(params, &w1);
1350            ch_shake.absorb(&w1_bytes[..w1_len]);
1351            let mut ct = [0u8; MAX_LAMBDA_OVER_4];
1352            ch_shake.squeeze(&mut ct[..lo4]);
1353
1354            let c = sample_in_ball(&ct[..lo4], params.tau);
1355            let c_hat = ntt(&c);
1356
1357            let mut cs1: [Poly; L] = core::array::from_fn(|_| Poly::default());
1358            for i in 0..L {
1359                cs1[i] = invntt(&ntt_mul(&c_hat, &s1_hat[i]));
1360            }
1361            let mut cs2: [Poly; K] = core::array::from_fn(|_| Poly::default());
1362            for i in 0..K {
1363                cs2[i] = invntt(&ntt_mul(&c_hat, &s2_hat[i]));
1364            }
1365
1366            // Each phase inspects every polynomial (no early `break`) so the
1367            // number of iterations does not reveal which index failed first.
1368            let mut z: [Poly; L] = core::array::from_fn(|_| Poly::default());
1369            let mut reject = false;
1370            for i in 0..L {
1371                z[i] = poly_add(&y[i], &cs1[i]);
1372                reject |= coefficients_exceed_bound(&z[i], gamma1beta);
1373            }
1374            if reject {
1375                continue;
1376            }
1377
1378            for i in 0..K {
1379                let r0 = poly_sub(&w[i], &cs2[i]);
1380                reject |= lowbits_exceed_bound(&r0, gamma2beta, params.gamma2_den);
1381            }
1382            if reject {
1383                continue;
1384            }
1385
1386            let mut ct0: [Poly; K] = core::array::from_fn(|_| Poly::default());
1387            for i in 0..K {
1388                ct0[i] = invntt(&ntt_mul(&c_hat, &t0_hat[i]));
1389                reject |= coefficients_exceed_bound(&ct0[i], gamma2);
1390            }
1391            if reject {
1392                continue;
1393            }
1394
1395            let mut total_hints: usize = 0;
1396            let mut h = [[0u8; N]; K];
1397            for i in 0..K {
1398                let (hi, count) = make_hint_poly(&ct0[i], &w[i], &cs2[i], params);
1399                h[i] = hi;
1400                total_hints += count;
1401            }
1402            if total_hints > params.omega {
1403                continue;
1404            }
1405
1406            sig_encode::<K, L>(params, &ct[..lo4], &z, &h, sig_out);
1407            // FIPS 204 Section 3.6.2: destroy the secret nonce seed.
1408            wipe(&mut nonce);
1409            return Ok(());
1410        }
1411    }
1412}
1413
1414fn verify_internal<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1415    params: &MlDsaParams,
1416    pk: &[u8; PK_SIZE],
1417    mu: &[u8; 64],
1418    sig: &[u8; SIG_SIZE],
1419) -> Result<(), MlDsaError> {
1420    let (rho, t1) = pk_decode::<K>(params, pk)?;
1421    let (ch, z, h) = sig_decode::<K, L>(params, sig)?;
1422
1423    let gamma1beta = params.gamma1 - params.beta;
1424
1425    // FIPS 204 §6.2 Algorithm 3 step 5: check ||z||∞ < γ1 − β before the
1426    // expensive matrix-vector product.
1427    for item in z.iter() {
1428        if coefficients_exceed_bound(item, gamma1beta) {
1429            return Err(MlDsaError::InvalidSignature);
1430        }
1431    }
1432
1433    let a = compute_matrix_a::<K, L>(&rho);
1434    let t1_hat = compute_t1_hat::<K>(&t1);
1435
1436    let c = sample_in_ball(&ch[..params.lambda_over_4], params.tau);
1437    let c_hat = ntt(&c);
1438
1439    let mut z_hat: [NttPoly; L] = core::array::from_fn(|_| NttPoly::default());
1440    for i in 0..L {
1441        z_hat[i] = ntt(&z[i]);
1442    }
1443
1444    let mut w_approx: [Poly; K] = core::array::from_fn(|_| Poly::default());
1445    for i in 0..K {
1446        let mut w_hat = NttPoly::default();
1447        for j in 0..L {
1448            w_hat = ntt_add(&w_hat, &ntt_mul(&a[i][j], &z_hat[j]));
1449        }
1450        w_hat = ntt_sub(&w_hat, &ntt_mul(&c_hat, &t1_hat[i]));
1451        w_approx[i] = invntt(&w_hat);
1452    }
1453
1454    let mut w1 = [[0u8; N]; K];
1455    for i in 0..K {
1456        w1[i] = use_hint_poly(&w_approx[i], &h[i], params);
1457    }
1458
1459    let mut ch_shake = Shake256::new();
1460    ch_shake.absorb(mu);
1461    let (w1_bytes, w1_len) = w1_encode::<K>(params, &w1);
1462    ch_shake.absorb(&w1_bytes[..w1_len]);
1463    let mut computed_ch = [0u8; MAX_LAMBDA_OVER_4];
1464    ch_shake.squeeze(&mut computed_ch[..params.lambda_over_4]);
1465
1466    if !constant_time_eq(&ch[..params.lambda_over_4], &computed_ch[..params.lambda_over_4]) {
1467        return Err(MlDsaError::InvalidSignature);
1468    }
1469
1470    Ok(())
1471}
1472
1473/// Verifies a signature over `message` with the optional context `ctx`.
1474pub(crate) fn verify_message<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1475    params: &MlDsaParams,
1476    pk: &[u8; PK_SIZE],
1477    message: &[u8],
1478    sig: &[u8; SIG_SIZE],
1479    ctx: &[u8],
1480) -> Result<(), MlDsaError> {
1481    let tr = compute_pubkey_hash(pk);
1482    let mu = compute_message_hash(&tr, message, ctx)?;
1483    verify_internal::<K, L, PK_SIZE, SIG_SIZE>(params, pk, &mu, sig)
1484}
1485
1486/// Verifies a signature over a precomputed 64-byte message representative `mu`
1487/// (FIPS 204 "external mu" verification).
1488pub(crate) fn verify_external_mu<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1489    params: &MlDsaParams,
1490    pk: &[u8; PK_SIZE],
1491    mu: &[u8; 64],
1492    sig: &[u8; SIG_SIZE],
1493) -> Result<(), MlDsaError> {
1494    verify_internal::<K, L, PK_SIZE, SIG_SIZE>(params, pk, mu, sig)
1495}
1496
1497/// AArch64 NEON implementations of the ML-DSA NTT kernels.
1498///
1499/// Unlike ML-KEM, LLVM does not auto-vectorize the 32-bit field arithmetic
1500/// here, so the vector kernels provide a real speed-up. NEON is part of the
1501/// aarch64 baseline, so selection is at compile time. Correctness is checked
1502/// against the scalar kernels by the `*_matches_scalar` tests and end-to-end by
1503/// the FIPS 204 known-answer tests.
1504#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
1505mod neon {
1506    use core::arch::aarch64::*;
1507
1508    use super::*;
1509
1510    /// `field_reduce_once` on four lanes.
1511    #[inline]
1512    #[target_feature(enable = "neon")]
1513    fn reduce_once4(x: uint32x4_t) -> uint32x4_t {
1514        let q = vdupq_n_u32(Q);
1515        let sub = vsubq_u32(x, q);
1516        let mask = vshrq_n_s32(vreinterpretq_s32_u32(sub), 31);
1517        let masked = vandq_u32(vreinterpretq_u32_s32(mask), q);
1518        vaddq_u32(sub, masked)
1519    }
1520
1521    #[inline]
1522    #[target_feature(enable = "neon")]
1523    fn add4(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t {
1524        reduce_once4(vaddq_u32(a, b))
1525    }
1526
1527    #[inline]
1528    #[target_feature(enable = "neon")]
1529    fn sub4(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t {
1530        reduce_once4(vaddq_u32(vsubq_u32(a, b), vdupq_n_u32(Q)))
1531    }
1532
1533    /// `field_reduce_once` on two lanes.
1534    #[inline]
1535    #[target_feature(enable = "neon")]
1536    fn reduce_once2(x: uint32x2_t) -> uint32x2_t {
1537        let q = vdup_n_u32(Q);
1538        let sub = vsub_u32(x, q);
1539        let mask = vshr_n_s32(vreinterpret_s32_u32(sub), 31);
1540        let masked = vand_u32(vreinterpret_u32_s32(mask), q);
1541        vadd_u32(sub, masked)
1542    }
1543
1544    /// `field_montgomery_reduce` on two 64-bit products, narrowed to 32 bits.
1545    #[inline]
1546    #[target_feature(enable = "neon")]
1547    fn montgomery_reduce2(x: uint64x2_t) -> uint32x2_t {
1548        let q = vdup_n_u32(Q);
1549        let t = vmul_u32(vmovn_u64(x), vdup_n_u32(QINV));
1550        let tq = vmull_u32(t, q);
1551        let u = vshrq_n_u64(vaddq_u64(x, tq), 32);
1552        reduce_once2(vmovn_u64(u))
1553    }
1554
1555    /// `field_montgomery_mul` on four lanes.
1556    #[inline]
1557    #[target_feature(enable = "neon")]
1558    fn montgomery_mul4(a: uint32x4_t, b: uint32x4_t) -> uint32x4_t {
1559        let lo = montgomery_reduce2(vmull_u32(vget_low_u32(a), vget_low_u32(b)));
1560        let hi = montgomery_reduce2(vmull_high_u32(a, b));
1561        vcombine_u32(lo, hi)
1562    }
1563
1564    #[inline]
1565    #[target_feature(enable = "neon")]
1566    fn montgomery_mul4_scalar(zeta: u32, y: uint32x4_t) -> uint32x4_t {
1567        montgomery_mul4(vdupq_n_u32(zeta), y)
1568    }
1569
1570    pub(super) fn ntt(f: &Poly) -> NttPoly {
1571        let mut f = NttPoly {
1572            coeffs: f.coeffs,
1573        };
1574        // SAFETY: NEON is enabled on aarch64. Each vector load/store accesses
1575        // `j` or `j + len` for `j < start + len` with `start + 2*len <= N` and
1576        // `len >= 8` a multiple of 4, so `j + len + 4 <= N`.
1577        let mut m: usize = 0;
1578        unsafe {
1579            let ptr = f.coeffs.as_mut_ptr();
1580
1581            let mut len: usize = 128;
1582            while len >= 8 {
1583                let mut start: usize = 0;
1584                while start < N {
1585                    m += 1;
1586                    let zeta = ZETAS[m];
1587                    let mut j = start;
1588                    while j < start + len {
1589                        let x = vld1q_u32(ptr.add(j));
1590                        let y = vld1q_u32(ptr.add(j + len));
1591                        let t = montgomery_mul4_scalar(zeta, y);
1592                        vst1q_u32(ptr.add(j), add4(x, t));
1593                        vst1q_u32(ptr.add(j + len), sub4(x, t));
1594                        j += 4;
1595                    }
1596                    start += 2 * len;
1597                }
1598                len /= 2;
1599            }
1600        }
1601
1602        // Remaining distance 4, 2 and 1 stages, identical to the scalar kernel.
1603        let c = &mut f.coeffs;
1604        let mut start: usize = 0;
1605        while start < N {
1606            m += 1;
1607            let zeta = ZETAS[m];
1608            for k in 0..4 {
1609                let t = field_montgomery_mul(zeta, c[start + 4 + k]);
1610                c[start + 4 + k] = field_sub(c[start + k], t);
1611                c[start + k] = field_add(c[start + k], t);
1612            }
1613            start += 8;
1614        }
1615        start = 0;
1616        while start < N {
1617            m += 1;
1618            let zeta = ZETAS[m];
1619            for k in 0..2 {
1620                let t = field_montgomery_mul(zeta, c[start + 2 + k]);
1621                c[start + 2 + k] = field_sub(c[start + k], t);
1622                c[start + k] = field_add(c[start + k], t);
1623            }
1624            start += 4;
1625        }
1626        start = 0;
1627        while start < N {
1628            m += 1;
1629            let zeta = ZETAS[m];
1630            let t = field_montgomery_mul(zeta, c[start + 1]);
1631            c[start + 1] = field_sub(c[start], t);
1632            c[start] = field_add(c[start], t);
1633            start += 2;
1634        }
1635
1636        f
1637    }
1638
1639    pub(super) fn invntt(f: &NttPoly) -> Poly {
1640        let mut f = NttPoly {
1641            coeffs: f.coeffs,
1642        };
1643        let c = &mut f.coeffs;
1644        let mut m: usize = 255;
1645
1646        // Distance 1, 2 and 4 stages, identical to the scalar kernel.
1647        let mut start: usize = 0;
1648        while start < N {
1649            let zeta = ZETAS[m];
1650            m -= 1;
1651            let t = c[start];
1652            c[start] = field_add(t, c[start + 1]);
1653            c[start + 1] = field_montgomery_mul(zeta, field_sub(c[start + 1], t));
1654            start += 2;
1655        }
1656        start = 0;
1657        while start < N {
1658            let zeta = ZETAS[m];
1659            m -= 1;
1660            for k in 0..2 {
1661                let t = c[start + k];
1662                c[start + k] = field_add(t, c[start + 2 + k]);
1663                c[start + 2 + k] = field_montgomery_mul(zeta, field_sub(c[start + 2 + k], t));
1664            }
1665            start += 4;
1666        }
1667        start = 0;
1668        while start < N {
1669            let zeta = ZETAS[m];
1670            m -= 1;
1671            for k in 0..4 {
1672                let t = c[start + k];
1673                c[start + k] = field_add(t, c[start + 4 + k]);
1674                c[start + 4 + k] = field_montgomery_mul(zeta, field_sub(c[start + 4 + k], t));
1675            }
1676            start += 8;
1677        }
1678
1679        // SAFETY: as in `ntt`; here `len` runs 8, 16, ..., 128.
1680        unsafe {
1681            let ptr = f.coeffs.as_mut_ptr();
1682            let mut len: usize = 8;
1683            while len < N {
1684                let mut start: usize = 0;
1685                while start < N {
1686                    let zeta = ZETAS[m];
1687                    m -= 1;
1688                    let mut j = start;
1689                    while j < start + len {
1690                        let x = vld1q_u32(ptr.add(j));
1691                        let y = vld1q_u32(ptr.add(j + len));
1692                        vst1q_u32(ptr.add(j), add4(x, y));
1693                        vst1q_u32(ptr.add(j + len), montgomery_mul4_scalar(zeta, sub4(y, x)));
1694                        j += 4;
1695                    }
1696                    start += 2 * len;
1697                }
1698                len *= 2;
1699            }
1700
1701            // Final scaling by N^-1.
1702            let ninv = vdupq_n_u32(N_INV);
1703            let mut i = 0;
1704            while i < N {
1705                vst1q_u32(ptr.add(i), montgomery_mul4(vld1q_u32(ptr.add(i)), ninv));
1706                i += 4;
1707            }
1708        }
1709
1710        Poly {
1711            coeffs: f.coeffs,
1712        }
1713    }
1714
1715    pub(super) fn ntt_mul(a: &NttPoly, b: &NttPoly) -> NttPoly {
1716        let mut r = NttPoly::default();
1717        let (ap, bp, rp) = (a.coeffs.as_ptr(), b.coeffs.as_ptr(), r.coeffs.as_mut_ptr());
1718        // SAFETY: all four lanes stay within the 256-element arrays.
1719        unsafe {
1720            let mut i = 0;
1721            while i < N {
1722                vst1q_u32(rp.add(i), montgomery_mul4(vld1q_u32(ap.add(i)), vld1q_u32(bp.add(i))));
1723                i += 4;
1724            }
1725        }
1726        r
1727    }
1728}
1729
1730/// AVX2 implementations of the ML-DSA NTT kernels (x86-64, `std`).
1731///
1732/// Selected at runtime with `is_x86_feature_detected!("avx2")`. Correctness is
1733/// checked against the scalar kernels by the `*_matches_scalar` tests (run
1734/// under emulation if no AVX2 host is available) and end-to-end by the FIPS 204
1735/// known-answer tests.
1736#[cfg(all(target_arch = "x86_64", feature = "std"))]
1737mod avx2 {
1738    use core::arch::x86_64::*;
1739
1740    use super::*;
1741
1742    /// `field_reduce_once` on eight lanes.
1743    #[inline]
1744    #[target_feature(enable = "avx2")]
1745    fn reduce_once8(x: __m256i) -> __m256i {
1746        let q = _mm256_set1_epi32(Q as i32);
1747        let sub = _mm256_sub_epi32(x, q);
1748        let mask = _mm256_cmpgt_epi32(q, x); // all-ones where x < q
1749        _mm256_blendv_epi8(sub, x, mask)
1750    }
1751
1752    #[inline]
1753    #[target_feature(enable = "avx2")]
1754    fn add8(a: __m256i, b: __m256i) -> __m256i {
1755        reduce_once8(_mm256_add_epi32(a, b))
1756    }
1757
1758    #[inline]
1759    #[target_feature(enable = "avx2")]
1760    fn sub8(a: __m256i, b: __m256i) -> __m256i {
1761        reduce_once8(_mm256_add_epi32(_mm256_sub_epi32(a, b), _mm256_set1_epi32(Q as i32)))
1762    }
1763
1764    /// Montgomery reduction of four 64-bit products (one per 64-bit lane),
1765    /// yielding the reduced 32-bit value in the low half of each lane.
1766    #[inline]
1767    #[target_feature(enable = "avx2")]
1768    fn montgomery_reduce_4x64(x: __m256i) -> __m256i {
1769        let t = _mm256_mul_epu32(x, _mm256_set1_epi32(QINV as i32));
1770        let tq = _mm256_mul_epu32(t, _mm256_set1_epi32(Q as i32));
1771        let u = _mm256_srli_epi64(_mm256_add_epi64(x, tq), 32);
1772        reduce_once8(u)
1773    }
1774
1775    /// `field_montgomery_mul` on eight lanes.
1776    #[inline]
1777    #[target_feature(enable = "avx2")]
1778    fn montgomery_mul8(a: __m256i, b: __m256i) -> __m256i {
1779        let even = montgomery_reduce_4x64(_mm256_mul_epu32(a, b));
1780        let odd = montgomery_reduce_4x64(_mm256_mul_epu32(_mm256_srli_epi64(a, 32), _mm256_srli_epi64(b, 32)));
1781        _mm256_or_si256(even, _mm256_slli_epi64(odd, 32))
1782    }
1783
1784    #[inline]
1785    #[target_feature(enable = "avx2")]
1786    fn montgomery_mul8_scalar(zeta: u32, y: __m256i) -> __m256i {
1787        montgomery_mul8(_mm256_set1_epi32(zeta as i32), y)
1788    }
1789
1790    #[target_feature(enable = "avx2")]
1791    pub(super) unsafe fn ntt(f: &Poly) -> NttPoly {
1792        let mut f = NttPoly {
1793            coeffs: f.coeffs,
1794        };
1795        // SAFETY: AVX2 is enabled by the caller's runtime check. Every vector
1796        // load/store stays within `N` because `len >= 8` is a multiple of 4 and
1797        // `start + 2*len <= N`.
1798        unsafe {
1799            let ptr = f.coeffs.as_mut_ptr();
1800            let mut m: usize = 0;
1801
1802            let mut len: usize = 128;
1803            while len >= 8 {
1804                let mut start: usize = 0;
1805                while start < N {
1806                    m += 1;
1807                    let zeta = ZETAS[m];
1808                    let mut j = start;
1809                    while j < start + len {
1810                        let x = _mm256_loadu_si256(ptr.add(j).cast());
1811                        let y = _mm256_loadu_si256(ptr.add(j + len).cast());
1812                        let t = montgomery_mul8_scalar(zeta, y);
1813                        _mm256_storeu_si256(ptr.add(j).cast(), add8(x, t));
1814                        _mm256_storeu_si256(ptr.add(j + len).cast(), sub8(x, t));
1815                        j += 8;
1816                    }
1817                    start += 2 * len;
1818                }
1819                len /= 2;
1820            }
1821
1822            // Remaining distance 4, 2 and 1 stages.
1823            let c = &mut f.coeffs;
1824            let mut start: usize = 0;
1825            while start < N {
1826                m += 1;
1827                let zeta = ZETAS[m];
1828                for k in 0..4 {
1829                    let t = field_montgomery_mul(zeta, c[start + 4 + k]);
1830                    c[start + 4 + k] = field_sub(c[start + k], t);
1831                    c[start + k] = field_add(c[start + k], t);
1832                }
1833                start += 8;
1834            }
1835            start = 0;
1836            while start < N {
1837                m += 1;
1838                let zeta = ZETAS[m];
1839                for k in 0..2 {
1840                    let t = field_montgomery_mul(zeta, c[start + 2 + k]);
1841                    c[start + 2 + k] = field_sub(c[start + k], t);
1842                    c[start + k] = field_add(c[start + k], t);
1843                }
1844                start += 4;
1845            }
1846            start = 0;
1847            while start < N {
1848                m += 1;
1849                let zeta = ZETAS[m];
1850                let t = field_montgomery_mul(zeta, c[start + 1]);
1851                c[start + 1] = field_sub(c[start], t);
1852                c[start] = field_add(c[start], t);
1853                start += 2;
1854            }
1855        }
1856
1857        f
1858    }
1859
1860    #[target_feature(enable = "avx2")]
1861    pub(super) unsafe fn invntt(f: &NttPoly) -> Poly {
1862        let mut f = NttPoly {
1863            coeffs: f.coeffs,
1864        };
1865        let c = &mut f.coeffs;
1866        let mut m: usize = 255;
1867
1868        // Distance 1, 2 and 4 stages.
1869        let mut start: usize = 0;
1870        while start < N {
1871            let zeta = ZETAS[m];
1872            m -= 1;
1873            let t = c[start];
1874            c[start] = field_add(t, c[start + 1]);
1875            c[start + 1] = field_montgomery_mul(zeta, field_sub(c[start + 1], t));
1876            start += 2;
1877        }
1878        start = 0;
1879        while start < N {
1880            let zeta = ZETAS[m];
1881            m -= 1;
1882            for k in 0..2 {
1883                let t = c[start + k];
1884                c[start + k] = field_add(t, c[start + 2 + k]);
1885                c[start + 2 + k] = field_montgomery_mul(zeta, field_sub(c[start + 2 + k], t));
1886            }
1887            start += 4;
1888        }
1889        start = 0;
1890        while start < N {
1891            let zeta = ZETAS[m];
1892            m -= 1;
1893            for k in 0..4 {
1894                let t = c[start + k];
1895                c[start + k] = field_add(t, c[start + 4 + k]);
1896                c[start + 4 + k] = field_montgomery_mul(zeta, field_sub(c[start + 4 + k], t));
1897            }
1898            start += 8;
1899        }
1900
1901        // SAFETY: as in `ntt`; `len` runs 8, 16, ..., 128.
1902        unsafe {
1903            let ptr = f.coeffs.as_mut_ptr();
1904            let mut len: usize = 8;
1905            while len < N {
1906                let mut start: usize = 0;
1907                while start < N {
1908                    let zeta = ZETAS[m];
1909                    m -= 1;
1910                    let mut j = start;
1911                    while j < start + len {
1912                        let x = _mm256_loadu_si256(ptr.add(j).cast());
1913                        let y = _mm256_loadu_si256(ptr.add(j + len).cast());
1914                        _mm256_storeu_si256(ptr.add(j).cast(), add8(x, y));
1915                        _mm256_storeu_si256(ptr.add(j + len).cast(), montgomery_mul8_scalar(zeta, sub8(y, x)));
1916                        j += 8;
1917                    }
1918                    start += 2 * len;
1919                }
1920                len *= 2;
1921            }
1922
1923            let ninv = _mm256_set1_epi32(N_INV as i32);
1924            let mut i = 0;
1925            while i < N {
1926                _mm256_storeu_si256(ptr.add(i).cast(), montgomery_mul8(_mm256_loadu_si256(ptr.add(i).cast()), ninv));
1927                i += 8;
1928            }
1929        }
1930
1931        Poly {
1932            coeffs: f.coeffs,
1933        }
1934    }
1935
1936    #[target_feature(enable = "avx2")]
1937    pub(super) unsafe fn ntt_mul(a: &NttPoly, b: &NttPoly) -> NttPoly {
1938        let mut r = NttPoly::default();
1939        // SAFETY: all eight lanes stay within the 256-element arrays.
1940        unsafe {
1941            let (ap, bp, rp) = (a.coeffs.as_ptr(), b.coeffs.as_ptr(), r.coeffs.as_mut_ptr());
1942            let mut i = 0;
1943            while i < N {
1944                _mm256_storeu_si256(
1945                    rp.add(i).cast(),
1946                    montgomery_mul8(_mm256_loadu_si256(ap.add(i).cast()), _mm256_loadu_si256(bp.add(i).cast())),
1947                );
1948                i += 8;
1949            }
1950        }
1951        r
1952    }
1953}
1954
1955#[cfg(test)]
1956mod tests {
1957    use super::*;
1958
1959    #[test]
1960    fn ntt_round_trip() {
1961        let mut shake = Shake128::new();
1962        for _ in 0..100 {
1963            let mut poly = Poly::default();
1964            for j in 0..N {
1965                let mut b = [0u8; 4];
1966                shake.squeeze(&mut b);
1967                let x = u32::from_le_bytes(b) % Q;
1968                poly.coeffs[j] = field_to_montgomery(x);
1969            }
1970            let fwd = ntt(&poly);
1971            let back = invntt(&fwd);
1972            for j in 0..N {
1973                assert_eq!(poly.coeffs[j], back.coeffs[j], "NTT round-trip failed at coeff {}", j);
1974            }
1975        }
1976    }
1977
1978    #[test]
1979    #[cfg(not(debug_assertions))]
1980    fn power2round_consistency() {
1981        for x in 0u32..Q {
1982            let mr = field_to_montgomery(x);
1983            let (r1, r0) = power2round(mr);
1984            let recovered = (r1 as u32) << D;
1985
1986            let expected_r0 = if x >= recovered {
1987                x - recovered
1988            } else {
1989                x.wrapping_sub(recovered)
1990            };
1991
1992            assert!(
1993                expected_r0 < (1 << D) || expected_r0 >= Q - (1 << D) + 1,
1994                "power2round: r0 out of range at x={}, r1={}, r0_expected={}",
1995                x,
1996                r1,
1997                expected_r0
1998            );
1999
2000            let got_r0 = field_from_montgomery(r0);
2001            assert!(
2002                got_r0 == expected_r0 || got_r0 == expected_r0.wrapping_add(Q) || got_r0 == expected_r0.wrapping_sub(Q),
2003                "power2round: r0 mismatch at x={}, r1={}, expected_r0={}, got_r0={}",
2004                x,
2005                r1,
2006                expected_r0,
2007                got_r0
2008            );
2009        }
2010    }
2011
2012    #[test]
2013    #[cfg(not(debug_assertions))]
2014    fn highbits32_exhaustive() {
2015        for x in 0u32..Q {
2016            let h = highbits32(x);
2017            assert!(h < 16, "highbits32: h={} out of range at x={}", h, x);
2018            let (r1, _) = decompose32(field_to_montgomery(x));
2019            assert_eq!(h, r1, "highbits32 vs decompose32 r1 mismatch at x={}", x);
2020        }
2021    }
2022
2023    #[test]
2024    #[cfg(not(debug_assertions))]
2025    fn highbits88_exhaustive() {
2026        for x in 0u32..Q {
2027            let h = highbits88(x);
2028            assert!(h < 44, "highbits88: h={} out of range at x={}", h, x);
2029            let (r1, _) = decompose88(field_to_montgomery(x));
2030            assert_eq!(h, r1, "highbits88 vs decompose88 r1 mismatch at x={}", x);
2031        }
2032    }
2033
2034    #[test]
2035    fn make_hint32_correctness() {
2036        let mut shake = Shake128::new();
2037        for _ in 0..5000 {
2038            let mut b = [0u8; 12];
2039            shake.squeeze(&mut b);
2040            let ct0_val = u32::from_le_bytes(b[0..4].try_into().unwrap()) % Q;
2041            let w_val = u32::from_le_bytes(b[4..8].try_into().unwrap()) % Q;
2042            let cs2_val = u32::from_le_bytes(b[8..12].try_into().unwrap()) % Q;
2043            let ct0 = field_to_montgomery(ct0_val);
2044            let w = field_to_montgomery(w_val);
2045            let cs2 = field_to_montgomery(cs2_val);
2046            let h = make_hint32(ct0, w, cs2);
2047            assert!(h == 0 || h == 1, "make_hint32: hint not 0 or 1");
2048        }
2049    }
2050
2051    #[test]
2052    fn make_hint88_correctness() {
2053        let mut shake = Shake128::new();
2054        for _ in 0..5000 {
2055            let mut b = [0u8; 12];
2056            shake.squeeze(&mut b);
2057            let ct0_val = u32::from_le_bytes(b[0..4].try_into().unwrap()) % Q;
2058            let w_val = u32::from_le_bytes(b[4..8].try_into().unwrap()) % Q;
2059            let cs2_val = u32::from_le_bytes(b[8..12].try_into().unwrap()) % Q;
2060            let ct0 = field_to_montgomery(ct0_val);
2061            let w = field_to_montgomery(w_val);
2062            let cs2 = field_to_montgomery(cs2_val);
2063            let h = make_hint88(ct0, w, cs2);
2064            assert!(h == 0 || h == 1, "make_hint88: hint not 0 or 1");
2065        }
2066    }
2067
2068    #[test]
2069    fn pk_encode_decode_round_trip_65() {
2070        let mut t1 = [[0u16; N]; 6];
2071        let mut shake = Shake128::new();
2072        for row in t1.iter_mut() {
2073            for c in row.iter_mut() {
2074                let mut b = [0u8; 2];
2075                shake.squeeze(&mut b);
2076                *c = u16::from_le_bytes(b) & 0x3FF;
2077            }
2078        }
2079        let rho = [7u8; 32];
2080        let mut pk = [0u8; PARAMS_65.public_key_size];
2081        pk_encode::<6>(&rho, &t1, &mut pk);
2082        let (rho2, t1_2) = pk_decode::<6>(&PARAMS_65, &pk).unwrap();
2083        assert_eq!(rho, rho2);
2084        assert_eq!(t1, t1_2);
2085    }
2086
2087    #[test]
2088    fn bitpack_unpack_round_trip_18() {
2089        let mut shake = Shake128::new();
2090        let mut z = Poly::default();
2091        let b = 1i32 << 17;
2092        for c in z.coeffs.iter_mut() {
2093            let mut buf = [0u8; 4];
2094            shake.squeeze(&mut buf);
2095            let v = (u32::from_le_bytes(buf) % (2 * b as u32)) as i32 - (b - 1);
2096            *c = field_to_montgomery(v.rem_euclid(Q as i32) as u32);
2097        }
2098        let mut out = [0u8; 18 * N / 8];
2099        bitpack_18(&z, &mut out);
2100        let back = bitunpack_18(&out);
2101        assert_eq!(z, back);
2102    }
2103
2104    #[test]
2105    fn bitpack_unpack_round_trip_20() {
2106        let mut shake = Shake128::new();
2107        let mut z = Poly::default();
2108        let b = 1i32 << 19;
2109        for c in z.coeffs.iter_mut() {
2110            let mut buf = [0u8; 4];
2111            shake.squeeze(&mut buf);
2112            let v = (u32::from_le_bytes(buf) % (2 * b as u32)) as i32 - (b - 1);
2113            *c = field_to_montgomery(v.rem_euclid(Q as i32) as u32);
2114        }
2115        let mut out = [0u8; 20 * N / 8];
2116        bitpack_20(&z, &mut out);
2117        let back = bitunpack_20(&out);
2118        assert_eq!(z, back);
2119    }
2120
2121    /// Naive FIPS 204 Algorithm 37/38/40 reference used to validate the
2122    /// fixed-point implementations. `x` is a canonical value in `[0, q)`.
2123    fn spec_decompose(x: i64, gamma2_den: i64) -> (i64, i64) {
2124        let q = Q as i64;
2125        let gamma2 = (q - 1) / gamma2_den;
2126        let two_gamma2 = 2 * gamma2;
2127        let mut r0 = x % two_gamma2;
2128        if r0 > gamma2 {
2129            r0 -= two_gamma2;
2130        }
2131        if r0 <= -gamma2 {
2132            r0 += two_gamma2;
2133        }
2134        if x - r0 == q - 1 {
2135            (0, r0 - 1)
2136        } else {
2137            ((x - r0) / two_gamma2, r0)
2138        }
2139    }
2140
2141    fn spec_highbits(x: i64, gamma2_den: i64) -> i64 {
2142        spec_decompose(x, gamma2_den).0
2143    }
2144
2145    fn spec_use_hint(h: i64, x: i64, gamma2_den: i64) -> i64 {
2146        let (r1, r0) = spec_decompose(x, gamma2_den);
2147        let m = gamma2_den / 2;
2148        if h == 1 {
2149            if r0 > 0 { (r1 + 1) % m } else { (r1 - 1 + m) % m }
2150        } else {
2151            r1
2152        }
2153    }
2154
2155    fn check_field_ops(x: i64) {
2156        for den in [32i64, 88] {
2157            let (sr1, sr0) = spec_decompose(x, den);
2158            let (gr1, gr0) = if den == 32 {
2159                let (a, b) = decompose32(field_to_montgomery(x as u32));
2160                (a as i64, b as i64)
2161            } else {
2162                let (a, b) = decompose88(field_to_montgomery(x as u32));
2163                (a as i64, b as i64)
2164            };
2165            assert_eq!((gr1, gr0), (sr1, sr0), "decompose mismatch for x={x}, gamma2_den={den}");
2166
2167            let gh = if den == 32 {
2168                highbits32(x as u32) as i64
2169            } else {
2170                highbits88(x as u32) as i64
2171            };
2172            assert_eq!(gh, spec_highbits(x, den), "highbits mismatch for x={x}, den={den}");
2173
2174            for h in 0..2u8 {
2175                let gu = if den == 32 {
2176                    use_hint32(field_to_montgomery(x as u32), h) as i64
2177                } else {
2178                    use_hint88(field_to_montgomery(x as u32), h) as i64
2179                };
2180                assert_eq!(
2181                    gu,
2182                    spec_use_hint(h as i64, x, den),
2183                    "use_hint mismatch for x={x}, h={h}, den={den}"
2184                );
2185            }
2186        }
2187    }
2188
2189    /// Exhaustively compares `decompose`, `highbits` and `use_hint` against a
2190    /// naive transcription of FIPS 204 over every field element. Slow
2191    /// (8.4M values x 2 gamma2 cases); run with `--ignored`.
2192    #[test]
2193    #[ignore = "exhaustive over all q field elements; run with --ignored"]
2194    fn decompose_highbits_use_hint_match_specification_exhaustive() {
2195        for x in 0..Q as i64 {
2196            check_field_ops(x);
2197        }
2198    }
2199
2200    /// Fast randomized smoke test of the same equalities.
2201    #[test]
2202    fn decompose_highbits_use_hint_match_specification_sampled() {
2203        let mut shake = Shake128::new();
2204        shake.absorb(b"field-ops");
2205        for _ in 0..50_000 {
2206            let mut buf = [0u8; 4];
2207            shake.squeeze(&mut buf);
2208            let x = (u32::from_le_bytes(buf) % Q) as i64;
2209            check_field_ops(x);
2210        }
2211        // Exercise the interesting boundaries explicitly.
2212        for x in [
2213            0,
2214            1,
2215            Q as i64 - 1,
2216            Q as i64 / 2,
2217            (Q as i64 - 1) / 32,
2218            (Q as i64 - 1) / 88,
2219        ] {
2220            check_field_ops(x);
2221        }
2222    }
2223
2224    /// Direct unit tests for the three FIPS 204 Algorithm 21 hint checks. The
2225    /// Wycheproof `verify` vectors cover these through the public API; these
2226    /// tests pin the decoder behaviour and its error paths.
2227    #[test]
2228    fn hint_decode_enforces_encoding_rules() {
2229        let params = &PARAMS_65; // omega = 55, k = 6
2230        let omega = params.omega;
2231        const K: usize = 6;
2232
2233        let empty = [0u8; 61];
2234        assert!(hint_decode::<K>(params, &empty).is_ok());
2235
2236        // Strictly increasing indices within a polynomial are accepted.
2237        // Counts must be non-decreasing, so every polynomial records the
2238        // running total.
2239        let mut ok = [0u8; 61];
2240        ok[0] = 3;
2241        ok[1] = 10;
2242        ok[2] = 200;
2243        for i in 0..K {
2244            ok[omega + i] = 3;
2245        }
2246        assert!(hint_decode::<K>(params, &ok).is_ok());
2247
2248        // Duplicate index (CVE-2026-24850 class).
2249        let mut dup = [0u8; 61];
2250        dup[0] = 7;
2251        dup[1] = 7;
2252        for i in 0..K {
2253            dup[omega + i] = 2;
2254        }
2255        assert_eq!(hint_decode::<K>(params, &dup), Err(MlDsaError::InvalidSignature));
2256
2257        // Decreasing index.
2258        let mut dec = [0u8; 61];
2259        dec[0] = 9;
2260        dec[1] = 4;
2261        for i in 0..K {
2262            dec[omega + i] = 2;
2263        }
2264        assert_eq!(hint_decode::<K>(params, &dec), Err(MlDsaError::InvalidSignature));
2265
2266        // Count exceeds omega.
2267        let mut over = [0u8; 61];
2268        over[omega] = (omega + 1) as u8;
2269        assert_eq!(hint_decode::<K>(params, &over), Err(MlDsaError::InvalidSignature));
2270
2271        // Non-monotone cumulative counts.
2272        let mut nonmono = [0u8; 61];
2273        nonmono[0] = 5;
2274        nonmono[omega] = 1;
2275        nonmono[omega + 1] = 0;
2276        assert_eq!(hint_decode::<K>(params, &nonmono), Err(MlDsaError::InvalidSignature));
2277
2278        // Non-zero padding after the last hint.
2279        let mut pad = [0u8; 61];
2280        pad[0] = 5;
2281        pad[1] = 0xff;
2282        for i in 0..K {
2283            pad[omega + i] = 1;
2284        }
2285        assert_eq!(hint_decode::<K>(params, &pad), Err(MlDsaError::InvalidSignature));
2286    }
2287
2288    fn random_poly(shake: &mut Shake128) -> Poly {
2289        let mut p = Poly::default();
2290        let mut buf = [0u8; 4];
2291        for c in p.coeffs.iter_mut() {
2292            shake.squeeze(&mut buf);
2293            *c = u32::from_le_bytes(buf) % Q;
2294        }
2295        p
2296    }
2297
2298    /// The NEON NTT must be bit-identical to the scalar NTT for arbitrary
2299    /// inputs. On non-aarch64 targets both are the scalar implementation.
2300    #[test]
2301    fn ntt_matches_scalar() {
2302        let mut shake = Shake128::new();
2303        shake.absorb(b"mldsa-ntt-equivalence");
2304        for _ in 0..500 {
2305            let p = random_poly(&mut shake);
2306            let simd = ntt(&p);
2307            let scalar = ntt_scalar(&p);
2308            assert_eq!(simd.coeffs, scalar.coeffs, "NEON NTT differs from scalar");
2309        }
2310    }
2311
2312    #[test]
2313    fn invntt_matches_scalar() {
2314        let mut shake = Shake128::new();
2315        shake.absorb(b"mldsa-invntt-equivalence");
2316        for _ in 0..500 {
2317            let p = random_poly(&mut shake);
2318            let n = NttPoly {
2319                coeffs: p.coeffs,
2320            };
2321            let simd = invntt(&n);
2322            let scalar = invntt_scalar(&n);
2323            assert_eq!(simd.coeffs, scalar.coeffs, "NEON inverse NTT differs from scalar");
2324        }
2325    }
2326
2327    #[test]
2328    fn ntt_mul_matches_scalar() {
2329        let mut shake = Shake128::new();
2330        shake.absorb(b"mldsa-nttmul-equivalence");
2331        for _ in 0..500 {
2332            let a = NttPoly {
2333                coeffs: random_poly(&mut shake).coeffs,
2334            };
2335            let b = NttPoly {
2336                coeffs: random_poly(&mut shake).coeffs,
2337            };
2338            let simd = ntt_mul(&a, &b);
2339            let scalar = ntt_mul_scalar(&a, &b);
2340            assert_eq!(simd.coeffs, scalar.coeffs, "NEON ntt_mul differs from scalar");
2341        }
2342    }
2343}