1use 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
16pub(crate) const N: usize = 256;
18pub(crate) const SEED_SIZE: usize = 32;
20pub(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#[derive(Clone, Copy)]
61pub(crate) struct MlDsaParams {
62 pub(crate) eta: u32,
64 pub(crate) tau: usize,
66 pub(crate) beta: u32,
68 pub(crate) gamma1: u32,
70 pub(crate) gamma1_bits: usize,
72 pub(crate) gamma2: u32,
74 pub(crate) gamma2_den: u32,
76 pub(crate) omega: usize,
78 pub(crate) lambda_over_4: usize,
80 pub(crate) polyz_bytes: usize,
82 pub(crate) public_key_size: usize,
84 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 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 let poly_start = idx;
893 while idx < limit {
894 let j = sig[idx] as usize;
895 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 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 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#[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 pub(crate) const fn new() -> Self {
1065 unsafe { core::mem::zeroed() }
1067 }
1068
1069 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 #[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 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 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
1342pub(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
1355pub(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}