Skip to main content

bouncycastle_mldsa/
polynomial.rs

1//! Represents a polynomial over the ML-DSA ring.
2
3use crate::aux_functions::{
4    ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, reduce32,
5};
6use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q};
7use core::ops::{Index, IndexMut};
8
9/// A polynomial over the ML-DSA ring.
10///
11/// Dev note: The following structure does not necessarily need to be declared as public.
12/// There is no real scenario where this function needs to be called directly.
13/// However, in order to test the Debug and Display traits, it is necessary to use STD, so those
14/// can't be tested from inline tests in this file and the real unit tests are in a different crate.
15/// That's the reason why pub is used.
16///
17/// # 🚨 Security 🚨
18/// Polynomials themselves are not inherently secret since sometimes they are part of public keys
19/// and sometimes private keys.
20/// It is the responsibility of the caller to wrap sensitive instances in `Secret<Polynomial>`.
21#[derive(Clone, Copy)]
22pub struct Polynomial {
23    pub(crate) coeffs: [i32; N],
24}
25
26/// Convenience function to avoid ".0" all over the place.
27impl Index<usize> for Polynomial {
28    type Output = i32;
29
30    fn index(&self, index: usize) -> &Self::Output {
31        &self.coeffs[index]
32    }
33}
34/// Convenience function to avoid ".0" all over the place.
35impl IndexMut<usize> for Polynomial {
36    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
37        &mut self.coeffs[index]
38    }
39}
40
41impl Polynomial {
42    /// Create a new polynomial with all coefficients set to zero.
43    pub const fn new() -> Self {
44        Self { coeffs: [0i32; N] }
45    }
46
47    pub(crate) fn conditional_add_q(&mut self) {
48        for x in self.coeffs.iter_mut() {
49            *x = conditional_add_q(*x);
50        }
51    }
52
53    pub(crate) fn reduce(&mut self) {
54        for i in 0..N {
55            self[i] = montgomery_reduce(self[i] as i64);
56        }
57    }
58
59    /// Reduces every coefficient to |𝑐| < π‘ž without changing its residue. See [`reduce32`] for when
60    /// this is required; note that [`Self::reduce`] is a *Montgomery* reduction and is not a substitute.
61    pub(crate) fn reduce32(&mut self) {
62        for x in self.coeffs.iter_mut() {
63            *x = reduce32(*x);
64        }
65    }
66
67    /// Algorithm 44 AddNTT(π‘Ž, 𝑏)Μ‚
68    /// Computes the sum a + 𝑏 of two elements π‘Ž, 𝑏 ∈ π‘‡π‘ž.
69    /// Note: result could be up to 2q.
70    pub(crate) fn add_ntt(&mut self, w: &Self) {
71        for i in 0..N {
72            self[i] += w[i];
73        }
74    }
75
76    pub(crate) fn sub(&mut self, w: &Self) {
77        for i in 0..N {
78            self[i] -= w[i];
79        }
80    }
81
82    pub(crate) fn high_bits<const GAMMA2: i32>(&self) -> Self {
83        let mut w = Self::new();
84        for i in 0..N {
85            w[i] = high_bits::<GAMMA2>(self[i]);
86        }
87
88        w
89    }
90
91    pub(crate) fn low_bits<const GAMMA2: i32>(&self) -> Self {
92        let mut w = Self::new();
93        for i in 0..N {
94            w[i] = low_bits::<GAMMA2>(self[i]);
95        }
96
97        w
98    }
99
100    pub(crate) fn check_norm<const BOUND: i32>(&self) -> bool {
101        // It is acceptable that this function is not constant-time (returns true early)
102        // The reason being because it is used in a rejection loop.
103        // That is, the early quit here leads to rejection, dropping the secret values and
104        // continuing to the top of the rejection loop with generating new secret values,
105        // or failing the signature validation.
106        // So the i32 that we just checked in a non-constant-time manner is about to get thrown away.
107
108        // Note: this formulation of the check_norm function usually requires this bounds check
109        //  if bound > (q - 1) / 8 {
110        //     return true;
111        //  }
112        // but since BOUND is a constant here, a debug_assert is performed to make sure the value is what we expect.
113        debug_assert!(BOUND <= (q - 1) / 8);
114
115        let mut t: i32;
116        for x in self.coeffs.iter() {
117            t = *x >> 31;
118            t = *x - (t & (2 * *x));
119
120            if t >= BOUND {
121                return true;
122            }
123        }
124        false
125    }
126
127    pub(crate) fn shift_left<const d: i32>(&mut self) {
128        for x in self.coeffs.iter_mut() {
129            *x <<= d;
130        }
131    }
132
133    /// Creates the hint vector, and also returns its hamming weight (i.e. the number of 1's).
134    pub(crate) fn make_hint<const GAMMA2: i32>(&self, r: &Self) -> (Self, i32) {
135        let mut out = Polynomial::new();
136        let mut count = 0i32;
137        for i in 0..N {
138            let x = make_hint::<GAMMA2>(self[i], r[i]);
139            out[i] = x;
140
141            // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch
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        let mut r = [0u8; POLY_W1_PACKED_LEN];
150
151        match POLY_W1_PACKED_LEN {
152            MLDSA44_POLY_W1_PACKED_LEN => {
153                for i in 0..N / 4 {
154                    r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8);
155                    r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8);
156                    r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8);
157                }
158            }
159            // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value
160            MLDSA65_POLY_W1_PACKED_LEN => {
161                for i in 0..N / 2 {
162                    r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8;
163                }
164            }
165            _ => {
166                unreachable!()
167            }
168        }
169
170        r
171    }
172
173    /// Algorithm 41 NTT(𝑀)
174    /// Computes the NTT.
175    /// Input: Polynomial 𝑀(𝑋) = Ξ£_{j=0}^{255} 𝑀𝑗𝑋𝑗 ∈ π‘…π‘ž.
176    /// Output: 𝑀_hat = (𝑀_hat\[0], ..., 𝑀_hat\[255]) ∈ π‘‡π‘ž.
177    ///
178    /// Note: by convention, variables holding the output of the NTT function should be named "_hat"
179    /// to indicate that they are in the NTT domain (sometimes called the frequency domain), not the natural domain.
180    /// Usage of the rust type system to enforce this is arguably unnecessary, since that's what the NIST
181    /// test vectors are for.
182    ///
183    /// Lazy reduction: the butterfly omits an explicit reduction modulo `q`
184    /// This is safe only because β€–inputβ€–βˆž ≀ q-1 (i.e. intermediates stay below ~5q
185    /// (well within i32) and the input of montgomery_reduce input stays below qΒ·2^{31}) and
186    /// the final result is reduced downstream.
187    pub(crate) fn ntt(&mut self) {
188        let mut m: usize = 0;
189        let mut len: usize = 128;
190
191        while len >= 1 {
192            let mut start: usize = 0;
193            while start < N {
194                m += 1;
195                let z: i32 = ZETAS[m];
196
197                for j in start..start + len {
198                    let t = montgomery_reduce(z as i64 * self[j + len] as i64);
199                    // '% q' not strictly needed cause it gets reduced at some point later.
200                    // Removing it gave +5% in benchmarking
201                    self[j + len] = self[j] - t;
202                    self[j] = self[j] + t; // '% q' not strictly needed
203                }
204                start = start + 2 * len;
205            }
206            len >>= 1;
207        }
208    }
209
210    /// Algorithm 42 NTTβˆ’1(𝑀_hat)
211    /// Computes the inverse of the NTT.
212    /// Input: 𝑀_hat = (𝑀_hat[0], … , 𝑀_hat[255]) ∈ π‘‡π‘ž.
213    /// Output: Polynomial 𝑀(𝑋) = Ξ£_{j=0}^{255} 𝑀𝑗𝑋𝑗 ∈ π‘…π‘ž
214    pub(crate) fn inv_ntt(&mut self) {
215        // A core input condition on the InverseNTT is that every input
216        // coefficient must satisfy |𝑀| < π‘ž for the sums to stay within an i32.
217        // The reason this is required is because Algorithm 42's butterflies (steps 13-14)
218        // run 8 levels without reducing, leading to an i32 overflow if the input was out-of-range.
219        // Callers must pass them through `reduce32` first.
220        // Note that only a crafted input, such as (Wycheproof mldsa_87_verify tcId 240/241) will trigger this.
221        debug_assert!(
222            self.coeffs.iter().all(|c| c.abs() < q),
223            "inv_ntt input coefficient not reduced below q; call reduce32() first"
224        );
225
226        let mut m: usize = N;
227        let mut len: usize = 1;
228
229        while len < N {
230            let mut start: usize = 0;
231            while start < N {
232                m -= 1;
233                let z = (-1) * ZETAS[m];
234
235                // j = start;
236                // while j < start + len {
237                for j in start..start + len {
238                    // 𝑑 ← 𝑀𝑗
239                    let t: i32 = self[j];
240
241                    // 𝑀𝑗 ← (𝑑 + 𝑀𝑗+𝑙𝑒𝑛) mod π‘ž
242                    self[j] = t + self[j + len];
243
244                    // 𝑀𝑗+𝑙𝑒𝑛 ← (𝑑 βˆ’ 𝑀𝑗+𝑙𝑒𝑛) mod π‘ž
245                    self[j + len] = t - self[j + len];
246
247                    // 𝑀𝑗+𝑙𝑒𝑛 ← (𝑧 β‹… 𝑀𝑗+𝑙𝑒𝑛) mod π‘ž
248                    self[j + len] = montgomery_reduce(z as i64 * self[j + len] as i64);
249                }
250                start = start + 2 * len;
251                // could be optimized to save the multiply-by-two since j finishes as `start + len`.
252                // That said 2* is just << 1, which is basically free.
253            }
254            len <<= 1;
255        }
256
257        // Final 1/256 normalization, in Montgomery form to match the montgomery_reduce
258        // bookkeeping used by every butterfly above (each contributes a 2^-32 factor).
259        // Note: f != 256^-1 mod q = 8347681. That value is only correct
260        // applied as a plain multiply (FIPS 204 Alg 42: w_j <- f * w_j mod q).
261        // Here we apply f via montgomery_reduce(f * w_j), so the constant is the
262        // Montgomery-domain form:  f = mont^2 / 256 mod q = 41978,  mont = 2^32 mod q.
263        // Do NOT substitute 8347681, as it is invalid through montgomery_reduce.
264        const f: i64 = 41978;
265        for j in 0..N {
266            // equiv. to the global constant N
267            self[j] = montgomery_reduce(f * self[j] as i64);
268        }
269    }
270}