dcrypt_algorithms/poly/ntt/mod.rs
1//! Number Theoretic Transform Implementation
2//!
3//! Generic NTT/iNTT for polynomials over finite fields, with full
4//! FIPS-204 compliance for ML-DSA and a generic cyclic-transform fallback.
5//!
6//! ## ML-DSA (FIPS-204)
7//! - Forward NTT: Algorithm 41 (DIF with standard domain I/O)
8//! - Inverse NTT: Algorithm 42 (GS with standard domain I/O)
9//! - Twiddle factors: Precomputed in Montgomery form (ζ·R mod q)
10//! - Butterfly differences: Kept in [0, 2Q) range as per spec
11//! - Pointwise multiplication: Standard domain multiplication
12//!
13//! ## Generic fallback
14//! - Cooley-Tukey NTT with on-the-fly twiddle computation
15//! - Full Montgomery domain processing
16//! - Pointwise multiplication: Montgomery domain multiplication
17
18use super::params::{Modulus, NttModulus, PostInvNtt};
19use super::polynomial::Polynomial;
20use crate::error::{Error, Result};
21
22/// Modular exponentiation in standard domain
23#[inline(always)]
24fn pow_mod<M: Modulus>(mut base: u32, mut exp: u32) -> u32 {
25 let mut acc: u32 = 1;
26 while exp != 0 {
27 if (exp & 1) == 1 {
28 acc = ((acc as u64 * base as u64) % M::Q as u64) as u32;
29 }
30 base = ((base as u64 * base as u64) % M::Q as u64) as u32;
31 exp >>= 1;
32 }
33 acc
34}
35
36/// Forward Number Theoretic Transform
37pub trait NttOperator<M: NttModulus> {
38 /// Performs forward NTT on polynomial in-place
39 ///
40 /// # ML-DSA (FIPS-204)
41 /// - Implements Algorithm 41 (DIF)
42 /// - Input: coefficients in standard domain
43 /// - Output: coefficients in standard domain
44 ///
45 /// # Generic fallback
46 /// - Implements Cooley-Tukey NTT
47 /// - Converts to Montgomery domain internally
48 fn ntt(poly: &mut Polynomial<M>) -> Result<()>;
49}
50
51/// Inverse Number Theoretic Transform
52pub trait InverseNttOperator<M: NttModulus> {
53 /// Performs inverse NTT on polynomial in-place
54 ///
55 /// # ML-DSA (FIPS-204)
56 /// - Implements Algorithm 42 (GS)
57 /// - Input: coefficients in standard domain
58 /// - Output: standard or Montgomery domain based on POST_INVNTT_MODE
59 ///
60 /// # Generic fallback
61 /// - Implements Cooley-Tukey inverse NTT
62 /// - Scales by N^(-1) and converts back to standard domain
63 fn inv_ntt(poly: &mut Polynomial<M>) -> Result<()>;
64}
65
66/// Cooley-Tukey NTT implementation
67pub struct CooleyTukeyNtt;
68
69/// Montgomery reduction: computes a * R^-1 mod Q
70///
71/// For a ∈ [0, Q·R), returns a·R^(-1) mod Q in [0, Q)
72#[inline(always)]
73pub fn montgomery_reduce<M: NttModulus>(a: u64) -> u32 {
74 let q = M::Q as u64;
75 let neg_qinv = M::NEG_QINV as u64;
76
77 // Compute m = (a * NEG_QINV) mod 2^32
78 let m = ((a as u32) as u64).wrapping_mul(neg_qinv) & 0xFFFFFFFF;
79 // Compute t = (a + m * q) >> 32
80 let t = a.wrapping_add(m.wrapping_mul(q)) >> 32;
81
82 // Conditional reduction
83 let result = t as u32;
84 let mask = ((result >= M::Q) as u32).wrapping_neg();
85 result.wrapping_sub(M::Q & mask)
86}
87
88/// Reduce any u32 to [0, Q)
89/// Handles both normal range and wrapped values from underflow
90#[inline]
91fn reduce_to_q<M: Modulus>(x: u32) -> u32 {
92 // Fast path for common case (x < 4Q)
93 let mut y = x;
94 y -= M::Q & ((y >= M::Q) as u32).wrapping_neg();
95 y -= M::Q & ((y >= M::Q) as u32).wrapping_neg();
96
97 if y < M::Q {
98 return y;
99 }
100
101 // Barrett reduction for large/wrapped values
102 let (mu, k) = if M::BARRETT_MU != 0 {
103 (M::BARRETT_MU, M::BARRETT_K)
104 } else {
105 // Dynamic computation for moduli without precomputed constants
106 let log_q = 64 - (M::Q as u64).leading_zeros(); // FIXED: Removed unnecessary cast
107 let k = log_q + 32;
108 let mu = (1u128 << k) / M::Q as u128; // FIXED: Removed unnecessary cast
109 (mu, k)
110 };
111
112 let x_wide = y as u128;
113 let q = ((x_wide * mu) >> k) as u32;
114 let mut r = y.wrapping_sub(q.wrapping_mul(M::Q));
115
116 r = r.wrapping_sub(M::Q & ((r >= M::Q) as u32).wrapping_neg());
117 r
118}
119
120/// Montgomery multiplication: a * b * R^-1 mod Q
121/// Accepts extended range inputs (e.g., [0, 9Q)) to preserve sign encoding
122#[inline(always)]
123fn montgomery_mul<M: NttModulus>(a: u32, b: u32) -> u32 {
124 montgomery_reduce::<M>((a as u64) * (b as u64))
125}
126
127/// Modular addition with full reduction
128#[inline(always)]
129fn add_mod<M: Modulus>(a: u32, b: u32) -> u32 {
130 ((a as u64 + b as u64) % M::Q as u64) as u32
131}
132
133/// Fast modular addition for inputs < Q
134#[inline(always)]
135fn add_mod_fast<M: Modulus>(a: u32, b: u32) -> u32 {
136 let s = a + b;
137 let mask = ((s >= M::Q) as u32).wrapping_neg();
138 s - (M::Q & mask)
139}
140
141/// Fast modular subtraction for inputs < Q
142#[inline(always)]
143fn sub_mod_fast<M: Modulus>(a: u32, b: u32) -> u32 {
144 let t = a.wrapping_add(M::Q).wrapping_sub(b);
145 let mask = ((t >= M::Q) as u32).wrapping_neg();
146 t - (M::Q & mask)
147}
148
149/// Modular subtraction returning [0, 2Q)
150/// Used in FIPS-204 butterflies to preserve sign information
151#[inline(always)]
152fn sub_mod_upto_2q<M: Modulus>(a: u32, b: u32) -> u32 {
153 a.wrapping_add(M::Q).wrapping_sub(b)
154}
155
156/// Convert standard domain to Montgomery domain
157#[inline(always)]
158fn to_montgomery<M: NttModulus>(val: u32) -> u32 {
159 ((val as u64 * M::MONT_R as u64) % M::Q as u64) as u32
160}
161
162impl<M: NttModulus> NttOperator<M> for CooleyTukeyNtt {
163 fn ntt(poly: &mut Polynomial<M>) -> Result<()> {
164 let n = M::N;
165 if n & (n - 1) != 0 {
166 return Err(Error::Parameter {
167 name: "NTT".into(),
168 reason: "Polynomial degree must be a power of 2".into(),
169 });
170 }
171
172 let coeffs = poly.as_mut_coeffs_slice();
173 let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
174
175 if is_ml_dsa {
176 // FIPS-204 Algorithm 41: Forward NTT
177 // Decimation-in-Frequency (DIF) with row-major twiddle traversal
178 // Input: standard domain, Output: standard domain
179 let mut k = 0;
180 let mut len = n / 2; // Start at 128 for N=256
181
182 while len >= 1 {
183 // Row-major (block-first) iteration matches twiddle table order
184 for start in (0..n).step_by(2 * len) {
185 let zeta = M::ZETAS[k]; // ζ·R mod q (Montgomery form)
186 k += 1;
187
188 for j in start..start + len {
189 let a = coeffs[j];
190 let b = coeffs[j + len];
191
192 // FIPS-204 DIF butterfly:
193 // t = ζ * b (Montgomery mul with ζ·R gives standard domain)
194 let t = montgomery_mul::<M>(b, zeta);
195 // a' = a + t mod q
196 coeffs[j] = add_mod::<M>(a, t);
197 // b' = a - t + Q (kept in [0, 2Q) per Algorithm 41)
198 coeffs[j + len] = sub_mod_upto_2q::<M>(a, t);
199 }
200 }
201
202 len >>= 1;
203 }
204
205 // Reduce all coefficients to [0, Q) for ML-DSA compatibility
206 for c in coeffs.iter_mut() {
207 *c = reduce_to_q::<M>(*c);
208 }
209 } else {
210 // Generic cyclic NTT.
211 for c in coeffs.iter_mut() {
212 *c = to_montgomery::<M>(*c);
213 }
214
215 let mut len = 1_usize;
216 while len < n {
217 let exp = n / (len << 1);
218 let root_std = pow_mod::<M>(M::ZETA, exp as u32);
219 let root_mont = to_montgomery::<M>(root_std);
220
221 for start in (0..n).step_by(len << 1) {
222 let mut w_mont = M::MONT_R;
223
224 for j in 0..len {
225 let u = coeffs[start + j];
226 let v = montgomery_mul::<M>(coeffs[start + j + len], w_mont);
227
228 coeffs[start + j] = add_mod_fast::<M>(u, v);
229 coeffs[start + j + len] = sub_mod_fast::<M>(u, v);
230
231 w_mont = montgomery_mul::<M>(w_mont, root_mont);
232 }
233 }
234 len <<= 1;
235 }
236 // Keep coefficients in Montgomery form for the inverse transform.
237 }
238
239 Ok(())
240 }
241}
242
243impl<M: NttModulus> InverseNttOperator<M> for CooleyTukeyNtt {
244 fn inv_ntt(poly: &mut Polynomial<M>) -> Result<()> {
245 let n = M::N;
246 if n & (n - 1) != 0 {
247 return Err(Error::Parameter {
248 name: "Inverse NTT".into(),
249 reason: "Polynomial degree must be a power of 2".into(),
250 });
251 }
252
253 let coeffs = poly.as_mut_coeffs_slice();
254 let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
255
256 if is_ml_dsa {
257 // FIPS-204 Algorithm 42: Inverse NTT
258 // Gentleman-Sande (GS) with row-major traversal
259
260 // Pre-condition: ensure coefficients < Q for GS butterflies
261 for c in coeffs.iter_mut() {
262 *c = reduce_to_q::<M>(*c);
263 }
264
265 let mut k = M::ZETAS.len(); // Start after last entry
266 let mut len = 1;
267
268 while len < n {
269 // Row-major iteration matching forward NTT structure
270 for start in (0..n).step_by(2 * len) {
271 k -= 1; // Traverse ZETAS in reverse
272
273 // Use negated forward twiddle for inverse
274 let zeta_fwd = M::ZETAS[k];
275 let zeta = if zeta_fwd == 0 { 0 } else { M::Q - zeta_fwd };
276
277 for j in start..start + len {
278 let t = coeffs[j];
279 let u = coeffs[j + len];
280
281 // FIPS-204 GS butterfly:
282 // Line 13: w_j ← w_j + w_{j+len}
283 coeffs[j] = add_mod::<M>(t, u);
284 // Line 14: w_{j+len} ← ζ^(-1) * (w_j - w_{j+len})
285 let diff = sub_mod_upto_2q::<M>(t, u);
286 coeffs[j + len] = montgomery_mul::<M>(diff, zeta);
287 }
288 }
289
290 len <<= 1;
291 }
292
293 // Final reduction before N^(-1) scaling
294 for c in coeffs.iter_mut() {
295 *c = reduce_to_q::<M>(*c);
296 }
297
298 // Scale by N^(-1) in standard domain
299 let n_inv_std = pow_mod::<M>(M::N as u32, M::Q - 2);
300 for c in coeffs.iter_mut() {
301 *c = ((*c as u64 * n_inv_std as u64) % M::Q as u64) as u32;
302 }
303
304 match M::POST_INVNTT_MODE {
305 PostInvNtt::Standard => {} // Already in standard domain
306 PostInvNtt::Montgomery => {
307 // Convert to Montgomery if requested
308 for c in coeffs.iter_mut() {
309 *c = to_montgomery::<M>(*c);
310 }
311 }
312 }
313 } else {
314 // Generic cyclic inverse NTT.
315 let root_inv_std = pow_mod::<M>(M::ZETA, M::Q - 2); // FIXED: Removed unnecessary cast
316
317 let mut len = n >> 1;
318 while len >= 1 {
319 let exp = n / (len << 1);
320 let root_std = pow_mod::<M>(root_inv_std, exp as u32);
321 let root_mont = to_montgomery::<M>(root_std);
322
323 for start in (0..n).step_by(len << 1) {
324 let mut w_mont = M::MONT_R;
325
326 for j in 0..len {
327 let u = coeffs[start + j];
328 let v = coeffs[start + j + len];
329
330 coeffs[start + j] = add_mod_fast::<M>(u, v);
331 coeffs[start + j + len] =
332 montgomery_mul::<M>(sub_mod_fast::<M>(u, v), w_mont);
333
334 w_mont = montgomery_mul::<M>(w_mont, root_mont);
335 }
336 }
337 len >>= 1;
338 }
339
340 // Scale by N^(-1)
341 for c in coeffs.iter_mut() {
342 *c = montgomery_mul::<M>(*c, M::N_INV);
343 }
344
345 if M::POST_INVNTT_MODE == PostInvNtt::Standard {
346 for c in coeffs.iter_mut() {
347 *c = montgomery_reduce::<M>(*c as u64);
348 }
349 }
350 }
351
352 Ok(())
353 }
354}
355
356/// Extension methods for Polynomial
357impl<M: NttModulus> Polynomial<M> {
358 /// Convert polynomial to NTT domain
359 pub fn ntt_inplace(&mut self) -> Result<()> {
360 CooleyTukeyNtt::ntt(self)
361 }
362
363 /// Convert polynomial from NTT domain
364 pub fn from_ntt_inplace(&mut self) -> Result<()> {
365 CooleyTukeyNtt::inv_ntt(self)
366 }
367
368 /// Pointwise multiplication in NTT domain
369 ///
370 /// Both polynomials must already be in NTT domain.
371 /// For ML-DSA: inputs/output in standard domain (post-NTT)
372 /// For the generic fallback: inputs/output in Montgomery domain
373 pub fn ntt_mul(&self, other: &Self) -> Self {
374 let mut result = Self::zero();
375 let n = M::N;
376 let is_ml_dsa = !M::ZETAS.is_empty(); // FIXED: Use is_empty()
377
378 if is_ml_dsa {
379 // ML-DSA: coefficients are in standard domain after NTT
380 // Use standard multiplication
381 for i in 0..n {
382 result.coeffs[i] =
383 ((self.coeffs[i] as u64 * other.coeffs[i] as u64) % M::Q as u64) as u32;
384 }
385 } else {
386 // Generic coefficients are in Montgomery domain after NTT.
387 // Use Montgomery multiplication to keep result in Montgomery domain
388 for i in 0..n {
389 result.coeffs[i] = montgomery_mul::<M>(self.coeffs[i], other.coeffs[i]);
390 }
391 }
392
393 result
394 }
395}
396
397#[cfg(test)]
398mod tests;