Skip to main content

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}