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    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
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum MlDsaError {
37    ContextTooLong,
38    InvalidSignature,
39    InvalidPublicKey,
40    InvalidSignatureLength,
41}
42
43#[cfg(feature = "alloc")]
44impl core::fmt::Display for MlDsaError {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        match self {
47            MlDsaError::ContextTooLong => write!(f, "context length exceeds 255 bytes"),
48            MlDsaError::InvalidSignature => write!(f, "signature is not valid"),
49            MlDsaError::InvalidPublicKey => write!(f, "public key is not valid"),
50            MlDsaError::InvalidSignatureLength => write!(f, "signature length is not valid"),
51        }
52    }
53}
54
55/// The parameter set constants that differ between ML-DSA-44, ML-DSA-65 and
56/// ML-DSA-87.
57///
58/// The dimensions `k` and `l` are carried by const generics on the functions
59/// that need them; everything else is stored here.
60#[derive(Clone, Copy)]
61pub(crate) struct MlDsaParams {
62    /// Bound for the secret coefficients (2 or 4).
63    pub(crate) eta: u32,
64    /// Number of non-zero coefficients in the challenge polynomial (39/49/60).
65    pub(crate) tau: usize,
66    /// `tau * eta`.
67    pub(crate) beta: u32,
68    /// `1 << gamma1_bits`.
69    pub(crate) gamma1: u32,
70    /// `log2(gamma1)` (17 or 19).
71    pub(crate) gamma1_bits: usize,
72    /// `(q - 1) / gamma2_den`.
73    pub(crate) gamma2: u32,
74    /// Denominator of `gamma2` (32 or 88).
75    pub(crate) gamma2_den: u32,
76    /// Maximum number of hints (80/55/75).
77    pub(crate) omega: usize,
78    /// `lambda / 4` (32/48/64).
79    pub(crate) lambda_over_4: usize,
80    /// Encoded size of one `z` polynomial: `(gamma1_bits + 1) * N / 8`.
81    pub(crate) polyz_bytes: usize,
82    /// Encoded public key size in bytes.
83    pub(crate) public_key_size: usize,
84    /// Signature size in bytes.
85    pub(crate) signature_size: usize,
86}
87
88pub(crate) const PARAMS_44: MlDsaParams = MlDsaParams {
89    eta: 2,
90    tau: 39,
91    beta: 78,
92    gamma1: 1 << 17,
93    gamma1_bits: 17,
94    gamma2: (Q - 1) / 88,
95    gamma2_den: 88,
96    omega: 80,
97    lambda_over_4: 32,
98    polyz_bytes: 18 * N / 8,
99    public_key_size: 1312,
100    signature_size: 2420,
101};
102
103pub(crate) const PARAMS_65: MlDsaParams = MlDsaParams {
104    eta: 4,
105    tau: 49,
106    beta: 196,
107    gamma1: 1 << 19,
108    gamma1_bits: 19,
109    gamma2: (Q - 1) / 32,
110    gamma2_den: 32,
111    omega: 55,
112    lambda_over_4: 48,
113    polyz_bytes: 20 * N / 8,
114    public_key_size: 1952,
115    signature_size: 3309,
116};
117
118pub(crate) const PARAMS_87: MlDsaParams = MlDsaParams {
119    eta: 2,
120    tau: 60,
121    beta: 120,
122    gamma1: 1 << 19,
123    gamma1_bits: 19,
124    gamma2: (Q - 1) / 32,
125    gamma2_den: 32,
126    omega: 75,
127    lambda_over_4: 64,
128    polyz_bytes: 20 * N / 8,
129    public_key_size: 2592,
130    signature_size: 4627,
131};
132
133type FieldElement = u32;
134
135fn field_to_montgomery(a: u32) -> FieldElement {
136    debug_assert!(a < Q);
137    field_montgomery_mul(a, RR)
138}
139
140fn field_from_montgomery(a: FieldElement) -> u32 {
141    field_montgomery_reduce(a as u64)
142}
143
144fn field_montgomery_reduce(x: u64) -> u32 {
145    let t = (x as u32).wrapping_mul(QINV);
146    let u = (x + (t as u64) * (Q as u64)) >> 32;
147    field_reduce_once(u as u32)
148}
149
150fn field_montgomery_mul(a: FieldElement, b: FieldElement) -> FieldElement {
151    field_montgomery_reduce(a as u64 * b as u64)
152}
153
154fn field_reduce_once(x: u32) -> FieldElement {
155    let t = x.wrapping_sub(Q);
156    let mask = ((t as i32) >> 31) as u32;
157    t.wrapping_add(Q & mask)
158}
159
160fn field_add(a: FieldElement, b: FieldElement) -> FieldElement {
161    field_reduce_once(a.wrapping_add(b))
162}
163
164fn field_sub(a: FieldElement, b: FieldElement) -> FieldElement {
165    field_reduce_once(a.wrapping_sub(b).wrapping_add(Q))
166}
167
168fn field_sub_to_montgomery(a: u32, b: u32) -> FieldElement {
169    let x = a.wrapping_sub(b).wrapping_add(Q);
170    field_montgomery_mul(x, RR)
171}
172
173fn field_infinity_norm(r: FieldElement) -> u32 {
174    let x = field_from_montgomery(r);
175    let q_minus_x = Q - x;
176    let half_q = Q / 2;
177    let mask = ((half_q.wrapping_sub(x)) as i32 >> 31) as u32;
178    (mask & q_minus_x) | (!mask & x)
179}
180
181fn field_centered_mod(r: FieldElement) -> i32 {
182    let x = field_from_montgomery(r);
183    let x = x as i32;
184    let half_q = (Q / 2) as i32;
185    let mask = ((half_q - x) >> 31) as i32;
186    (mask & (x - Q as i32)) | (!mask & x)
187}
188
189fn power2round(r: FieldElement) -> (u16, FieldElement) {
190    let rr = field_from_montgomery(r);
191    let r1 = (rr + (1 << 12) - 1) >> 13;
192    let r0 = field_sub_to_montgomery(rr, r1 << 13);
193    (r1 as u16, r0)
194}
195
196fn highbits32(x: u32) -> u8 {
197    let r1 = (x + 127) >> 7;
198    let r1 = (r1 * 1025 + (1 << 21)) >> 22;
199    (r1 & 0b1111) as u8
200}
201
202fn highbits88(x: u32) -> u8 {
203    let r1 = (x + 127) >> 7;
204    let r1 = (r1 * 11275 + (1 << 23)) >> 24;
205    // r1 == 44 must map to 0; do it without a data-dependent branch.
206    let d = r1 ^ 44;
207    let not_eq = (d | d.wrapping_neg()) >> 31;
208    (r1 * not_eq) as u8
209}
210
211fn highbits(x: u32, gamma2_den: u32) -> u8 {
212    match gamma2_den {
213        32 => highbits32(x),
214        88 => highbits88(x),
215        _ => unreachable!(),
216    }
217}
218
219fn decompose32(r: FieldElement) -> (u8, i32) {
220    let x = field_from_montgomery(r) as i32;
221    let r1 = highbits32(x as u32);
222    let r0 = x - (r1 as i32) * 2 * (Q as i32 - 1) / 32;
223    let half_q = (Q / 2) as i32;
224    let mask = ((half_q - r0) >> 31) as i32;
225    let r0 = (mask & (r0 - Q as i32)) | (!mask & r0);
226    (r1, r0)
227}
228
229fn decompose88(r: FieldElement) -> (u8, i32) {
230    let x = field_from_montgomery(r) as i32;
231    let r1 = highbits88(x as u32);
232    let r0 = x - (r1 as i32) * 2 * (Q as i32 - 1) / 88;
233    let half_q = (Q / 2) as i32;
234    let mask = ((half_q - r0) >> 31) as i32;
235    let r0 = (mask & (r0 - Q as i32)) | (!mask & r0);
236    (r1, r0)
237}
238
239fn decompose(r: FieldElement, gamma2_den: u32) -> (u8, i32) {
240    match gamma2_den {
241        32 => decompose32(r),
242        88 => decompose88(r),
243        _ => unreachable!(),
244    }
245}
246
247fn make_hint32(ct0: FieldElement, w: FieldElement, cs2: FieldElement) -> u8 {
248    let r_plus_z = field_sub(w, cs2);
249    let v1 = highbits32(field_from_montgomery(r_plus_z));
250    let r = field_add(r_plus_z, ct0);
251    let r1 = highbits32(field_from_montgomery(r));
252    (v1 != r1) as u8
253}
254
255fn make_hint88(ct0: FieldElement, w: FieldElement, cs2: FieldElement) -> u8 {
256    let r_plus_z = field_sub(w, cs2);
257    let v1 = highbits88(field_from_montgomery(r_plus_z));
258    let r = field_add(r_plus_z, ct0);
259    let r1 = highbits88(field_from_montgomery(r));
260    (v1 != r1) as u8
261}
262
263fn make_hint(ct0: FieldElement, w: FieldElement, cs2: FieldElement, gamma2_den: u32) -> u8 {
264    match gamma2_den {
265        32 => make_hint32(ct0, w, cs2),
266        88 => make_hint88(ct0, w, cs2),
267        _ => unreachable!(),
268    }
269}
270
271fn use_hint32(r: FieldElement, hint: u8) -> u8 {
272    let (r1, r0) = decompose32(r);
273    if hint == 0 {
274        return r1;
275    }
276    let r0_gt_0 = !(r0.wrapping_sub(1) >> 31) as u8;
277    let r1_plus = r1.wrapping_add(1) & 0x0F;
278    let r1_minus = r1.wrapping_sub(1) & 0x0F;
279    (r0_gt_0 & r1_plus) | ((!r0_gt_0) & r1_minus)
280}
281
282fn use_hint88(r: FieldElement, hint: u8) -> u8 {
283    const M: u8 = 44;
284    let (mut r1, r0) = decompose88(r);
285    if hint == 0 {
286        return r1;
287    }
288    if r0 > 0 {
289        if r1 == M - 1 {
290            r1 = 0;
291        } else {
292            r1 += 1;
293        }
294    } else if r1 == 0 {
295        r1 = M - 1;
296    } else {
297        r1 -= 1;
298    }
299    r1
300}
301
302fn use_hint(r: FieldElement, hint: u8, gamma2_den: u32) -> u8 {
303    match gamma2_den {
304        32 => use_hint32(r, hint),
305        88 => use_hint88(r, hint),
306        _ => unreachable!(),
307    }
308}
309
310#[derive(Clone, Debug, PartialEq, Eq)]
311#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
312struct Poly {
313    coeffs: [FieldElement; N],
314}
315
316impl Default for Poly {
317    fn default() -> Self {
318        Self {
319            coeffs: [0u32; N],
320        }
321    }
322}
323
324#[derive(Clone, Debug, PartialEq, Eq)]
325#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
326struct NttPoly {
327    coeffs: [FieldElement; N],
328}
329
330impl Default for NttPoly {
331    fn default() -> Self {
332        Self {
333            coeffs: [0u32; N],
334        }
335    }
336}
337
338fn poly_add(a: &Poly, b: &Poly) -> Poly {
339    let mut r = Poly::default();
340    for i in 0..N {
341        r.coeffs[i] = field_add(a.coeffs[i], b.coeffs[i]);
342    }
343    r
344}
345
346fn poly_sub(a: &Poly, b: &Poly) -> Poly {
347    let mut r = Poly::default();
348    for i in 0..N {
349        r.coeffs[i] = field_sub(a.coeffs[i], b.coeffs[i]);
350    }
351    r
352}
353
354fn ntt_add(a: &NttPoly, b: &NttPoly) -> NttPoly {
355    let mut r = NttPoly::default();
356    for i in 0..N {
357        r.coeffs[i] = field_add(a.coeffs[i], b.coeffs[i]);
358    }
359    r
360}
361
362fn ntt_sub(a: &NttPoly, b: &NttPoly) -> NttPoly {
363    let mut r = NttPoly::default();
364    for i in 0..N {
365        r.coeffs[i] = field_sub(a.coeffs[i], b.coeffs[i]);
366    }
367    r
368}
369
370fn ntt_mul(a: &NttPoly, b: &NttPoly) -> NttPoly {
371    let mut r = NttPoly::default();
372    for i in 0..N {
373        r.coeffs[i] = field_montgomery_mul(a.coeffs[i], b.coeffs[i]);
374    }
375    r
376}
377
378const ZETAS: [FieldElement; 256] = [
379    4193792, 25847, 5771523, 7861508, 237124, 7602457, 7504169, 466468, 1826347, 2353451, 8021166, 6288512, 3119733,
380    5495562, 3111497, 2680103, 2725464, 1024112, 7300517, 3585928, 7830929, 7260833, 2619752, 6271868, 6262231,
381    4520680, 6980856, 5102745, 1757237, 8360995, 4010497, 280005, 2706023, 95776, 3077325, 3530437, 6718724, 4788269,
382    5842901, 3915439, 4519302, 5336701, 3574422, 5512770, 3539968, 8079950, 2348700, 7841118, 6681150, 6736599,
383    3505694, 4558682, 3507263, 6239768, 6779997, 3699596, 811944, 531354, 954230, 3881043, 3900724, 5823537, 2071892,
384    5582638, 4450022, 6851714, 4702672, 5339162, 6927966, 3475950, 2176455, 6795196, 7122806, 1939314, 4296819,
385    7380215, 5190273, 5223087, 4747489, 126922, 3412210, 7396998, 2147896, 2715295, 5412772, 4686924, 7969390, 5903370,
386    7709315, 7151892, 8357436, 7072248, 7998430, 1349076, 1852771, 6949987, 5037034, 264944, 508951, 3097992, 44288,
387    7280319, 904516, 3958618, 4656075, 8371839, 1653064, 5130689, 2389356, 8169440, 759969, 7063561, 189548, 4827145,
388    3159746, 6529015, 5971092, 8202977, 1315589, 1341330, 1285669, 6795489, 7567685, 6940675, 5361315, 4499357,
389    4751448, 3839961, 2091667, 3407706, 2316500, 3817976, 5037939, 2244091, 5933984, 4817955, 266997, 2434439, 7144689,
390    3513181, 4860065, 4621053, 7183191, 5187039, 900702, 1859098, 909542, 819034, 495491, 6767243, 8337157, 7857917,
391    7725090, 5257975, 2031748, 3207046, 4823422, 7855319, 7611795, 4784579, 342297, 286988, 5942594, 4108315, 3437287,
392    5038140, 1735879, 203044, 2842341, 2691481, 5790267, 1265009, 4055324, 1247620, 2486353, 1595974, 4613401, 1250494,
393    2635921, 4832145, 5386378, 1869119, 1903435, 7329447, 7047359, 1237275, 5062207, 6950192, 7929317, 1312455,
394    3306115, 6417775, 7100756, 1917081, 5834105, 7005614, 1500165, 777191, 2235880, 3406031, 7838005, 5548557, 6709241,
395    6533464, 5796124, 4656147, 594136, 4603424, 6366809, 2432395, 2454455, 8215696, 1957272, 3369112, 185531, 7173032,
396    5196991, 162844, 1616392, 3014001, 810149, 1652634, 4686184, 6581310, 5341501, 3523897, 3866901, 269760, 2213111,
397    7404533, 1717735, 472078, 7953734, 1723600, 6577327, 1910376, 6712985, 7276084, 8119771, 4546524, 5441381, 6144432,
398    7959518, 6094090, 183443, 7403526, 1612842, 4834730, 7826001, 3919660, 8332111, 7018208, 3937738, 1400424, 7534263,
399    1976782,
400];
401
402fn ntt(f: &Poly) -> NttPoly {
403    let mut f = NttPoly {
404        coeffs: f.coeffs,
405    };
406    let mut m: usize = 0;
407
408    let mut len: usize = 128;
409    while len >= 8 {
410        let mut start: usize = 0;
411        while start < N {
412            m += 1;
413            let zeta = ZETAS[m];
414            let mid = start + len;
415            for j in (start..mid).step_by(2) {
416                let t = field_montgomery_mul(zeta, f.coeffs[j + len]);
417                f.coeffs[j + len] = field_sub(f.coeffs[j], t);
418                f.coeffs[j] = field_add(f.coeffs[j], t);
419                let t = field_montgomery_mul(zeta, f.coeffs[j + len + 1]);
420                f.coeffs[j + len + 1] = field_sub(f.coeffs[j + 1], t);
421                f.coeffs[j + 1] = field_add(f.coeffs[j + 1], t);
422            }
423            start += 2 * len;
424        }
425        len /= 2;
426    }
427
428    let mut start: usize = 0;
429    while start < N {
430        m += 1;
431        let zeta = ZETAS[m];
432        let t = field_montgomery_mul(zeta, f.coeffs[start + 4]);
433        f.coeffs[start + 4] = field_sub(f.coeffs[start], t);
434        f.coeffs[start] = field_add(f.coeffs[start], t);
435        let t = field_montgomery_mul(zeta, f.coeffs[start + 5]);
436        f.coeffs[start + 5] = field_sub(f.coeffs[start + 1], t);
437        f.coeffs[start + 1] = field_add(f.coeffs[start + 1], t);
438        let t = field_montgomery_mul(zeta, f.coeffs[start + 6]);
439        f.coeffs[start + 6] = field_sub(f.coeffs[start + 2], t);
440        f.coeffs[start + 2] = field_add(f.coeffs[start + 2], t);
441        let t = field_montgomery_mul(zeta, f.coeffs[start + 7]);
442        f.coeffs[start + 7] = field_sub(f.coeffs[start + 3], t);
443        f.coeffs[start + 3] = field_add(f.coeffs[start + 3], t);
444        start += 8;
445    }
446
447    start = 0;
448    while start < N {
449        m += 1;
450        let zeta = ZETAS[m];
451        let t = field_montgomery_mul(zeta, f.coeffs[start + 2]);
452        f.coeffs[start + 2] = field_sub(f.coeffs[start], t);
453        f.coeffs[start] = field_add(f.coeffs[start], t);
454        let t = field_montgomery_mul(zeta, f.coeffs[start + 3]);
455        f.coeffs[start + 3] = field_sub(f.coeffs[start + 1], t);
456        f.coeffs[start + 1] = field_add(f.coeffs[start + 1], t);
457        start += 4;
458    }
459
460    start = 0;
461    while start < N {
462        m += 1;
463        let zeta = ZETAS[m];
464        let t = field_montgomery_mul(zeta, f.coeffs[start + 1]);
465        f.coeffs[start + 1] = field_sub(f.coeffs[start], t);
466        f.coeffs[start] = field_add(f.coeffs[start], t);
467        start += 2;
468    }
469
470    f
471}
472
473fn invntt(f: &NttPoly) -> Poly {
474    let mut f = NttPoly {
475        coeffs: f.coeffs,
476    };
477    let mut m: usize = 255;
478
479    let mut start: usize = 0;
480    while start < N {
481        let zeta = ZETAS[m];
482        m -= 1;
483        let t = f.coeffs[start];
484        f.coeffs[start] = field_add(t, f.coeffs[start + 1]);
485        f.coeffs[start + 1] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 1], t));
486        start += 2;
487    }
488
489    start = 0;
490    while start < N {
491        let zeta = ZETAS[m];
492        m -= 1;
493        let t = f.coeffs[start];
494        f.coeffs[start] = field_add(t, f.coeffs[start + 2]);
495        f.coeffs[start + 2] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 2], t));
496        let t = f.coeffs[start + 1];
497        f.coeffs[start + 1] = field_add(t, f.coeffs[start + 3]);
498        f.coeffs[start + 3] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 3], t));
499        start += 4;
500    }
501
502    start = 0;
503    while start < N {
504        let zeta = ZETAS[m];
505        m -= 1;
506        let t = f.coeffs[start];
507        f.coeffs[start] = field_add(t, f.coeffs[start + 4]);
508        f.coeffs[start + 4] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 4], t));
509        let t = f.coeffs[start + 1];
510        f.coeffs[start + 1] = field_add(t, f.coeffs[start + 5]);
511        f.coeffs[start + 5] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 5], t));
512        let t = f.coeffs[start + 2];
513        f.coeffs[start + 2] = field_add(t, f.coeffs[start + 6]);
514        f.coeffs[start + 6] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 6], t));
515        let t = f.coeffs[start + 3];
516        f.coeffs[start + 3] = field_add(t, f.coeffs[start + 7]);
517        f.coeffs[start + 7] = field_montgomery_mul(zeta, field_sub(f.coeffs[start + 7], t));
518        start += 8;
519    }
520
521    let mut len: usize = 8;
522    while len < N {
523        let mut start: usize = 0;
524        while start < N {
525            let zeta = ZETAS[m];
526            m -= 1;
527            let mid = start + len;
528            for j in (start..mid).step_by(2) {
529                let t = f.coeffs[j];
530                f.coeffs[j] = field_add(t, f.coeffs[j + len]);
531                let diff = field_sub(f.coeffs[j + len], t);
532                f.coeffs[j + len] = field_montgomery_mul(zeta, diff);
533                let t = f.coeffs[j + 1];
534                f.coeffs[j + 1] = field_add(t, f.coeffs[j + len + 1]);
535                let diff = field_sub(f.coeffs[j + len + 1], t);
536                f.coeffs[j + len + 1] = field_montgomery_mul(zeta, diff);
537            }
538            start += 2 * len;
539        }
540        len *= 2;
541    }
542
543    let mut r = Poly::default();
544    for i in 0..N {
545        r.coeffs[i] = field_montgomery_mul(f.coeffs[i], N_INV);
546    }
547    r
548}
549
550fn sample_ntt(rho: &[u8; 32], s: u8, r: u8) -> NttPoly {
551    let mut shake = Shake128::new();
552    shake.absorb(rho);
553    shake.absorb(&[s, r]);
554
555    let mut a = NttPoly::default();
556    let mut j: usize = 0;
557    let mut buf = [0u8; 168];
558    let mut off: usize = 168;
559
560    loop {
561        if off >= 168 {
562            shake.squeeze(&mut buf);
563            off = 0;
564        }
565        let v = (buf[off] as u32) | ((buf[off + 1] as u32) << 8) | ((buf[off + 2] as u32) << 16);
566        off += 3;
567        let v = v & 0x7FFFFF;
568        if v < Q {
569            a.coeffs[j] = field_to_montgomery(v);
570            j += 1;
571            if j >= N {
572                break;
573            }
574        }
575    }
576    a
577}
578
579fn coeff_from_half_byte(b: u8, eta: u32) -> Option<FieldElement> {
580    match eta {
581        2 => {
582            if b > 14 {
583                None
584            } else {
585                Some(field_sub_to_montgomery(2, (b % 5) as u32))
586            }
587        }
588        4 => {
589            if b > 8 {
590                None
591            } else {
592                Some(field_sub_to_montgomery(4, b as u32))
593            }
594        }
595        _ => unreachable!(),
596    }
597}
598
599fn sample_bounded_poly(rho: &[u8], r: u8, eta: u32) -> Poly {
600    let mut shake = Shake256::new();
601    shake.absorb(rho);
602    shake.absorb(&[r, 0]);
603
604    let mut a = Poly::default();
605    let mut j: usize = 0;
606    let mut buf = [0u8; 136];
607    let mut off: usize = 136;
608
609    loop {
610        if off >= 136 {
611            shake.squeeze(&mut buf);
612            off = 0;
613        }
614        let z0 = buf[off] & 0x0F;
615        let z1 = buf[off] >> 4;
616        off += 1;
617
618        if let Some(c) = coeff_from_half_byte(z0, eta) {
619            a.coeffs[j] = c;
620            j += 1;
621            if j >= N {
622                break;
623            }
624        }
625        if let Some(c) = coeff_from_half_byte(z1, eta) {
626            a.coeffs[j] = c;
627            j += 1;
628            if j >= N {
629                break;
630            }
631        }
632    }
633    a
634}
635
636fn sample_in_ball(rho: &[u8], tau: usize) -> Poly {
637    let mut shake = Shake256::new();
638    shake.absorb(rho);
639    let mut s = [0u8; 8];
640    shake.squeeze(&mut s);
641
642    let mut c = Poly::default();
643    let mut signs: u64 = u64::from_le_bytes(s);
644
645    for i in (N - tau)..N {
646        let mut jb = [0u8; 1];
647        loop {
648            shake.squeeze(&mut jb);
649            if jb[0] as usize <= i {
650                break;
651            }
652        }
653        let j = jb[0] as usize;
654        c.coeffs[i] = c.coeffs[j];
655        if (signs & 1) == 0 {
656            c.coeffs[j] = ONE;
657        } else {
658            c.coeffs[j] = MINUS_ONE;
659        }
660        signs >>= 1;
661    }
662
663    c
664}
665
666fn expand_mask(nonce: &[u8; 64], kappa: usize, params: &MlDsaParams) -> Poly {
667    let mut shake = Shake256::new();
668    shake.absorb(nonce);
669    shake.absorb(&(kappa as u16).to_le_bytes());
670
671    let mut buf = [0u8; MAX_POLYZ_BYTES];
672    shake.squeeze(&mut buf[..params.polyz_bytes]);
673    bitunpack(&buf[..params.polyz_bytes], params.gamma1_bits)
674}
675
676fn highbits_poly(w: &Poly, params: &MlDsaParams) -> [u8; N] {
677    let mut r = [0u8; N];
678    for i in 0..N {
679        r[i] = highbits(field_from_montgomery(w.coeffs[i]), params.gamma2_den);
680    }
681    r
682}
683
684fn make_hint_poly(ct0: &Poly, w: &Poly, cs2: &Poly, params: &MlDsaParams) -> ([u8; N], usize) {
685    let mut h = [0u8; N];
686    let mut count = 0usize;
687    for i in 0..N {
688        h[i] = make_hint(ct0.coeffs[i], w.coeffs[i], cs2.coeffs[i], params.gamma2_den);
689        count += h[i] as usize;
690    }
691    (h, count)
692}
693
694fn use_hint_poly(r: &Poly, h: &[u8; N], params: &MlDsaParams) -> [u8; N] {
695    let mut w = [0u8; N];
696    for i in 0..N {
697        w[i] = use_hint(r.coeffs[i], h[i], params.gamma2_den);
698    }
699    w
700}
701
702fn coefficients_exceed_bound(w: &Poly, bound: u32) -> bool {
703    for i in 0..N {
704        if field_infinity_norm(w.coeffs[i]) >= bound {
705            return true;
706        }
707    }
708    false
709}
710
711fn lowbits_exceed_bound(w: &Poly, bound: u32, gamma2_den: u32) -> bool {
712    for i in 0..N {
713        let (_, r0) = decompose(w.coeffs[i], gamma2_den);
714        let abs_r0 = (r0 ^ (r0 >> 31)).wrapping_sub(r0 >> 31) as u32;
715        if abs_r0 >= bound {
716            return true;
717        }
718    }
719    false
720}
721
722fn pk_encode<const K: usize>(rho: &[u8; 32], t1: &[[u16; N]; K], out: &mut [u8]) {
723    debug_assert_eq!(out.len(), 32 + K * N * 10 / 8);
724    out[..32].copy_from_slice(rho);
725    let mut pos = 32;
726
727    for w in t1.iter() {
728        for i in (0..N).step_by(4) {
729            let c0 = w[i] as u32;
730            let c1 = w[i + 1] as u32;
731            let c2 = w[i + 2] as u32;
732            let c3 = w[i + 3] as u32;
733            out[pos] = (c0 & 0xFF) as u8;
734            out[pos + 1] = ((c0 >> 8) | (c1 << 2)) as u8;
735            out[pos + 2] = ((c1 >> 6) | (c2 << 4)) as u8;
736            out[pos + 3] = ((c2 >> 4) | (c3 << 6)) as u8;
737            out[pos + 4] = (c3 >> 2) as u8;
738            pos += 5;
739        }
740    }
741}
742
743fn pk_decode<const K: usize>(params: &MlDsaParams, pk: &[u8]) -> Result<([u8; 32], [[u16; N]; K]), MlDsaError> {
744    if pk.len() != params.public_key_size {
745        return Err(MlDsaError::InvalidPublicKey);
746    }
747    let mut rho = [0u8; 32];
748    rho.copy_from_slice(&pk[..32]);
749    let mut t1 = [[0u16; N]; K];
750    let mut pos = 32;
751
752    for r in 0..K {
753        for i in (0..N).step_by(4) {
754            let b0 = pk[pos] as u16;
755            let b1 = pk[pos + 1] as u16;
756            let b2 = pk[pos + 2] as u16;
757            let b3 = pk[pos + 3] as u16;
758            let b4 = pk[pos + 4] as u16;
759            t1[r][i] = b0 | ((b1 & 0b0000_0011) << 8);
760            t1[r][i + 1] = (b1 >> 2) | ((b2 & 0b0000_1111) << 6);
761            t1[r][i + 2] = (b2 >> 4) | ((b3 & 0b0011_1111) << 4);
762            t1[r][i + 3] = (b3 >> 6) | ((b4 & 0b1111_1111) << 2);
763            pos += 5;
764        }
765    }
766    Ok((rho, t1))
767}
768
769fn bitpack_18(z: &Poly, out: &mut [u8]) {
770    const B: u32 = 1 << 17;
771    let mut q = 0usize;
772
773    for i in (0..N).step_by(4) {
774        let w0 = (B as i32 - field_centered_mod(z.coeffs[i])) as u32;
775        out[q] = w0 as u8;
776        out[q + 1] = (w0 >> 8) as u8;
777        out[q + 2] = (w0 >> 16) as u8;
778        let w1 = (B as i32 - field_centered_mod(z.coeffs[i + 1])) as u32;
779        out[q + 2] |= (w1 << 2) as u8;
780        out[q + 3] = (w1 >> 6) as u8;
781        out[q + 4] = (w1 >> 14) as u8;
782        let w2 = (B as i32 - field_centered_mod(z.coeffs[i + 2])) as u32;
783        out[q + 4] |= (w2 << 4) as u8;
784        out[q + 5] = (w2 >> 4) as u8;
785        out[q + 6] = (w2 >> 12) as u8;
786        let w3 = (B as i32 - field_centered_mod(z.coeffs[i + 3])) as u32;
787        out[q + 6] |= (w3 << 6) as u8;
788        out[q + 7] = (w3 >> 2) as u8;
789        out[q + 8] = (w3 >> 10) as u8;
790        q += 9;
791    }
792}
793
794fn bitpack_20(z: &Poly, out: &mut [u8]) {
795    let b = 1u32 << 19;
796    let mut q = 0usize;
797
798    for i in (0..N).step_by(2) {
799        let w0 = (b as i32 - field_centered_mod(z.coeffs[i])) as u32;
800        out[q] = w0 as u8;
801        out[q + 1] = (w0 >> 8) as u8;
802        out[q + 2] = (w0 >> 16) as u8;
803        let w1 = (b as i32 - field_centered_mod(z.coeffs[i + 1])) as u32;
804        out[q + 2] |= ((w1 & 0x0F) << 4) as u8;
805        out[q + 3] = (w1 >> 4) as u8;
806        out[q + 4] = (w1 >> 12) as u8;
807        q += 5;
808    }
809}
810
811fn bitpack(z: &Poly, gamma1_bits: usize, out: &mut [u8]) {
812    match gamma1_bits {
813        17 => bitpack_18(z, out),
814        19 => bitpack_20(z, out),
815        _ => unreachable!(),
816    }
817}
818
819fn bitunpack_18(v: &[u8]) -> Poly {
820    const B: u32 = 1 << 17;
821    const MASK18: u32 = (1 << 18) - 1;
822    let mut r = Poly::default();
823    let mut p = v;
824
825    for i in (0..N).step_by(4) {
826        let w0 = (p[0] as u32) | ((p[1] as u32) << 8) | ((p[2] as u32) << 16);
827        r.coeffs[i] = field_sub_to_montgomery(B, w0 & MASK18);
828        let w1 = ((p[2] as u32) >> 2) | ((p[3] as u32) << 6) | ((p[4] as u32) << 14);
829        r.coeffs[i + 1] = field_sub_to_montgomery(B, w1 & MASK18);
830        let w2 = ((p[4] as u32) >> 4) | ((p[5] as u32) << 4) | ((p[6] as u32) << 12);
831        r.coeffs[i + 2] = field_sub_to_montgomery(B, w2 & MASK18);
832        let w3 = ((p[6] as u32) >> 6) | ((p[7] as u32) << 2) | ((p[8] as u32) << 10);
833        r.coeffs[i + 3] = field_sub_to_montgomery(B, w3 & MASK18);
834        p = &p[9..];
835    }
836    r
837}
838
839fn bitunpack_20(v: &[u8]) -> Poly {
840    let b = 1u32 << 19;
841    let mask20 = (1u32 << 20) - 1;
842    let mut r = Poly::default();
843    let mut p = v;
844
845    for i in (0..N).step_by(2) {
846        let w0 = (p[0] as u32) | ((p[1] as u32) << 8) | ((p[2] as u32) << 16);
847        r.coeffs[i] = field_sub_to_montgomery(b, w0 & mask20);
848        let w1 = ((p[2] as u32) >> 4) | ((p[3] as u32) << 4) | ((p[4] as u32) << 12);
849        r.coeffs[i + 1] = field_sub_to_montgomery(b, w1 & mask20);
850        p = &p[5..];
851    }
852    r
853}
854
855fn bitunpack(v: &[u8], gamma1_bits: usize) -> Poly {
856    match gamma1_bits {
857        17 => bitunpack_18(v),
858        19 => bitunpack_20(v),
859        _ => unreachable!(),
860    }
861}
862
863fn hint_encode<const K: usize>(params: &MlDsaParams, h: &[[u8; N]; K], out: &mut [u8]) {
864    let omega = params.omega;
865    debug_assert_eq!(out.len(), omega + K);
866    out.fill(0);
867    let mut idx: usize = 0;
868
869    for i in 0..K {
870        for j in 0..N {
871            if h[i][j] != 0 {
872                out[idx] = j as u8;
873                idx += 1;
874            }
875        }
876        out[omega + i] = idx as u8;
877    }
878}
879
880fn hint_decode<const K: usize>(params: &MlDsaParams, sig: &[u8]) -> Result<[[u8; N]; K], MlDsaError> {
881    let omega = params.omega;
882    debug_assert_eq!(sig.len(), omega + K);
883    let mut h = [[0u8; N]; K];
884    let mut idx: usize = 0;
885
886    for i in 0..K {
887        let limit = sig[omega + i] as usize;
888        if limit < idx || limit > omega {
889            return Err(MlDsaError::InvalidSignature);
890        }
891        // Track polynomial start so the ordering check doesn't fire across polynomial boundaries.
892        let poly_start = idx;
893        while idx < limit {
894            let j = sig[idx] as usize;
895            // FIPS 204 §6.2 Algorithm 24: indices within a polynomial must be strictly increasing.
896            if idx > poly_start && sig[idx - 1] as usize >= j {
897                return Err(MlDsaError::InvalidSignature);
898            }
899            if j >= N {
900                return Err(MlDsaError::InvalidSignature);
901            }
902            h[i][j] = 1;
903            idx += 1;
904        }
905    }
906    for k in idx..omega {
907        if sig[k] != 0 {
908            return Err(MlDsaError::InvalidSignature);
909        }
910    }
911    Ok(h)
912}
913
914fn sig_encode<const K: usize, const L: usize>(
915    params: &MlDsaParams,
916    ch: &[u8],
917    z: &[Poly; L],
918    h: &[[u8; N]; K],
919    out: &mut [u8],
920) {
921    debug_assert_eq!(out.len(), params.signature_size);
922    let lo4 = params.lambda_over_4;
923    out[..lo4].copy_from_slice(ch);
924
925    let mut pos = lo4;
926    for poly in z.iter() {
927        let pz = params.polyz_bytes;
928        bitpack(poly, params.gamma1_bits, &mut out[pos..pos + pz]);
929        pos += pz;
930    }
931
932    hint_encode::<K>(params, h, &mut out[pos..]);
933}
934
935fn sig_decode<const K: usize, const L: usize>(
936    params: &MlDsaParams,
937    sig: &[u8],
938) -> Result<([u8; MAX_LAMBDA_OVER_4], [Poly; L], [[u8; N]; K]), MlDsaError> {
939    if sig.len() != params.signature_size {
940        return Err(MlDsaError::InvalidSignatureLength);
941    }
942    let lo4 = params.lambda_over_4;
943    let mut ch = [0u8; MAX_LAMBDA_OVER_4];
944    ch[..lo4].copy_from_slice(&sig[..lo4]);
945
946    let mut z: [Poly; L] = core::array::from_fn(|_| Poly::default());
947    let mut pos = lo4;
948    for poly in z.iter_mut() {
949        let pz = params.polyz_bytes;
950        *poly = bitunpack(&sig[pos..pos + pz], params.gamma1_bits);
951        pos += pz;
952    }
953
954    let h = hint_decode::<K>(params, &sig[pos..])?;
955
956    Ok((ch, z, h))
957}
958
959fn w1_encode<const K: usize>(params: &MlDsaParams, w1: &[[u8; N]; K]) -> ([u8; MAX_W1_BYTES], usize) {
960    let mut buf = [0u8; MAX_W1_BYTES];
961    let mut pos = 0usize;
962
963    match params.gamma2_den {
964        32 => {
965            // Coefficients are <= 15, four bits each.
966            for w in w1.iter() {
967                for i in (0..N).step_by(2) {
968                    buf[pos] = w[i] | (w[i + 1] << 4);
969                    pos += 1;
970                }
971            }
972        }
973        88 => {
974            // Coefficients are <= 43, six bits each.
975            for w in w1.iter() {
976                for i in (0..N).step_by(4) {
977                    let (b0, b1, b2, b3) = (w[i], w[i + 1], w[i + 2], w[i + 3]);
978                    buf[pos] = b0 | (b1 << 6);
979                    buf[pos + 1] = (b1 >> 2) | (b2 << 4);
980                    buf[pos + 2] = (b2 >> 4) | (b3 << 2);
981                    pos += 3;
982                }
983            }
984        }
985        _ => unreachable!(),
986    }
987
988    (buf, pos)
989}
990
991fn compute_matrix_a<const K: usize, const L: usize>(rho: &[u8; 32]) -> [[NttPoly; L]; K] {
992    core::array::from_fn(|r| core::array::from_fn(|s| sample_ntt(rho, s as u8, r as u8)))
993}
994
995fn compute_matrix_a_into<const K: usize, const L: usize>(a: &mut [[NttPoly; L]; K], rho: &[u8; 32]) {
996    for r in 0..K {
997        for s in 0..L {
998            a[r][s] = sample_ntt(rho, s as u8, r as u8);
999        }
1000    }
1001}
1002
1003pub(crate) fn compute_pubkey_hash(pk: &[u8]) -> [u8; 64] {
1004    let mut shake = Shake256::new();
1005    shake.absorb(pk);
1006    let mut tr = [0u8; 64];
1007    shake.squeeze(&mut tr);
1008    tr
1009}
1010
1011fn compute_message_hash(tr: &[u8; 64], message: &[u8], ctx: &[u8]) -> Result<[u8; 64], MlDsaError> {
1012    if ctx.len() > CONTEXT_MAX_LEN {
1013        return Err(MlDsaError::ContextTooLong);
1014    }
1015    let mut shake = Shake256::new();
1016    shake.absorb(tr);
1017    shake.absorb(&[0u8]);
1018    shake.absorb(&[ctx.len() as u8]);
1019    shake.absorb(ctx);
1020    shake.absorb(message);
1021    let mut mu = [0u8; 64];
1022    shake.squeeze(&mut mu);
1023    Ok(mu)
1024}
1025
1026fn compute_t1_hat<const K: usize>(t1: &[[u16; N]; K]) -> [NttPoly; K] {
1027    core::array::from_fn(|i| {
1028        let mut w = Poly::default();
1029        for j in 0..N {
1030            w.coeffs[j] = field_to_montgomery((t1[i][j] as u32) << D);
1031        }
1032        ntt(&w)
1033    })
1034}
1035
1036/// Expanded key material shared by all ML-DSA parameter sets.
1037///
1038/// This is the only way to sign: key generation runs once when the key is
1039/// initialized and the resulting matrix `A` and secret vectors in the NTT
1040/// domain are cached, so signing does not repeat the expensive key generation.
1041///
1042/// The value is a plain fixed-size type that never allocates, so it can live in
1043/// a `static` on `no_std` and embedded targets. Secrets are zeroized on drop
1044/// when the `zeroize` feature is enabled.
1045#[derive(Debug)]
1046#[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))]
1047pub(crate) struct MlDsaKeyMaterial<const K: usize, const L: usize, const PK_SIZE: usize> {
1048    seed: [u8; SEED_SIZE],
1049    key_bytes: [u8; 32],
1050    pk: [u8; PK_SIZE],
1051    tr: [u8; 64],
1052    a: [[NttPoly; L]; K],
1053    s1_hat: [NttPoly; L],
1054    s2_hat: [NttPoly; K],
1055    t0_hat: [NttPoly; K],
1056}
1057
1058impl<const K: usize, const L: usize, const PK_SIZE: usize> MlDsaKeyMaterial<K, L, PK_SIZE> {
1059    /// Creates a zeroed, uninitialized key material value.
1060    ///
1061    /// SAFETY: all fields are plain arrays of integers and polynomials whose
1062    /// all-zero bit pattern is a valid value, so `zeroed` is sound here. The
1063    /// key must be initialized before signing.
1064    pub(crate) const fn new() -> Self {
1065        // SAFETY: see above.
1066        unsafe { core::mem::zeroed() }
1067    }
1068
1069    /// Expands `seed` into a fresh signing key.
1070    pub(crate) fn from_seed(params: &MlDsaParams, seed: &[u8; SEED_SIZE]) -> Self {
1071        let mut this = Self::new();
1072        this.init(params, seed);
1073        this
1074    }
1075
1076    /// Generates a fresh random signing key.
1077    #[cfg(feature = "random")]
1078    pub(crate) fn from_random(params: &MlDsaParams) -> Self {
1079        let mut this = Self::new();
1080        this.generate(params);
1081        this
1082    }
1083
1084    /// Expands `seed` into a signing key, overwriting any previous state.
1085    pub(crate) fn init(&mut self, params: &MlDsaParams, seed: &[u8; SEED_SIZE]) {
1086        debug_assert_eq!(PK_SIZE, params.public_key_size);
1087        let mut shake = Shake256::new();
1088        shake.absorb(seed);
1089        shake.absorb(&[K as u8, L as u8]);
1090        let mut rho = [0u8; 32];
1091        let mut rhos = [0u8; 64];
1092        shake.squeeze(&mut rho);
1093        shake.squeeze(&mut rhos);
1094        shake.squeeze(&mut self.key_bytes);
1095
1096        compute_matrix_a_into::<K, L>(&mut self.a, &rho);
1097
1098        for r in 0..L {
1099            let s1 = sample_bounded_poly(&rhos, r as u8, params.eta);
1100            self.s1_hat[r] = ntt(&s1);
1101        }
1102        for r in 0..K {
1103            let s2 = sample_bounded_poly(&rhos, (L + r) as u8, params.eta);
1104            self.s2_hat[r] = ntt(&s2);
1105        }
1106
1107        let mut t1 = [[0u16; N]; K];
1108        for i in 0..K {
1109            let mut t_hat = self.s2_hat[i].clone();
1110            for j in 0..L {
1111                t_hat = ntt_add(&t_hat, &ntt_mul(&self.a[i][j], &self.s1_hat[j]));
1112            }
1113            let t = invntt(&t_hat);
1114            let mut t0 = Poly::default();
1115            for j in 0..N {
1116                (t1[i][j], t0.coeffs[j]) = power2round(t.coeffs[j]);
1117            }
1118            self.t0_hat[i] = ntt(&t0);
1119        }
1120
1121        pk_encode::<K>(&rho, &t1, &mut self.pk);
1122        self.tr = compute_pubkey_hash(&self.pk);
1123        self.seed = *seed;
1124    }
1125
1126    #[cfg(feature = "random")]
1127    pub(crate) fn generate(&mut self, params: &MlDsaParams) -> [u8; SEED_SIZE] {
1128        let seed: [u8; SEED_SIZE] = crate::random::random_bytes();
1129        self.init(params, &seed);
1130        seed
1131    }
1132
1133    pub(crate) fn public_key(&self) -> &[u8; PK_SIZE] {
1134        &self.pk
1135    }
1136
1137    pub(crate) fn seed(&self) -> &[u8; SEED_SIZE] {
1138        &self.seed
1139    }
1140
1141    pub(crate) fn sign_derand_into(
1142        &self,
1143        params: &MlDsaParams,
1144        message: &[u8],
1145        ctx: &[u8],
1146        rnd: &[u8; 32],
1147        sig_out: &mut [u8],
1148    ) -> Result<(), MlDsaError> {
1149        let mu = compute_message_hash(&self.tr, message, ctx)?;
1150        self.sign_internal(params, &mu, rnd, sig_out);
1151        Ok(())
1152    }
1153
1154    pub(crate) fn sign_external_mu_derand_into(
1155        &self,
1156        params: &MlDsaParams,
1157        mu: &[u8; 64],
1158        rnd: &[u8; 32],
1159        sig_out: &mut [u8],
1160    ) {
1161        self.sign_internal(params, mu, rnd, sig_out);
1162    }
1163
1164    fn sign_internal(&self, params: &MlDsaParams, mu: &[u8; 64], rnd: &[u8; 32], sig_out: &mut [u8]) {
1165        debug_assert_eq!(sig_out.len(), params.signature_size);
1166        let a = &self.a;
1167        let s1_hat = &self.s1_hat;
1168        let s2_hat = &self.s2_hat;
1169        let t0_hat = &self.t0_hat;
1170
1171        let gamma1beta = params.gamma1 - params.beta;
1172        let gamma2 = params.gamma2;
1173        let gamma2beta = gamma2 - params.beta;
1174        let lo4 = params.lambda_over_4;
1175
1176        let mut h_shake = Shake256::new();
1177        h_shake.absorb(&self.key_bytes);
1178        h_shake.absorb(rnd);
1179        h_shake.absorb(mu);
1180        let mut nonce = [0u8; 64];
1181        h_shake.squeeze(&mut nonce);
1182
1183        let mut kappa: usize = 0;
1184
1185        loop {
1186            let mut y: [Poly; L] = core::array::from_fn(|_| Poly::default());
1187            for item in y.iter_mut() {
1188                *item = expand_mask(&nonce, kappa, params);
1189                kappa += 1;
1190            }
1191
1192            let mut y_hat: [NttPoly; L] = core::array::from_fn(|_| NttPoly::default());
1193            for i in 0..L {
1194                y_hat[i] = ntt(&y[i]);
1195            }
1196
1197            let mut w: [Poly; K] = core::array::from_fn(|_| Poly::default());
1198            for i in 0..K {
1199                let mut w_hat = NttPoly::default();
1200                for j in 0..L {
1201                    w_hat = ntt_add(&w_hat, &ntt_mul(&a[i][j], &y_hat[j]));
1202                }
1203                w[i] = invntt(&w_hat);
1204            }
1205
1206            let mut w1 = [[0u8; N]; K];
1207            for i in 0..K {
1208                w1[i] = highbits_poly(&w[i], params);
1209            }
1210
1211            let mut ch_shake = Shake256::new();
1212            ch_shake.absorb(mu);
1213            let (w1_bytes, w1_len) = w1_encode::<K>(params, &w1);
1214            ch_shake.absorb(&w1_bytes[..w1_len]);
1215            let mut ct = [0u8; MAX_LAMBDA_OVER_4];
1216            ch_shake.squeeze(&mut ct[..lo4]);
1217
1218            let c = sample_in_ball(&ct[..lo4], params.tau);
1219            let c_hat = ntt(&c);
1220
1221            let mut cs1: [Poly; L] = core::array::from_fn(|_| Poly::default());
1222            for i in 0..L {
1223                cs1[i] = invntt(&ntt_mul(&c_hat, &s1_hat[i]));
1224            }
1225            let mut cs2: [Poly; K] = core::array::from_fn(|_| Poly::default());
1226            for i in 0..K {
1227                cs2[i] = invntt(&ntt_mul(&c_hat, &s2_hat[i]));
1228            }
1229
1230            let mut z: [Poly; L] = core::array::from_fn(|_| Poly::default());
1231            let mut reject = false;
1232            for i in 0..L {
1233                z[i] = poly_add(&y[i], &cs1[i]);
1234                if coefficients_exceed_bound(&z[i], gamma1beta) {
1235                    reject = true;
1236                    break;
1237                }
1238            }
1239            if reject {
1240                continue;
1241            }
1242
1243            for i in 0..K {
1244                let r0 = poly_sub(&w[i], &cs2[i]);
1245                if lowbits_exceed_bound(&r0, gamma2beta, params.gamma2_den) {
1246                    reject = true;
1247                    break;
1248                }
1249            }
1250            if reject {
1251                continue;
1252            }
1253
1254            let mut ct0: [Poly; K] = core::array::from_fn(|_| Poly::default());
1255            for i in 0..K {
1256                ct0[i] = invntt(&ntt_mul(&c_hat, &t0_hat[i]));
1257                if coefficients_exceed_bound(&ct0[i], gamma2) {
1258                    reject = true;
1259                    break;
1260                }
1261            }
1262            if reject {
1263                continue;
1264            }
1265
1266            let mut total_hints: usize = 0;
1267            let mut h = [[0u8; N]; K];
1268            for i in 0..K {
1269                let (hi, count) = make_hint_poly(&ct0[i], &w[i], &cs2[i], params);
1270                h[i] = hi;
1271                total_hints += count;
1272            }
1273            if total_hints > params.omega {
1274                continue;
1275            }
1276
1277            sig_encode::<K, L>(params, &ct[..lo4], &z, &h, sig_out);
1278            return;
1279        }
1280    }
1281}
1282
1283fn verify_internal<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1284    params: &MlDsaParams,
1285    pk: &[u8; PK_SIZE],
1286    mu: &[u8; 64],
1287    sig: &[u8; SIG_SIZE],
1288) -> Result<(), MlDsaError> {
1289    let (rho, t1) = pk_decode::<K>(params, pk)?;
1290    let (ch, z, h) = sig_decode::<K, L>(params, sig)?;
1291
1292    let gamma1beta = params.gamma1 - params.beta;
1293
1294    // FIPS 204 §6.2 Algorithm 3 step 5: check ||z||∞ < γ1 − β before the
1295    // expensive matrix-vector product.
1296    for item in z.iter() {
1297        if coefficients_exceed_bound(item, gamma1beta) {
1298            return Err(MlDsaError::InvalidSignature);
1299        }
1300    }
1301
1302    let a = compute_matrix_a::<K, L>(&rho);
1303    let t1_hat = compute_t1_hat::<K>(&t1);
1304
1305    let c = sample_in_ball(&ch[..params.lambda_over_4], params.tau);
1306    let c_hat = ntt(&c);
1307
1308    let mut z_hat: [NttPoly; L] = core::array::from_fn(|_| NttPoly::default());
1309    for i in 0..L {
1310        z_hat[i] = ntt(&z[i]);
1311    }
1312
1313    let mut w_approx: [Poly; K] = core::array::from_fn(|_| Poly::default());
1314    for i in 0..K {
1315        let mut w_hat = NttPoly::default();
1316        for j in 0..L {
1317            w_hat = ntt_add(&w_hat, &ntt_mul(&a[i][j], &z_hat[j]));
1318        }
1319        w_hat = ntt_sub(&w_hat, &ntt_mul(&c_hat, &t1_hat[i]));
1320        w_approx[i] = invntt(&w_hat);
1321    }
1322
1323    let mut w1 = [[0u8; N]; K];
1324    for i in 0..K {
1325        w1[i] = use_hint_poly(&w_approx[i], &h[i], params);
1326    }
1327
1328    let mut ch_shake = Shake256::new();
1329    ch_shake.absorb(mu);
1330    let (w1_bytes, w1_len) = w1_encode::<K>(params, &w1);
1331    ch_shake.absorb(&w1_bytes[..w1_len]);
1332    let mut computed_ch = [0u8; MAX_LAMBDA_OVER_4];
1333    ch_shake.squeeze(&mut computed_ch[..params.lambda_over_4]);
1334
1335    if !constant_time_eq(&ch[..params.lambda_over_4], &computed_ch[..params.lambda_over_4]) {
1336        return Err(MlDsaError::InvalidSignature);
1337    }
1338
1339    Ok(())
1340}
1341
1342/// Verifies a signature over `message` with the optional context `ctx`.
1343pub(crate) fn verify_message<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1344    params: &MlDsaParams,
1345    pk: &[u8; PK_SIZE],
1346    message: &[u8],
1347    sig: &[u8; SIG_SIZE],
1348    ctx: &[u8],
1349) -> Result<(), MlDsaError> {
1350    let tr = compute_pubkey_hash(pk);
1351    let mu = compute_message_hash(&tr, message, ctx)?;
1352    verify_internal::<K, L, PK_SIZE, SIG_SIZE>(params, pk, &mu, sig)
1353}
1354
1355/// Verifies a signature over a precomputed 64-byte message representative `mu`
1356/// (FIPS 204 "external mu" verification).
1357pub(crate) fn verify_external_mu<const K: usize, const L: usize, const PK_SIZE: usize, const SIG_SIZE: usize>(
1358    params: &MlDsaParams,
1359    pk: &[u8; PK_SIZE],
1360    mu: &[u8; 64],
1361    sig: &[u8; SIG_SIZE],
1362) -> Result<(), MlDsaError> {
1363    verify_internal::<K, L, PK_SIZE, SIG_SIZE>(params, pk, mu, sig)
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369
1370    #[test]
1371    fn ntt_round_trip() {
1372        let mut shake = Shake128::new();
1373        for _ in 0..100 {
1374            let mut poly = Poly::default();
1375            for j in 0..N {
1376                let mut b = [0u8; 4];
1377                shake.squeeze(&mut b);
1378                let x = u32::from_le_bytes(b) % Q;
1379                poly.coeffs[j] = field_to_montgomery(x);
1380            }
1381            let fwd = ntt(&poly);
1382            let back = invntt(&fwd);
1383            for j in 0..N {
1384                assert_eq!(poly.coeffs[j], back.coeffs[j], "NTT round-trip failed at coeff {}", j);
1385            }
1386        }
1387    }
1388
1389    #[test]
1390    #[cfg(not(debug_assertions))]
1391    fn power2round_consistency() {
1392        for x in 0u32..Q {
1393            let mr = field_to_montgomery(x);
1394            let (r1, r0) = power2round(mr);
1395            let recovered = (r1 as u32) << D;
1396
1397            let expected_r0 = if x >= recovered {
1398                x - recovered
1399            } else {
1400                x.wrapping_sub(recovered)
1401            };
1402
1403            assert!(
1404                expected_r0 < (1 << D) || expected_r0 >= Q - (1 << D) + 1,
1405                "power2round: r0 out of range at x={}, r1={}, r0_expected={}",
1406                x,
1407                r1,
1408                expected_r0
1409            );
1410
1411            let got_r0 = field_from_montgomery(r0);
1412            assert!(
1413                got_r0 == expected_r0 || got_r0 == expected_r0.wrapping_add(Q) || got_r0 == expected_r0.wrapping_sub(Q),
1414                "power2round: r0 mismatch at x={}, r1={}, expected_r0={}, got_r0={}",
1415                x,
1416                r1,
1417                expected_r0,
1418                got_r0
1419            );
1420        }
1421    }
1422
1423    #[test]
1424    #[cfg(not(debug_assertions))]
1425    fn highbits32_exhaustive() {
1426        for x in 0u32..Q {
1427            let h = highbits32(x);
1428            assert!(h < 16, "highbits32: h={} out of range at x={}", h, x);
1429            let (r1, _) = decompose32(field_to_montgomery(x));
1430            assert_eq!(h, r1, "highbits32 vs decompose32 r1 mismatch at x={}", x);
1431        }
1432    }
1433
1434    #[test]
1435    #[cfg(not(debug_assertions))]
1436    fn highbits88_exhaustive() {
1437        for x in 0u32..Q {
1438            let h = highbits88(x);
1439            assert!(h < 44, "highbits88: h={} out of range at x={}", h, x);
1440            let (r1, _) = decompose88(field_to_montgomery(x));
1441            assert_eq!(h, r1, "highbits88 vs decompose88 r1 mismatch at x={}", x);
1442        }
1443    }
1444
1445    #[test]
1446    fn make_hint32_correctness() {
1447        let mut shake = Shake128::new();
1448        for _ in 0..5000 {
1449            let mut b = [0u8; 12];
1450            shake.squeeze(&mut b);
1451            let ct0_val = u32::from_le_bytes(b[0..4].try_into().unwrap()) % Q;
1452            let w_val = u32::from_le_bytes(b[4..8].try_into().unwrap()) % Q;
1453            let cs2_val = u32::from_le_bytes(b[8..12].try_into().unwrap()) % Q;
1454            let ct0 = field_to_montgomery(ct0_val);
1455            let w = field_to_montgomery(w_val);
1456            let cs2 = field_to_montgomery(cs2_val);
1457            let h = make_hint32(ct0, w, cs2);
1458            assert!(h == 0 || h == 1, "make_hint32: hint not 0 or 1");
1459        }
1460    }
1461
1462    #[test]
1463    fn make_hint88_correctness() {
1464        let mut shake = Shake128::new();
1465        for _ in 0..5000 {
1466            let mut b = [0u8; 12];
1467            shake.squeeze(&mut b);
1468            let ct0_val = u32::from_le_bytes(b[0..4].try_into().unwrap()) % Q;
1469            let w_val = u32::from_le_bytes(b[4..8].try_into().unwrap()) % Q;
1470            let cs2_val = u32::from_le_bytes(b[8..12].try_into().unwrap()) % Q;
1471            let ct0 = field_to_montgomery(ct0_val);
1472            let w = field_to_montgomery(w_val);
1473            let cs2 = field_to_montgomery(cs2_val);
1474            let h = make_hint88(ct0, w, cs2);
1475            assert!(h == 0 || h == 1, "make_hint88: hint not 0 or 1");
1476        }
1477    }
1478
1479    #[test]
1480    fn pk_encode_decode_round_trip_65() {
1481        let mut t1 = [[0u16; N]; 6];
1482        let mut shake = Shake128::new();
1483        for row in t1.iter_mut() {
1484            for c in row.iter_mut() {
1485                let mut b = [0u8; 2];
1486                shake.squeeze(&mut b);
1487                *c = u16::from_le_bytes(b) & 0x3FF;
1488            }
1489        }
1490        let rho = [7u8; 32];
1491        let mut pk = [0u8; PARAMS_65.public_key_size];
1492        pk_encode::<6>(&rho, &t1, &mut pk);
1493        let (rho2, t1_2) = pk_decode::<6>(&PARAMS_65, &pk).unwrap();
1494        assert_eq!(rho, rho2);
1495        assert_eq!(t1, t1_2);
1496    }
1497
1498    #[test]
1499    fn bitpack_unpack_round_trip_18() {
1500        let mut shake = Shake128::new();
1501        let mut z = Poly::default();
1502        let b = 1i32 << 17;
1503        for c in z.coeffs.iter_mut() {
1504            let mut buf = [0u8; 4];
1505            shake.squeeze(&mut buf);
1506            let v = (u32::from_le_bytes(buf) % (2 * b as u32)) as i32 - (b - 1);
1507            *c = field_to_montgomery(v.rem_euclid(Q as i32) as u32);
1508        }
1509        let mut out = [0u8; 18 * N / 8];
1510        bitpack_18(&z, &mut out);
1511        let back = bitunpack_18(&out);
1512        assert_eq!(z, back);
1513    }
1514
1515    #[test]
1516    fn bitpack_unpack_round_trip_20() {
1517        let mut shake = Shake128::new();
1518        let mut z = Poly::default();
1519        let b = 1i32 << 19;
1520        for c in z.coeffs.iter_mut() {
1521            let mut buf = [0u8; 4];
1522            shake.squeeze(&mut buf);
1523            let v = (u32::from_le_bytes(buf) % (2 * b as u32)) as i32 - (b - 1);
1524            *c = field_to_montgomery(v.rem_euclid(Q as i32) as u32);
1525        }
1526        let mut out = [0u8; 20 * N / 8];
1527        bitpack_20(&z, &mut out);
1528        let back = bitunpack_20(&out);
1529        assert_eq!(z, back);
1530    }
1531}