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,
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 /// Algorithm 44 AddNTT(π, π)Μ
60 /// Computes the sum a + π of two elements π, π β ππ.
61 /// Note: result could be up to 2q.
62 pub(crate) fn add_ntt(&mut self, w: &Self) {
63 for i in 0..N {
64 self[i] += w[i];
65 }
66 }
67
68 pub(crate) fn sub(&mut self, w: &Self) {
69 for i in 0..N {
70 self[i] -= w[i];
71 }
72 }
73
74 pub(crate) fn high_bits<const GAMMA2: i32>(&self) -> Self {
75 let mut w = Self::new();
76 for i in 0..N {
77 w[i] = high_bits::<GAMMA2>(self[i]);
78 }
79
80 w
81 }
82
83 pub(crate) fn low_bits<const GAMMA2: i32>(&self) -> Self {
84 let mut w = Self::new();
85 for i in 0..N {
86 w[i] = low_bits::<GAMMA2>(self[i]);
87 }
88
89 w
90 }
91
92 pub(crate) fn check_norm<const BOUND: i32>(&self) -> bool {
93 // It is acceptable that this function is not constant-time (returns true early)
94 // The reason being because it is used in a rejection loop.
95 // That is, the early quit here leads to rejection, dropping the secret values and
96 // continuing to the top of the rejection loop with generating new secret values,
97 // or failing the signature validation.
98 // So the i32 that we just checked in a non-constant-time manner is about to get thrown away.
99
100 // Note: this formulation of the check_norm function usually requires this bounds check
101 // if bound > (q - 1) / 8 {
102 // return true;
103 // }
104 // but since BOUND is a constant here, a debug_assert is performed to make sure the value is what we expect.
105 debug_assert!(BOUND <= (q - 1) / 8);
106
107 let mut t: i32;
108 for x in self.coeffs.iter() {
109 t = *x >> 31;
110 t = *x - (t & (2 * *x));
111
112 if t >= BOUND {
113 return true;
114 }
115 }
116 false
117 }
118
119 pub(crate) fn shift_left<const d: i32>(&mut self) {
120 for x in self.coeffs.iter_mut() {
121 *x <<= d;
122 }
123 }
124
125 /// Creates the hint vector, and also returns its hamming weight (i.e. the number of 1's).
126 pub(crate) fn make_hint<const GAMMA2: i32>(&self, r: &Self) -> (Self, i32) {
127 let mut out = Polynomial::new();
128 let mut count = 0i32;
129 for i in 0..N {
130 let x = make_hint::<GAMMA2>(self[i], r[i]);
131 out[i] = x;
132
133 // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch
134 count += x;
135 }
136
137 (out, count)
138 }
139
140 pub(crate) fn w1_encode<const POLY_W1_PACKED_LEN: usize>(&self) -> [u8; POLY_W1_PACKED_LEN] {
141 let mut r = [0u8; POLY_W1_PACKED_LEN];
142
143 match POLY_W1_PACKED_LEN {
144 MLDSA44_POLY_W1_PACKED_LEN => {
145 for i in 0..N / 4 {
146 r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8);
147 r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8);
148 r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8);
149 }
150 }
151 // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value
152 MLDSA65_POLY_W1_PACKED_LEN => {
153 for i in 0..N / 2 {
154 r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8;
155 }
156 }
157 _ => {
158 unreachable!()
159 }
160 }
161
162 r
163 }
164
165 /// Algorithm 41 NTT(π€)
166 /// Computes the NTT.
167 /// Input: Polynomial π€(π) = Ξ£_{j=0}^{255} π€πππ β π
π.
168 /// Output: π€_hat = (π€_hat\[0], ..., π€_hat\[255]) β ππ.
169 ///
170 /// Note: by convention, variables holding the output of the NTT function should be named "_hat"
171 /// to indicate that they are in the NTT domain (sometimes called the frequency domain), not the natural domain.
172 /// Usage of the rust type system to enforce this is arguably unnecessary, since that's what the NIST
173 /// test vectors are for.
174 ///
175 /// Lazy reduction: the butterfly omits an explicit reduction modulo `q`
176 /// This is safe only because βinputββ β€ q-1 (i.e. intermediates stay below ~5q
177 /// (well within i32) and the input of montgomery_reduce input stays below qΒ·2^{31}) and
178 /// the final result is reduced downstream.
179 pub(crate) fn ntt(&mut self) {
180 let mut m: usize = 0;
181 let mut len: usize = 128;
182
183 while len >= 1 {
184 let mut start: usize = 0;
185 while start < N {
186 m += 1;
187 let z: i32 = ZETAS[m];
188
189 for j in start..start + len {
190 let t = montgomery_reduce(z as i64 * self[j + len] as i64);
191 // '% q' not strictly needed cause it gets reduced at some point later.
192 // Removing it gave +5% in benchmarking
193 self[j + len] = self[j] - t;
194 self[j] = self[j] + t; // '% q' not strictly needed
195 }
196 start = start + 2 * len;
197 }
198 len >>= 1;
199 }
200 }
201
202 /// Algorithm 42 NTTβ1(π€_hat)
203 /// Computes the inverse of the NTT.
204 /// Input: π€_hat = (π€_hat[0], β¦ , π€_hat[255]) β ππ.
205 /// Output: Polynomial π€(π) = Ξ£_{j=0}^{255} π€πππ β π
π
206 pub(crate) fn inv_ntt(&mut self) {
207 let mut m: usize = N;
208 let mut len: usize = 1;
209
210 while len < N {
211 let mut start: usize = 0;
212 while start < N {
213 m -= 1;
214 let z = (-1) * ZETAS[m];
215
216 // j = start;
217 // while j < start + len {
218 for j in start..start + len {
219 // π‘ β π€π
220 let t: i32 = self[j];
221
222 // π€π β (π‘ + π€π+πππ) mod π
223 self[j] = t + self[j + len];
224
225 // π€π+πππ β (π‘ β π€π+πππ) mod π
226 self[j + len] = t - self[j + len];
227
228 // π€π+πππ β (π§ β
π€π+πππ) mod π
229 self[j + len] = montgomery_reduce(z as i64 * self[j + len] as i64);
230 }
231 start = start + 2 * len;
232 // could be optimized to save the multiply-by-two since j finishes as `start + len`.
233 // That said 2* is just << 1, which is basically free.
234 }
235 len <<= 1;
236 }
237
238 // Final 1/256 normalization, in Montgomery form to match the montgomery_reduce
239 // bookkeeping used by every butterfly above (each contributes a 2^-32 factor).
240 // Note: f != 256^-1 mod q = 8347681. That value is only correct
241 // applied as a plain multiply (FIPS 204 Alg 42: w_j <- f * w_j mod q).
242 // Here we apply f via montgomery_reduce(f * w_j), so the constant is the
243 // Montgomery-domain form: f = mont^2 / 256 mod q = 41978, mont = 2^32 mod q.
244 // Do NOT substitute 8347681, as it is invalid through montgomery_reduce.
245 const f: i64 = 41978;
246 for j in 0..N {
247 // equiv. to the global constant N
248 self[j] = montgomery_reduce(f * self[j] as i64);
249 }
250 }
251}