bouncycastle_mldsa_lowmemory/polynomial.rs
1//! Represents a polynomial over the ML-DSA ring.
2
3use crate::aux_functions::{high_bits, low_bits, make_hint, use_hint};
4use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q, q_inv};
5use core::ops::{Index, IndexMut};
6
7/// A polynomial over the ML-DSA ring.
8///
9/// Dev note: The following structure does not necessarily need to be declared as public.
10/// There is no real scenario where this function needs to be called directly.
11/// However, in order to test the Debug and Display traits, it is necessary to use STD, so those
12/// can't be tested from inline tests in this file and the real unit tests are in a different crate.
13/// That's the reason why pub is used.
14///
15/// # ๐จ Security ๐จ
16/// Polynomials themselves are not inherently secret since sometimes they are part of public keys
17/// and sometimes private keys.
18/// It is the responsibility of the caller to wrap sensitive instances in `Secret<Polynomial>`.
19/// Note: at the moment, nothing in this crate uses `Secret<Polynomial>`, so I have left the `impl ZeroizablePrimitive` commented-out.
20#[derive(Clone, Copy)]
21pub struct Polynomial {
22 pub(crate) coeffs: [i32; N],
23}
24
25/// Convenience function to avoid ".0" all over the place.
26impl Index<usize> for Polynomial {
27 type Output = i32;
28
29 fn index(&self, index: usize) -> &Self::Output {
30 &self.coeffs[index]
31 }
32}
33/// Convenience function to avoid ".0" all over the place.
34impl IndexMut<usize> for Polynomial {
35 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
36 &mut self.coeffs[index]
37 }
38}
39
40// Turn this back on if we want to start tagging things as `Secret<Polynomial>`.
41// impl ZeroizablePrimitive for Polynomial {
42// const ZEROED: Self = Self::new();
43// }
44
45impl Polynomial {
46 /// Create a new polynomial with all coefficients set to zero.
47 pub const fn new() -> Self {
48 Self { coeffs: [0i32; N] }
49 }
50
51 pub(crate) fn conditional_add_q(&mut self) {
52 for x in self.coeffs.iter_mut() {
53 *x = conditional_add_q(*x);
54 }
55 }
56
57 /// Reduces every coefficient to |๐| < ๐ without changing its residue. See [`reduce32`] for when
58 /// this is required for security reasons; note that [`Self::reduce`] is a *Montgomery* reduction and is not a substitute.
59 pub(crate) fn reduce32(&mut self) {
60 for x in self.coeffs.iter_mut() {
61 *x = reduce32(*x);
62 }
63 }
64
65 /// Algorithm 44 AddNTT(๐, ๐)ฬ
66 /// Computes the sum a + ๐ of two elements ๐, ๐ โ ๐๐.
67 /// Note: result could be up to 2q.
68 pub(crate) fn add_ntt(&mut self, w: &Self) {
69 for i in 0..N {
70 self[i] += w[i];
71 }
72 }
73
74 pub(crate) fn sub(&mut self, w: &Self) {
75 for i in 0..N {
76 self[i] -= w[i];
77 }
78 }
79
80 /// Algorithm 45 MultiplyNTT(๐, ๐)ฬ
81 /// Computes the product ๐ โฬ ๐ of two elements ๐, ๐ โ ๐๐.
82 /// Input: ๐, ๐ โ ๐๐.
83 /// Output: ๐ โ ๐๐.
84 /// Multiply the coefficients in this polynomial by those in another polynomial and perform montgomery reduction.
85 /// Also called pointwise montgomery multiplication
86 pub(crate) fn multiply_ntt(&mut self, b: &Polynomial) {
87 for i in 0..N {
88 self[i] = montgomery_reduce((self[i] as i64) * (b[i] as i64));
89 }
90 }
91
92 pub(crate) fn high_bits<const GAMMA2: i32>(&mut self) {
93 for i in 0..N {
94 self[i] = high_bits::<GAMMA2>(self[i]);
95 }
96 }
97
98 pub(crate) fn low_bits<const GAMMA2: i32>(&mut self) {
99 for i in 0..N {
100 self[i] = low_bits::<GAMMA2>(self[i]);
101 }
102 }
103
104 pub(crate) fn check_norm<const BOUND: i32>(&self) -> bool {
105 // Fine that this is not constant-time (returns true early) because it is used in a rejection loop.
106 // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing
107 // the signature validation.
108 // So the i32 that was just checked in a non-constant-time manner is about to get thrown away.
109
110 // Note: this formulation of the check_norm function usually requires this bounds check
111 // if bound > (q - 1) / 8 {
112 // return true;
113 // }
114 // but since BOUND is a constant here, a debug_assert is done to ensure the value is what is expected.
115 debug_assert!(BOUND <= (q - 1) / 8);
116
117 let mut t: i32;
118 for x in self.coeffs.iter() {
119 t = *x >> 31;
120 t = *x - (t & (2 * *x));
121
122 if t >= BOUND {
123 return true;
124 }
125 }
126 false
127 }
128
129 pub(crate) fn shift_left<const d: i32>(&mut self) {
130 for x in self.coeffs.iter_mut() {
131 *x <<= d;
132 }
133 }
134
135 /// Creates the hint vector, and also returns its hamming weight (ie the number of 1's).
136 pub(crate) fn make_hint_row<const GAMMA2: i32>(&self, r: &Self) -> (Self, i32) {
137 let mut out = Polynomial::new();
138 let mut count = 0i32;
139 for i in 0..N {
140 let x = make_hint::<GAMMA2>(self[i], r[i]);
141 out[i] = x;
142 count += x;
143 }
144
145 (out, count)
146 }
147
148 pub(crate) fn w1_encode<const POLY_W1_PACKED_LEN: usize>(&self) -> [u8; POLY_W1_PACKED_LEN] {
149 // It might seem counter-intuitive for a low-memory implementation to create a tmp buffer
150 // rather than work in the provided buffer, but experimentation shows that
151 // rust is roughly an order of magnitude faster working in a scope-local array than
152 // in a referenced piece of memory.
153 // This is possibly because, once the rust compiler understands that the intermediate values are scope-local,
154 // it performs optimizations throughout all of the computation into CPU registers and skips, in this case,
155 // several hundred physical memory writes.
156 // So while it looks odd to use a scope variable in a low-memory implementation, it's way faster
157 // while seemingly maintaining the same physical memory footprint.
158 let mut r = [0u8; POLY_W1_PACKED_LEN];
159
160 match POLY_W1_PACKED_LEN {
161 MLDSA44_POLY_W1_PACKED_LEN => {
162 for i in 0..N / 4 {
163 r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8);
164 r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8);
165 r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8);
166 }
167 }
168 // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value
169 MLDSA65_POLY_W1_PACKED_LEN => {
170 for i in 0..N / 2 {
171 r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8;
172 }
173 }
174 _ => {
175 unreachable!()
176 }
177 }
178
179 r
180 }
181
182 /// Algorithm 41 NTT(๐ค)
183 /// Computes the NTT.
184 /// Input: Polynomial ๐ค(๐) = ฮฃ_{j=0}^{255} ๐ค๐๐๐ โ ๐
๐.
185 /// Output: ๐ค_hat = (๐ค_hat\[0], ..., ๐ค_hat\[255]) โ ๐๐.
186 ///
187 /// Note: by convention, variables holding the output of the NTT function should be named "_hat"
188 /// to indicate that they are in the NTT domain (sometimes called the frequency domain), not the natural domain.
189 /// Usage of the rust type system to enforce this is arguably unnecessary, since that's what the NIST
190 /// test vectors are for.
191 ///
192 /// Design choice: the NTT is not done in-place, but copy data to a new array.
193 /// This uses slightly more memory and requires a copy, but makes the code easier to read
194 /// and less likely to contain a bug. But this optimization could be considered in the future.
195 pub(crate) fn ntt(&mut self) {
196 let mut m: usize = 0;
197 let mut len: usize = 128;
198
199 while len >= 1 {
200 let mut start: usize = 0;
201 while start < N {
202 m += 1;
203 let z: i32 = ZETAS[m];
204
205 for j in start..start + len {
206 let t = montgomery_reduce(z as i64 * self[j + len] as i64);
207 self[j + len] = self[j] - t; // '% q' not strictly needed cause it gets reduced at some point later. Removing it gave +5% in benchmarking
208 self[j] = self[j] + t; // '% q' not strictly needed
209 }
210 start = start + 2 * len;
211 }
212 len >>= 1;
213 }
214 }
215
216 /// Algorithm 42 NTTโ1(๐ค)ฬ
217 /// Computes the inverse of the NTT.
218 /// Input: ฬฬ ฬ ๐ค = (๐ค\[0], โฆ , ๐ค\[255]) โ ๐๐.
219 /// Output: Polynomial ๐ค(๐) = โ255
220 /// ๐=0 ๐ค๐๐๐ โ ๐
๐
221 pub(crate) fn inv_ntt(&mut self) {
222 // A core input condition on the InverseNTT is that every input
223 // coefficient must satisfy |๐ค| < ๐ for the sums to stay within an i32.
224 // The reason this is required is because Algorithm 42's butterflies (steps 13-14)
225 // run 8 levels without reducing, leading to an i32 overflow if the input was out-of-range.
226 // Callers must pass them through `reduce32` first.
227 // Note that only a crafted input, such as (Wycheproof mldsa_87_verify tcId 240/241) will trigger this.
228 debug_assert!(
229 self.coeffs.iter().all(|c| c.abs() < q),
230 "inv_ntt input coefficient not reduced below q; call reduce32() first"
231 );
232
233 let mut m: usize = N;
234 let mut len: usize = 1;
235
236 while len < N {
237 let mut start: usize = 0;
238 while start < N {
239 m -= 1;
240 let z = (-1) * ZETAS[m];
241
242 // j = start;
243 // while j < start + len {
244 for j in start..start + len {
245 // ๐ก โ ๐ค๐
246 let t: i32 = self[j];
247
248 // ๐ค๐ โ (๐ก + ๐ค๐+๐๐๐) mod ๐
249 self[j] = t + self[j + len];
250
251 // ๐ค๐+๐๐๐ โ (๐ก โ ๐ค๐+๐๐๐) mod ๐
252 self[j + len] = t - self[j + len];
253
254 // ๐ค๐+๐๐๐ โ (๐ง โ
๐ค๐+๐๐๐) mod ๐
255 self[j + len] = montgomery_reduce(z as i64 * self[j + len] as i64);
256 }
257 start = start + 2 * len; // could be optimized to save the multiply-by-two since j finishes as `start + len`. That said 2* is just << 1, which is basically free.
258 }
259 len <<= 1;
260 }
261
262 // f = 256^-1 mod q
263 // const f: i64 = 8347681;
264 // bc-java uses this value rather than the one in FIPS 204
265 const f: i64 = 41978;
266 for j in 0..N {
267 // equiv. to the global constant N
268 self[j] = montgomery_reduce(f * self[j] as i64);
269 }
270 }
271
272 pub(crate) fn use_hint<const GAMMA2: i32>(&mut self, h: &Polynomial) {
273 for i in 0..N {
274 self[i] = use_hint::<GAMMA2>(self[i], h[i]);
275 }
276 }
277}
278
279/// FIPS 204 Algorithm 49
280/// As described in FIPS 204 Appendix A, montgomery reduction allows for efficient computation
281/// of expressions of the form c = a * b (mod q).
282/// The output is not necessarily less than q in absolute value, but it is less than 2q in absolute value
283pub(crate) fn montgomery_reduce(a: i64) -> i32 {
284 debug_assert!(a > -((q as i64) << 31) && a < ((q as i64) << 31));
285
286 // 2: ๐ก โ ((๐ mod 2^32) โ
QINV) mod 2^32
287 let t: i32 = (a as i32).wrapping_mul(q_inv);
288
289 // 3: ๐ โ (๐ โ ๐ก โ
๐)/2^32
290 ((a - ((t as i64) * (q as i64))) >> 32) as i32
291}
292
293pub(crate) fn conditional_add_q(a: i32) -> i32 {
294 a + ((a >> 31) & q)
295}
296
297/// Plain (non-Montgomery) reduction: for ๐ โค 2^31 โ 2^22 โ 1 returns ๐ โก ๐ (mod ๐) with |๐| โค 6283008 < ๐.
298/// `reduce32` in the reference implementation (pq-crystals/dilithium, ref/reduce.c).
299///
300/// This is not in FIPS 204 since it assumes all arithmetic is done mod q.
301/// In this implementation, sums of Montgomery products are left unreduced, but `inv_ntt` (Algorithm 42)
302/// needs |input| < ๐ to stay within an `i32`, so this is applied to those sums first. Omitting it before
303/// the verifier's NTTโปยน is exploitable (<https://eprint.iacr.org/2026/1032>, Wycheproof mldsa_87_verify tcId 240/241).
304pub(crate) fn reduce32(a: i32) -> i32 {
305 // The reference implementation's stated input bound; above it `a + 2^22` overflows.
306 debug_assert!(a <= i32::MAX - (1 << 22));
307 let t = (a + (1 << 22)) >> 23;
308 a - t * q
309}
310
311#[test]
312/// These are the results it's giving; I'm not sure if these are "correct" or not.
313fn test_conditional_add_q() {
314 assert_eq!(conditional_add_q(-q - 1), -1);
315 assert_eq!(conditional_add_q(-q), 0);
316 assert_eq!(conditional_add_q(-q - 2), -2);
317 assert_eq!(conditional_add_q(-q + 1), 1);
318 assert_eq!(conditional_add_q(-1), q - 1);
319 assert_eq!(conditional_add_q(0), 0);
320 assert_eq!(conditional_add_q(1), 1);
321 assert_eq!(conditional_add_q(q - 1), q - 1);
322 assert_eq!(conditional_add_q(q), q);
323 assert_eq!(conditional_add_q(q + 1), q + 1);
324}
325
326/// Constants for NTT
327const ZETAS: [i32; 256] = [
328 0, 25847, -2608894, -518909, 237124, -777960, -876248, 466468, 1826347, 2353451, -359251,
329 -2091905, 3119733, -2884855, 3111497, 2680103, 2725464, 1024112, -1079900, 3585928, -549488,
330 -1119584, 2619752, -2108549, -2118186, -3859737, -1399561, -3277672, 1757237, -19422, 4010497,
331 280005, 2706023, 95776, 3077325, 3530437, -1661693, -3592148, -2537516, 3915439, -3861115,
332 -3043716, 3574422, -2867647, 3539968, -300467, 2348700, -539299, -1699267, -1643818, 3505694,
333 -3821735, 3507263, -2140649, -1600420, 3699596, 811944, 531354, 954230, 3881043, 3900724,
334 -2556880, 2071892, -2797779, -3930395, -1528703, -3677745, -3041255, -1452451, 3475950,
335 2176455, -1585221, -1257611, 1939314, -4083598, -1000202, -3190144, -3157330, -3632928, 126922,
336 3412210, -983419, 2147896, 2715295, -2967645, -3693493, -411027, -2477047, -671102, -1228525,
337 -22981, -1308169, -381987, 1349076, 1852771, -1430430, -3343383, 264944, 508951, 3097992,
338 44288, -1100098, 904516, 3958618, -3724342, -8578, 1653064, -3249728, 2389356, -210977, 759969,
339 -1316856, 189548, -3553272, 3159746, -1851402, -2409325, -177440, 1315589, 1341330, 1285669,
340 -1584928, -812732, -1439742, -3019102, -3881060, -3628969, 3839961, 2091667, 3407706, 2316500,
341 3817976, -3342478, 2244091, -2446433, -3562462, 266997, 2434439, -1235728, 3513181, -3520352,
342 -3759364, -1197226, -3193378, 900702, 1859098, 909542, 819034, 495491, -1613174, -43260,
343 -522500, -655327, -3122442, 2031748, 3207046, -3556995, -525098, -768622, -3595838, 342297,
344 286988, -2437823, 4108315, 3437287, -3342277, 1735879, 203044, 2842341, 2691481, -2590150,
345 1265009, 4055324, 1247620, 2486353, 1595974, -3767016, 1250494, 2635921, -3548272, -2994039,
346 1869119, 1903435, -1050970, -1333058, 1237275, -3318210, -1430225, -451100, 1312455, 3306115,
347 -1962642, -1279661, 1917081, -2546312, -1374803, 1500165, 777191, 2235880, 3406031, -542412,
348 -2831860, -1671176, -1846953, -2584293, -3724270, 594136, -3776993, -2013608, 2432395, 2454455,
349 -164721, 1957272, 3369112, 185531, -1207385, -3183426, 162844, 1616392, 3014001, 810149,
350 1652634, -3694233, -1799107, -3038916, 3523897, 3866901, 269760, 2213111, -975884, 1717735,
351 472078, -426683, 1723600, -1803090, 1910376, -1667432, -1104333, -260646, -3833893, -2939036,
352 -2235985, -420899, -2286327, 183443, -976891, 1612842, -3545687, -554416, 3919660, -48306,
353 -1362209, 3937738, 1400424, -846154, 1976782,
354];
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 #[test]
360 fn test_reduce32() {
361 // congruent to the input mod q, and within the reference implementation's stated output range
362 for &a in &[
363 0,
364 1,
365 -1,
366 q - 1,
367 q,
368 q + 1,
369 -q,
370 -q - 1,
371 8 * q,
372 -8 * q,
373 6283008,
374 -6283008,
375 i32::MIN,
376 i32::MAX - (1 << 22),
377 ] {
378 let r = reduce32(a);
379 assert!((-6283008..=6283008).contains(&r), "reduce32({a}) = {r} out of range");
380 assert_eq!(
381 (r as i64 - a as i64).rem_euclid(q as i64),
382 0,
383 "reduce32({a}) = {r} not congruent"
384 );
385 assert!(r.abs() < q);
386 }
387 // the largest sum inv_ntt may see: (l + 1) products each in (-q, q), for l = 7
388 assert!(reduce32(8 * q - 8).abs() < q);
389 assert!(reduce32(-(8 * q - 8)).abs() < q);
390 }
391}