modmath/field.rs
1//! `Field<T, P>` — Montgomery-form modular arithmetic context, parameterized
2//! over the limb personality (`P: Personality`).
3//!
4//! This module owns the *generic* Montgomery surface (`mul`, `exp`, `add`,
5//! `sub`, `reduce`, `into_raw`, `zero`, `one`); curve- or RSA-specific
6//! specializations (`lazy_add`, Fermat `inv`, Solinas reduction, etc.) live
7//! in consumer crates that wrap these types.
8//!
9//! ## Personality parameter
10//!
11//! `Field<T, P>` selects the *algorithm* at the modmath level (variable-
12//! time CIOS with branching finalize vs. CT CIOS with conditional-select
13//! finalize); the personality of `T` itself (e.g. `FixedUInt<W, N, Nct>`
14//! vs `FixedUInt<W, N, Ct>`) selects the *limb-primitive* bodies. In
15//! practice the two personalities co-vary at the construction site —
16//! you build `Field<FixedUInt<W, N, Nct>, Nct>` for verify paths and
17//! `Field<FixedUInt<W, N, Ct>, Ct>` for signing paths. Type aliases
18//! [`FieldCt`] / [`FieldNct`] (and the matching [`ResidueCt`] /
19//! [`ResidueNct`]) are the canonical per-personality spellings — they
20//! read naturally at the call site and sidestep the type-inference
21//! ambiguity that bare `Field::new(modulus)` hits when two `impl`
22//! blocks (Nct/Ct) are both method-resolution candidates.
23//!
24//! Because the Nct and Ct algorithms have **different trait bounds on `T`**
25//! (`T: CiosMontMul` for Nct vs `T: CiosMontMulCt + ConditionallySelectable`
26//! for Ct), the per-personality method bodies live in separate `impl`
27//! blocks rather than dispatching via `match P::TAG`. Only the bounds
28//! shared by both algorithms (precompute, `new`, `zero`, `one`, the
29//! residue brand) live in the common `impl<T, P: Personality>` block.
30//!
31//! ## Branding
32//!
33//! Each `Field<T, P>` instance is implicitly tagged by its borrow lifetime
34//! `'f`, and the residues it produces carry that same brand plus the
35//! personality parameter:
36//!
37//! ```ignore
38//! let field = Field::new(modulus).unwrap();
39//! let r: Residue<'_, U256, Nct> = field.reduce(&seven);
40//! ```
41//!
42//! The borrow checker prevents a `Residue` from outliving its parent
43//! `Field`, and the `P` parameter prevents an Nct residue from being fed
44//! to a Ct method (and vice versa) at compile time. Covariance does not
45//! prevent mixing residues from two `Field` instances built in the same
46//! scope with the same `P` — a known limitation matched by ed25519's
47//! `field.rs`. A generative brand
48//! (`PhantomData<fn(&'f ()) -> &'f ()>` + closure) would close that gap
49//! when a future consumer needs it.
50
51use core::marker::PhantomData;
52
53use const_num_traits::{Ct, Nct, Odd, Personality};
54
55use crate::montgomery::basic_mont::{
56 wide_montgomery_mul, wide_montgomery_mul_acc, wide_montgomery_mul_acc_ct,
57 wide_montgomery_mul_ct, wide_redc, wide_redc_ct,
58};
59use crate::montgomery::{
60 CiosMontMul, CiosMontMulCt, compute_n_prime_newton, compute_r_mod_n, compute_r2_mod_n,
61 type_bit_width,
62};
63use crate::parity::Parity;
64use crate::wide_mul::WideMul;
65
66/// Bound on the value stored in a [`Residue`]. With the `zeroize`
67/// feature this requires `T: Zeroize`; otherwise it's vacuous.
68#[cfg(feature = "zeroize")]
69pub trait MontStorage: zeroize::Zeroize {}
70#[cfg(feature = "zeroize")]
71impl<T: zeroize::Zeroize> MontStorage for T {}
72
73#[cfg(not(feature = "zeroize"))]
74pub trait MontStorage {}
75#[cfg(not(feature = "zeroize"))]
76impl<T> MontStorage for T {}
77
78// ---------------------------------------------------------------------------
79// Field<T, P>
80// ---------------------------------------------------------------------------
81
82/// Montgomery context over modulus `T`, with algorithm choice driven by
83/// the personality marker `P` (defaults to [`Nct`] = variable-time fast
84/// path).
85///
86/// Use `Field<T, Nct>` for operations on **public** data only — signature
87/// verification, RSA public-key encryption, anything whose inputs are not
88/// secret. Use `Field<T, Ct>` (also reachable via the [`FieldCt`] type
89/// alias) for secret-handling paths.
90///
91/// `Clone` is a trivial 4×`T` memcpy — it does NOT re-run the
92/// `compute_r_mod_n` / `compute_r2_mod_n` precompute that `new()` does.
93/// Callers building a `Field` once per key and then reusing it (e.g.
94/// RSA-CRT) should clone the prebuilt instance rather than calling
95/// `new()` again with the same modulus.
96#[derive(Clone, Debug)]
97pub struct Field<T, P: Personality = Nct> {
98 modulus: T,
99 n_prime: T,
100 r_mod_n: T,
101 r2_mod_n: T,
102 _p: PhantomData<fn() -> P>,
103}
104
105/// Alias for the Nct variant of [`Field`]. Equivalent to `Field<T, Nct>`
106/// (matches the default personality). Provided for symmetry with
107/// [`FieldCt`] and to side-step the construction-site type-ambiguity
108/// pitfall — `FieldNct::new(modulus)` resolves unambiguously without
109/// the type-annotation/turbofish friction of `Field::new(modulus)`.
110pub type FieldNct<T> = Field<T, Nct>;
111
112/// Alias for the Ct variant of [`Field`]. Equivalent to `Field<T, Ct>`.
113/// Reads naturally at construction sites and sidesteps the type-inference
114/// ambiguity that bare `Field::new(modulus)` hits — `FieldCt::new(modulus)`
115/// resolves unambiguously because the alias fixes `P = Ct` at the type
116/// level. Symmetric with [`FieldNct`].
117pub type FieldCt<T> = Field<T, Ct>;
118
119/// A value in `Field<T, P>`, stored implicitly in Montgomery form.
120///
121/// The `'f` lifetime brand ties this residue to its parent `Field`; the
122/// `P` parameter ties it to the parent's algorithm personality. The
123/// borrow checker rejects code that uses a residue after its `Field` is
124/// dropped, or that mixes residues across personalities. See module docs
125/// for the covariance caveat.
126#[derive(Clone, Debug, PartialEq, Eq)]
127pub struct Residue<'f, T: MontStorage, P: Personality = Nct> {
128 mont: T,
129 _brand: PhantomData<&'f ()>,
130 _p: PhantomData<fn() -> P>,
131}
132
133#[cfg(feature = "zeroize")]
134impl<'f, T: MontStorage, P: Personality> zeroize::Zeroize for Residue<'f, T, P> {
135 fn zeroize(&mut self) {
136 self.mont.zeroize();
137 }
138}
139
140#[cfg(feature = "zeroize")]
141impl<'f, T: MontStorage, P: Personality> Drop for Residue<'f, T, P> {
142 fn drop(&mut self) {
143 self.mont.zeroize();
144 }
145}
146
147#[cfg(feature = "zeroize")]
148impl<'f, T: MontStorage, P: Personality> zeroize::ZeroizeOnDrop for Residue<'f, T, P> {}
149
150/// Alias for the Nct variant of [`Residue`]. Equivalent to
151/// `Residue<'f, T, Nct>`. Symmetric with [`ResidueCt`].
152pub type ResidueNct<'f, T> = Residue<'f, T, Nct>;
153
154/// Alias for the Ct variant of [`Residue`]. Equivalent to
155/// `Residue<'f, T, Ct>`. Symmetric with [`ResidueNct`].
156pub type ResidueCt<'f, T> = Residue<'f, T, Ct>;
157
158// ---------------------------------------------------------------------------
159// Shared impls (any P)
160// ---------------------------------------------------------------------------
161
162impl<T: MontStorage, P: Personality> Residue<'_, T, P> {
163 /// Returns a reference to the underlying Montgomery-form value.
164 ///
165 /// **Escape hatch.** Intended for downstream specialization layers
166 /// (e.g. `Curve25519Field`) that implement fast paths reading the raw
167 /// limbs. General consumers should not call this — use the methods on
168 /// [`Field`] instead.
169 pub fn mont_value(&self) -> &T {
170 &self.mont
171 }
172}
173
174impl<T, P: Personality> Field<T, P> {
175 /// Construct a `Field` directly from already-computed Montgomery
176 /// parameters. **`const fn` — usable in const initializers.**
177 ///
178 /// Intended for callers whose modulus is statically known at compile
179 /// time (curve constants, PQC parameters, RSA group constants for a
180 /// fixed key, etc.) and who want to expose the Field as a `const`
181 /// associated item or static, rather than paying the runtime
182 /// [`new`](Self::new) precompute on each instantiation.
183 ///
184 /// **The caller is responsible for the correctness of `n_prime`,
185 /// `r_mod_n`, and `r2_mod_n`** — see [`compute_n_prime_newton`],
186 /// [`compute_r_mod_n`], and [`compute_r2_mod_n`] for the algorithms.
187 /// Those helpers are not `const fn` today (their bodies use trait
188 /// method calls on `T`), so const-initializer callers must either
189 /// hand-compute the values, use a build script, or — for primitive
190 /// integers — compute them in a non-const context once at startup
191 /// and cache.
192 ///
193 /// No invariant checking is performed here. Passing inconsistent
194 /// parameters produces a `Field` whose arithmetic methods return
195 /// silently incorrect results.
196 ///
197 /// [`compute_n_prime_newton`]: crate::compute_n_prime_newton
198 /// [`compute_r_mod_n`]: crate::compute_r_mod_n
199 /// [`compute_r2_mod_n`]: crate::compute_r2_mod_n
200 pub const fn from_precomputed(modulus: T, n_prime: T, r_mod_n: T, r2_mod_n: T) -> Self {
201 Self {
202 modulus,
203 n_prime,
204 r_mod_n,
205 r2_mod_n,
206 _p: PhantomData,
207 }
208 }
209}
210
211impl<T, P: Personality> Field<T, P>
212where
213 T: Copy
214 + PartialEq
215 + PartialOrd
216 + const_num_traits::Zero
217 + const_num_traits::One
218 + const_num_traits::WrappingMul
219 + const_num_traits::WrappingAdd
220 + const_num_traits::WrappingSub
221 + const_num_traits::ops::overflowing::OverflowingAdd
222 + core::ops::Add<Output = T>
223 + core::ops::Sub<Output = T>
224 + core::ops::Mul<Output = T>
225 + Parity
226 + MontStorage,
227{
228 /// Construct a new `Field` from an already-proven-odd modulus.
229 ///
230 /// **Infallible.** The `Odd<T>` typestate hoists the "modulus is odd and
231 /// nonzero" precondition to the caller's trust boundary — typically a
232 /// single `Odd::new(p)?` (or `Odd::new(p).unwrap()` for a const modulus)
233 /// at config load. No runtime check inside this constructor, and the
234 /// `panic_fmt` symbol that an `unwrap()` on the old `Option` API would
235 /// have synthesized stays out of the linked binary on embedded targets
236 /// when the boundary check is const-evaluated.
237 ///
238 /// `Odd<T>` covers both the "non-zero" and "odd" halves (zero is even),
239 /// so this also discharges the modulus-nonzero check that [`new`] does.
240 ///
241 /// [`new`]: Self::new
242 pub fn new_odd(modulus: Odd<T>) -> Self {
243 let modulus = modulus.get();
244 let w = type_bit_width::<T>();
245 let n_prime = compute_n_prime_newton(modulus, w);
246 let r_mod_n = compute_r_mod_n(modulus, w);
247 let r2_mod_n = compute_r2_mod_n(r_mod_n, modulus, w);
248 Self {
249 modulus,
250 n_prime,
251 r_mod_n,
252 r2_mod_n,
253 _p: PhantomData,
254 }
255 }
256
257 /// Construct a new `Field` from an already-proven-odd modulus,
258 /// using the **constant-time** precompute path.
259 ///
260 /// Same precompute as [`new_odd`] (Newton's iteration for `N'`,
261 /// repeated modular doublings for `R mod N` and `R² mod N`), but
262 /// the doubling-and-reduction loop in
263 /// [`compute_r_mod_n_ct`](crate::montgomery::compute_r_mod_n_ct)
264 /// avoids value-dependent branches on the modulus.
265 ///
266 /// Use this when `modulus` is secret (e.g. RSA-CRT private primes
267 /// `p`, `q`). For public moduli (ed25519 / Curve25519 / krabipqc),
268 /// [`new_odd`] is faster and equivalent.
269 ///
270 /// Cost vs [`new_odd`]: one extra `wrapping_sub` and one
271 /// `conditional_select` per modular doubling step (`w` per
272 /// precompute call). Negligible against the subsequent field
273 /// operations the precompute amortizes.
274 ///
275 /// [`new_odd`]: Self::new_odd
276 pub fn new_odd_ct(modulus: Odd<T>) -> Self
277 where
278 T: subtle::ConditionallySelectable + subtle::ConstantTimeLess,
279 {
280 let modulus = modulus.get();
281 let w = type_bit_width::<T>();
282 let n_prime = compute_n_prime_newton(modulus, w);
283 let r_mod_n = crate::montgomery::compute_r_mod_n_ct(modulus, w);
284 let r2_mod_n = crate::montgomery::compute_r2_mod_n_ct(r_mod_n, modulus, w);
285 Self {
286 modulus,
287 n_prime,
288 r_mod_n,
289 r2_mod_n,
290 _p: PhantomData,
291 }
292 }
293
294 /// Construct a new `Field` over the given (odd, nonzero) `modulus`.
295 ///
296 /// Returns `None` if `modulus` is zero or even (Montgomery requires odd N).
297 /// Thin wrapper around [`new_odd`] that performs the parity proof at
298 /// runtime. Prefer [`new_odd`] in panic-sensitive paths so the modulus
299 /// proof becomes a one-shot boundary check rather than a returned
300 /// `Option<Self>` the caller must `.unwrap()`.
301 ///
302 /// [`new_odd`]: Self::new_odd
303 pub fn new(modulus: T) -> Option<Self> {
304 Odd::new(modulus).map(Self::new_odd)
305 }
306
307 /// Returns the modulus by reference.
308 ///
309 /// Returning `&T` rather than `T` avoids a memcpy of the full modulus
310 /// (~256 bytes for a 2048-bit carrier) at the call site. Consumers that
311 /// need a `T` by value can copy at the use point.
312 pub fn modulus(&self) -> &T {
313 &self.modulus
314 }
315
316 /// The additive identity (0 in Montgomery form is 0).
317 pub fn zero(&self) -> Residue<'_, T, P> {
318 Residue {
319 mont: T::zero(),
320 _brand: PhantomData,
321 _p: PhantomData,
322 }
323 }
324
325 /// The multiplicative identity (1 in Montgomery form is `R mod N`).
326 pub fn one(&self) -> Residue<'_, T, P> {
327 Residue {
328 mont: self.r_mod_n,
329 _brand: PhantomData,
330 _p: PhantomData,
331 }
332 }
333
334 /// Reconstruct a [`Residue`] from a raw value already in Montgomery form.
335 ///
336 /// **Escape hatch.** Intended for downstream specialization layers that
337 /// persist or compute Montgomery-form values outside this module and
338 /// need to re-attach the brand. The caller must guarantee `mont` is in
339 /// `[0, modulus)` and represents some value `x` such that
340 /// `mont == x * R mod modulus`.
341 pub fn residue_from_mont(&self, mont: T) -> Residue<'_, T, P> {
342 Residue {
343 mont,
344 _brand: PhantomData,
345 _p: PhantomData,
346 }
347 }
348}
349
350// ---------------------------------------------------------------------------
351// Nct-only impls — variable-time CIOS with branching finalize
352// ---------------------------------------------------------------------------
353
354impl<T> Field<T, Nct>
355where
356 T: Copy
357 + PartialEq
358 + PartialOrd
359 + const_num_traits::Zero
360 + const_num_traits::One
361 + const_num_traits::WrappingMul
362 + const_num_traits::WrappingAdd
363 + const_num_traits::WrappingSub
364 + const_num_traits::ops::overflowing::OverflowingAdd
365 + core::ops::Add<Output = T>
366 + core::ops::Sub<Output = T>
367 + core::ops::Mul<Output = T>
368 + Parity
369 + crate::NonCt
370 + MontStorage,
371{
372 /// Convert a raw value `< modulus` (or arbitrary value, which is then
373 /// reduced) to Montgomery form. Returns a brand-tagged [`Residue`].
374 pub fn reduce(&self, raw: &T) -> Residue<'_, T, Nct>
375 where
376 T: WideMul,
377 {
378 let mont = wide_montgomery_mul(*raw, self.r2_mod_n, self.modulus, self.n_prime);
379 Residue {
380 mont,
381 _brand: PhantomData,
382 _p: PhantomData,
383 }
384 }
385
386 /// Convert a [`Residue`] back to its raw `T` representative in `[0, modulus)`.
387 #[allow(clippy::wrong_self_convention)]
388 pub fn into_raw(&self, r: &Residue<'_, T, Nct>) -> T
389 where
390 T: WideMul,
391 {
392 wide_redc(r.mont, T::zero(), self.modulus, self.n_prime)
393 }
394
395 /// Modular addition: `(a + b) mod modulus`.
396 ///
397 /// **The conditional-subtract here is non-negotiable.** Future consumers
398 /// with a modulus narrower than `T::BITS` may be tempted to skip the
399 /// `wrapping_add` + cond-sub path (since `2 * modulus < 2^T::BITS` for
400 /// them, the wraparound never fires). That's a correct optimization, but
401 /// it belongs in a specialization layer (e.g. `Curve25519Field` in the
402 /// ed25519 crate), not in `modmath::Field`. Patches removing the
403 /// wrapping path will break RSA-CRT (full-width modulus, no slack) and
404 /// any other consumer at full type width; ed25519 has slack and uses its
405 /// own lazy variant in `Curve25519Field`.
406 pub fn add(&self, a: &Residue<'_, T, Nct>, b: &Residue<'_, T, Nct>) -> Residue<'_, T, Nct> {
407 let mont = crate::add::basic_mod_add_pr(a.mont, b.mont, self.modulus);
408 Residue {
409 mont,
410 _brand: PhantomData,
411 _p: PhantomData,
412 }
413 }
414
415 /// Modular subtraction: `(a - b) mod modulus`.
416 ///
417 /// Same load-bearing contract as [`add`](Self::add) — the borrow-detect
418 /// branch is required at full type width.
419 pub fn sub(&self, a: &Residue<'_, T, Nct>, b: &Residue<'_, T, Nct>) -> Residue<'_, T, Nct> {
420 let mont = crate::sub::basic_mod_sub_pr(a.mont, b.mont, self.modulus);
421 Residue {
422 mont,
423 _brand: PhantomData,
424 _p: PhantomData,
425 }
426 }
427
428 /// Modular multiplication via CIOS Montgomery multiplication.
429 ///
430 /// CIOS interleaves multiplication and reduction in one pass (~2N² + N
431 /// limb mults vs ~3N² for separate wide-mul + REDC), which dominates the
432 /// inner-loop cost on constrained cores. The functional output is
433 /// identical to `wide_montgomery_mul`.
434 ///
435 /// Marked `#[inline]` deliberately: this is the documented inner-loop
436 /// wrapper for Montgomery exponentiation, the body is a single trait
437 /// method call, and skipping it across crate boundaries costs ~250
438 /// cycles per call under `opt-level="z"` on Cortex-M. Not blanket cargo
439 /// culting — surgical on the actual hot path.
440 #[inline]
441 pub fn mul(&self, a: &Residue<'_, T, Nct>, b: &Residue<'_, T, Nct>) -> Residue<'_, T, Nct>
442 where
443 T: CiosMontMul,
444 {
445 let mont = CiosMontMul::cios_mont_mul(&a.mont, &b.mont, &self.modulus, &self.n_prime);
446 Residue {
447 mont,
448 _brand: PhantomData,
449 _p: PhantomData,
450 }
451 }
452
453 /// Modular exponentiation via square-and-multiply.
454 ///
455 /// `base` is taken as a [`Residue`] (already in Montgomery form); `exp`
456 /// is a raw `T`. The result is a [`Residue`] in Montgomery form.
457 ///
458 /// **Variable-time in `exp`.** The loop iterates `bit_length(exp)` times
459 /// and branches on each bit. Do not call with a secret `exp` — use the
460 /// Ct-variant `Field<T, Ct>::exp` instead.
461 pub fn exp(&self, base: &Residue<'_, T, Nct>, exp: &T) -> Residue<'_, T, Nct>
462 where
463 T: CiosMontMul + core::ops::ShrAssign<usize>,
464 {
465 let mut result = self.r_mod_n;
466 let mut base_var = base.mont;
467 let mut exp_val = *exp;
468 while exp_val > T::zero() {
469 if exp_val.is_odd() {
470 result =
471 CiosMontMul::cios_mont_mul(&result, &base_var, &self.modulus, &self.n_prime);
472 }
473 exp_val >>= 1;
474 if exp_val > T::zero() {
475 base_var =
476 CiosMontMul::cios_mont_mul(&base_var, &base_var, &self.modulus, &self.n_prime);
477 }
478 }
479 Residue {
480 mont: result,
481 _brand: PhantomData,
482 _p: PhantomData,
483 }
484 }
485
486 /// Wide multiply-accumulate: `(acc.0, acc.1) += a.mont * b.mont`.
487 ///
488 /// Brand-tagged wrapper around [`wide_montgomery_mul_acc`]. Pair
489 /// with [`Field::wide_redc`] to close the accumulator after a fused
490 /// inner-product loop. See the free-function for the `N ≤ R/q`
491 /// bound contract.
492 pub fn mul_acc(&self, acc: (T, T), a: &Residue<'_, T, Nct>, b: &Residue<'_, T, Nct>) -> (T, T)
493 where
494 T: WideMul,
495 {
496 wide_montgomery_mul_acc(acc.0, acc.1, a.mont, b.mont)
497 }
498
499 /// Close a wide accumulator into a brand-tagged [`Residue`].
500 pub fn wide_redc(&self, acc: (T, T)) -> Residue<'_, T, Nct>
501 where
502 T: WideMul,
503 {
504 let mont = wide_redc(acc.0, acc.1, self.modulus, self.n_prime);
505 Residue {
506 mont,
507 _brand: PhantomData,
508 _p: PhantomData,
509 }
510 }
511
512 /// Modular inverse via Fermat's little theorem: `a^(modulus − 2)`.
513 ///
514 /// **Requires `modulus` to be prime.** Variable-time over the bits
515 /// of `modulus − 2`. Returns `None` when `a` is the zero residue.
516 pub fn inv_fermat(&self, a: &Residue<'_, T, Nct>) -> Option<Residue<'_, T, Nct>>
517 where
518 T: CiosMontMul + core::ops::ShrAssign<usize>,
519 {
520 if a.mont == T::zero() {
521 return None;
522 }
523 let two = T::one().wrapping_add(T::one());
524 let exp_val = self.modulus.wrapping_sub(two);
525 Some(self.exp(a, &exp_val))
526 }
527
528 /// Modular inverse via extended Euclidean GCD on the raw Mont
529 /// value, then rebrand to Mont form via two Mont multiplies by
530 /// `R^2 mod N`.
531 ///
532 /// Works for any odd modulus (composite is fine). Variable-time —
533 /// do not call with secret inputs; use [`Self::inv_fermat`] for CT
534 /// paths. Returns `None` when `a` is not coprime to modulus.
535 pub fn inv_eea(&self, a: &Residue<'_, T, Nct>) -> Option<Residue<'_, T, Nct>>
536 where
537 T: WideMul + core::ops::Div<Output = T> + core::ops::Sub<Output = T>,
538 {
539 if a.mont == T::zero() {
540 return None;
541 }
542 let raw_inv = crate::inv::basic_mod_inv(a.mont, self.modulus)?;
543 // raw_inv = (a*R)^{-1} = a^{-1} * R^{-1} (residue form).
544 // Two Mont mults by R^2 lift it back to a^{-1} * R = Mont(a^{-1}).
545 let step1 = wide_montgomery_mul(raw_inv, self.r2_mod_n, self.modulus, self.n_prime);
546 let mont = wide_montgomery_mul(step1, self.r2_mod_n, self.modulus, self.n_prime);
547 Some(Residue {
548 mont,
549 _brand: PhantomData,
550 _p: PhantomData,
551 })
552 }
553}
554
555// ---------------------------------------------------------------------------
556// Ct-only impls — CT CIOS with conditional-select finalize
557// ---------------------------------------------------------------------------
558
559impl<'f, T> Residue<'f, T, Ct>
560where
561 T: subtle::ConditionallySelectable + MontStorage,
562{
563 /// Conditionally swap two residues in constant time.
564 ///
565 /// If `choice` is set, `a` and `b` exchange Montgomery-form values;
566 /// otherwise both are left unchanged. The operation is branchless.
567 ///
568 /// This is the primitive used by Montgomery ladders (x25519 scalar
569 /// multiplication, RSA blinded exponentiation). It is the **only**
570 /// residue swap that should appear in such a ladder; `std::mem::swap`
571 /// is not guaranteed to be branchless.
572 pub fn cswap(choice: subtle::Choice, a: &mut Self, b: &mut Self) {
573 T::conditional_swap(&mut a.mont, &mut b.mont, choice);
574 }
575}
576
577impl<'f, T> Residue<'f, T, Ct>
578where
579 T: subtle::ConstantTimeEq + MontStorage,
580{
581 /// Constant-time equality on the underlying Montgomery values.
582 ///
583 /// Use in place of derived `PartialEq` on Ct paths where the
584 /// equality outcome must not leak through timing (ML-KEM
585 /// decapsulation tag check, ed25519 signature verification).
586 pub fn ct_eq(&self, other: &Self) -> subtle::Choice {
587 self.mont.ct_eq(&other.mont)
588 }
589}
590
591impl<T> Field<T, Ct>
592where
593 T: Copy
594 + PartialEq
595 + PartialOrd
596 + const_num_traits::Zero
597 + const_num_traits::One
598 + const_num_traits::WrappingMul
599 + const_num_traits::WrappingAdd
600 + const_num_traits::WrappingSub
601 + const_num_traits::ops::overflowing::OverflowingAdd
602 + core::ops::Add<Output = T>
603 + core::ops::Sub<Output = T>
604 + core::ops::Mul<Output = T>
605 + Parity
606 + MontStorage,
607{
608 /// Construct a `Field<T, Ct>` from a **secret** modulus without a
609 /// value-dependent branch on the parity check.
610 ///
611 /// `Odd::new_ct` performs the parity check via [`CtParity`], producing a
612 /// masked [`subtle::CtOption`] rather than a control-flow branch. The
613 /// precompute (`compute_n_prime_newton`, `compute_r_mod_n`,
614 /// `compute_r2_mod_n`) runs unconditionally — its inputs are the secret
615 /// modulus's value, but the operations are constant-time word arithmetic
616 /// over the existing CT trait surface, and the `CtOption` wrapper
617 /// branchlessly masks the result if the modulus turned out to be even.
618 ///
619 /// Intended for the **RSA-CRT private-key path** where `p` and `q` are
620 /// secret primes. Public-modulus / verify-side callers should use
621 /// [`Field::new_odd`] instead — the secret-aware code path is strictly
622 /// more expensive on platforms with branch prediction.
623 ///
624 /// Collapses the boundary check at the consumer:
625 ///
626 /// ```ignore
627 /// // Old shape, panics on a secret-derived branch:
628 /// let field = Field::<_, Ct>::new(secret_p).expect("p is odd prime");
629 ///
630 /// // New shape, masked:
631 /// let field = Field::<_, Ct>::try_new_odd_ct(secret_p);
632 /// let result = field.map(|f| /* CT-sensitive ops */ );
633 /// ```
634 ///
635 /// [`CtParity`]: const_num_traits::CtParity
636 pub fn try_new_odd_ct(modulus: T) -> subtle::CtOption<Self>
637 where
638 T: const_num_traits::CtParity + subtle::ConditionallySelectable + subtle::ConstantTimeLess,
639 {
640 // Mask the parity check (no branch on the secret modulus). The
641 // precompute below uses the CT path ([`Self::new_odd_ct`]) so
642 // no value-dependent branches on the modulus value either —
643 // every step is `subtle::Choice`-masked. `CtOption::new(_,
644 // choice)` discards the result via the standard masked-`Some`
645 // pattern if the modulus turned out to be even.
646 let is_odd = modulus.ct_is_odd();
647 // SAFETY: when `is_odd` is unset the wrapped `Odd` proof carries a
648 // false predicate, but the resulting `Field` is unreachable through
649 // the `CtOption` mask. No body downstream consumes the proof except
650 // via the masked output.
651 let proof = unsafe { Odd::new_unchecked(modulus) };
652 let field = Self::new_odd_ct(proof);
653 subtle::CtOption::new(field, is_odd)
654 }
655
656 /// Convert a raw value to Montgomery form. Constant-time finalize.
657 pub fn reduce(&self, raw: &T) -> Residue<'_, T, Ct>
658 where
659 T: WideMul + subtle::ConditionallySelectable + subtle::ConstantTimeLess,
660 {
661 let mont = wide_montgomery_mul_ct(*raw, self.r2_mod_n, self.modulus, self.n_prime);
662 Residue {
663 mont,
664 _brand: PhantomData,
665 _p: PhantomData,
666 }
667 }
668
669 /// Convert a [`Residue`] back to raw form. Constant-time finalize.
670 #[allow(clippy::wrong_self_convention)]
671 pub fn into_raw(&self, r: &Residue<'_, T, Ct>) -> T
672 where
673 T: WideMul + subtle::ConditionallySelectable + subtle::ConstantTimeLess,
674 {
675 wide_redc_ct(r.mont, T::zero(), self.modulus, self.n_prime)
676 }
677
678 /// Modular addition — constant-time finalize.
679 ///
680 /// See `Field<T, Nct>::add` for the load-bearing comment about why the
681 /// wrapping cond-sub path is non-negotiable.
682 pub fn add(&self, a: &Residue<'_, T, Ct>, b: &Residue<'_, T, Ct>) -> Residue<'_, T, Ct>
683 where
684 T: subtle::ConditionallySelectable + subtle::ConstantTimeLess,
685 {
686 let sum = a.mont.wrapping_add(b.mont);
687 let sub = sum.wrapping_sub(self.modulus);
688 // Carry from wrapping: sum < a means wraparound occurred.
689 let carry = sum.ct_lt(&a.mont);
690 // Result >= modulus when !(sum < modulus).
691 let ge_m = !sum.ct_lt(&self.modulus);
692 let needs_sub = carry | ge_m;
693 let mont = T::conditional_select(&sum, &sub, needs_sub);
694 Residue {
695 mont,
696 _brand: PhantomData,
697 _p: PhantomData,
698 }
699 }
700
701 /// Modular subtraction — constant-time finalize.
702 ///
703 /// Same contract as `Field<T, Nct>::sub`.
704 pub fn sub(&self, a: &Residue<'_, T, Ct>, b: &Residue<'_, T, Ct>) -> Residue<'_, T, Ct>
705 where
706 T: subtle::ConditionallySelectable + subtle::ConstantTimeLess,
707 {
708 let diff = a.mont.wrapping_sub(b.mont);
709 let corrected = diff.wrapping_add(self.modulus);
710 // borrow == (a < b)
711 let borrow = a.mont.ct_lt(&b.mont);
712 let mont = T::conditional_select(&diff, &corrected, borrow);
713 Residue {
714 mont,
715 _brand: PhantomData,
716 _p: PhantomData,
717 }
718 }
719
720 /// Modular multiplication via CIOS — constant-time finalize.
721 ///
722 /// See `Field<T, Nct>::mul` for the rationale on CIOS vs. wide-REDC and
723 /// the `#[inline]` justification.
724 #[inline]
725 pub fn mul(&self, a: &Residue<'_, T, Ct>, b: &Residue<'_, T, Ct>) -> Residue<'_, T, Ct>
726 where
727 T: CiosMontMulCt,
728 {
729 let mont = CiosMontMulCt::cios_mont_mul_ct(&a.mont, &b.mont, &self.modulus, &self.n_prime);
730 Residue {
731 mont,
732 _brand: PhantomData,
733 _p: PhantomData,
734 }
735 }
736
737 /// Modular exponentiation — constant-time over `exp`.
738 ///
739 /// Implements a fixed-iteration Montgomery ladder over all
740 /// `bit_length(T)` bits of the exponent. Both square and multiply are
741 /// performed every iteration; the result is selected branchlessly. Loop
742 /// count does not depend on `exp`; per-iteration timing does not depend
743 /// on the bit pattern.
744 pub fn exp(&self, base: &Residue<'_, T, Ct>, exp: &T) -> Residue<'_, T, Ct>
745 where
746 T: CiosMontMulCt
747 + const_num_traits::CtIsZero
748 + subtle::ConditionallySelectable
749 + subtle::ConstantTimeEq
750 + core::ops::Shr<usize, Output = T>
751 + core::ops::BitAnd<Output = T>,
752 {
753 let w = type_bit_width::<T>();
754 let one = T::one();
755 let mut result = self.r_mod_n;
756
757 for i in (0..w).rev() {
758 // Always square.
759 result =
760 CiosMontMulCt::cios_mont_mul_ct(&result, &result, &self.modulus, &self.n_prime);
761 // Always compute the conditional product.
762 let multiplied =
763 CiosMontMulCt::cios_mont_mul_ct(&result, &base.mont, &self.modulus, &self.n_prime);
764 // Select based on bit i of exp.
765 let bit_t = (*exp >> i) & one;
766 let choice = !bit_t.ct_is_zero();
767 result = T::conditional_select(&result, &multiplied, choice);
768 }
769 Residue {
770 mont: result,
771 _brand: PhantomData,
772 _p: PhantomData,
773 }
774 }
775
776 /// Modular exponentiation — constant-time over the base, **variable-time
777 /// over the exponent**. Use when the exponent is public.
778 ///
779 /// This is the right primitive for several common cryptographic shapes:
780 ///
781 /// - **RSA encrypt / verify** — `m^e mod n` with the secret message `m`
782 /// and the public exponent `e` (typically 65537). Saves `bit_length(T)
783 /// - bit_length(e)` squarings vs. the fixed-iteration ladder, which is
784 /// ~2031 squarings at 2048-bit modulus when `e = 65537`.
785 /// - **Curve25519 Fermat inverse** — `a^(p-2) mod p` where `p - 2` is the
786 /// curve constant `2^255 - 21`. The exponent is public; the base is
787 /// the secret intermediate `Z`. Skip-on-zero square-and-multiply
788 /// matches the ~252-of-255 bits set without spending the per-bit
789 /// `conditional_select` cost of the fixed-iteration ladder.
790 /// - **Curve25519 square root** — `a^((p+3)/8) mod p`, same shape.
791 ///
792 /// The squarings and multiplications themselves go through CT primitives
793 /// ([`cios_montgomery_mul_ct`](crate::montgomery::cios::cios_montgomery_mul_ct)),
794 /// so the
795 /// base and intermediate Montgomery values do not leak through timing.
796 /// What DOES leak is the bit pattern of `exp` — which is fine by
797 /// construction: the caller asserts the exponent is public.
798 ///
799 /// **Do not call with a secret exponent.** Use [`exp`](Self::exp)
800 /// instead, which is a fixed-iteration Montgomery ladder.
801 pub fn exp_public_exp(&self, base: &Residue<'_, T, Ct>, exp: &T) -> Residue<'_, T, Ct>
802 where
803 T: CiosMontMulCt + core::ops::Shr<usize, Output = T> + core::ops::BitAnd<Output = T>,
804 {
805 let w = type_bit_width::<T>();
806 let one = T::one();
807 let zero = T::zero();
808
809 // Find the position of the highest set bit (1-indexed: hi == top + 1).
810 // This loop and the rest of the function leak `bit_length(exp)`,
811 // which is the documented contract — `exp` is public.
812 let mut hi = w;
813 while hi > 0 {
814 if (*exp >> (hi - 1)) & one != zero {
815 break;
816 }
817 hi -= 1;
818 }
819
820 if hi == 0 {
821 // exp == 0: return 1 in Montgomery form.
822 return Residue {
823 mont: self.r_mod_n,
824 _brand: PhantomData,
825 _p: PhantomData,
826 };
827 }
828
829 // The top bit is set, so result starts at `base` (base^1 contribution
830 // for the 2^(hi-1) term). Then iterate over the remaining bits.
831 let mut result = base.mont;
832 for i in (0..hi - 1).rev() {
833 // Square.
834 result =
835 CiosMontMulCt::cios_mont_mul_ct(&result, &result, &self.modulus, &self.n_prime);
836 // Multiply only when the bit is set — branch on a public value.
837 if (*exp >> i) & one != zero {
838 result = CiosMontMulCt::cios_mont_mul_ct(
839 &result,
840 &base.mont,
841 &self.modulus,
842 &self.n_prime,
843 );
844 }
845 }
846
847 Residue {
848 mont: result,
849 _brand: PhantomData,
850 _p: PhantomData,
851 }
852 }
853
854 /// Wide multiply-accumulate (CT carry).
855 ///
856 /// Brand-tagged wrapper around [`wide_montgomery_mul_acc_ct`]. Pair
857 /// with [`Field::wide_redc`] (CT variant) to close the accumulator.
858 /// See the free-function for the `N ≤ R/q` bound contract.
859 pub fn mul_acc(&self, acc: (T, T), a: &Residue<'_, T, Ct>, b: &Residue<'_, T, Ct>) -> (T, T)
860 where
861 T: WideMul + subtle::ConditionallySelectable,
862 {
863 wide_montgomery_mul_acc_ct(acc.0, acc.1, a.mont, b.mont)
864 }
865
866 /// Close a wide accumulator (CT finalize) into a brand-tagged
867 /// [`Residue`].
868 pub fn wide_redc(&self, acc: (T, T)) -> Residue<'_, T, Ct>
869 where
870 T: WideMul + subtle::ConditionallySelectable + subtle::ConstantTimeLess,
871 {
872 let mont = wide_redc_ct(acc.0, acc.1, self.modulus, self.n_prime);
873 Residue {
874 mont,
875 _brand: PhantomData,
876 _p: PhantomData,
877 }
878 }
879
880 /// Modular inverse via Fermat: `a^(modulus − 2)` through the fixed-
881 /// iteration CT Montgomery ladder.
882 ///
883 /// **Requires `modulus` to be prime.** Constant-time over `a`'s
884 /// bits and zero-ness via the fixed `T::BITS`-iteration Montgomery
885 /// ladder in [`Self::exp`]. The loop count depends only on the
886 /// carrier type's bit width, not on `modulus - 2`'s significant
887 /// bit count or `a`'s value. Returns `CtOption::None`-masked for
888 /// the zero residue.
889 ///
890 /// Cost: one full ladder over every bit of `T` (e.g. 256
891 /// square-and-multiply iterations for a 256-bit carrier over a
892 /// Curve25519 scalar field), regardless of whether `modulus - 2`
893 /// occupies the full carrier width. For composite moduli (RSA
894 /// `n = p·q`) where Fermat doesn't apply, use
895 /// [`Self::inv_safegcd_ct`] instead.
896 pub fn inv_fermat(&self, a: &Residue<'_, T, Ct>) -> subtle::CtOption<Residue<'_, T, Ct>>
897 where
898 T: CiosMontMulCt
899 + const_num_traits::CtIsZero
900 + subtle::ConditionallySelectable
901 + subtle::ConstantTimeEq
902 + core::ops::Shr<usize, Output = T>
903 + core::ops::BitAnd<Output = T>,
904 {
905 let a_is_nonzero = !a.mont.ct_is_zero();
906 let two = T::one().wrapping_add(T::one());
907 let exp_val = self.modulus.wrapping_sub(two);
908 let result = self.exp(a, &exp_val);
909 subtle::CtOption::new(result, a_is_nonzero)
910 }
911
912 /// Constant-time modular inverse via Bernstein-Yang divsteps.
913 /// **Works for any modulus** — composite (RSA `n = p·q`) or prime —
914 /// unlike [`inv_fermat`] which assumes a prime modulus.
915 ///
916 /// Returns `CtOption::None` masked when `gcd(value, modulus) != 1`
917 /// (no inverse exists). Failure timing is independent of input
918 /// magnitudes.
919 ///
920 /// The modulus may occupy the full carrier width (MSB set in
921 /// `T`) — a 2048-bit RSA modulus works in an exact 2048-bit
922 /// carrier. The algorithm's signed intermediates are carried in
923 /// an internally widened representation, so no headroom bit is
924 /// required of `T`.
925 ///
926 /// Used by RSA private-key blinding, where the modulus is the
927 /// composite `n = p·q` and Fermat's little theorem doesn't apply.
928 /// See the `inv::safegcd` module source for the algorithm and
929 /// full precondition list.
930 ///
931 /// [`inv_fermat`]: Self::inv_fermat
932 pub fn inv_safegcd_ct(&self, a: &Residue<'_, T, Ct>) -> subtle::CtOption<Residue<'_, T, Ct>>
933 where
934 T: CiosMontMulCt
935 + WideMul
936 + subtle::ConditionallySelectable
937 + subtle::ConstantTimeLess
938 + const_num_traits::CtIsZero
939 + modmath_cios::CiosRowOps
940 + core::ops::Shr<usize, Output = T>
941 + core::ops::Shl<usize, Output = T>
942 + core::ops::BitOr<Output = T>,
943 <T as modmath_cios::CiosRowOps>::Word: const_num_traits::CtParity,
944 {
945 // The value in the Residue is in Montgomery form. To get the
946 // Montgomery form of the inverse:
947 // a.mont = value · R mod n
948 // raw_inv = safegcd(a.mont, n) = a.mont⁻¹ mod n
949 // = (value · R)⁻¹ mod n
950 // = value⁻¹ · R⁻¹ mod n
951 // wanted: inv.mont = value⁻¹ · R mod n
952 // = raw_inv · R² mod n
953 // Computing raw_inv · R² mod n via Mont multiplications requires
954 // **two** multiplications by R², not one:
955 // m1 = REDC(raw_inv · R²) = raw_inv · R mod n (= value⁻¹ raw)
956 // m2 = REDC(m1 · R²) = m1 · R mod n (= value⁻¹ · R = inv.mont)
957 // The first multiplication "converts raw_inv into something that
958 // multiplied by R again gives the desired Mont form". The
959 // second multiplication does that final · R step. Equivalent
960 // to one multiplication by R³, but we only have R² cached.
961 let inv_raw = crate::inv::safegcd::safegcd_inv_ct(&a.mont, &self.modulus);
962 // Extract the raw inverse, defaulting to zero when safegcd
963 // reports `None`. The two REDCs run unconditionally on the
964 // extracted value — under the CtOption mask any garbage they
965 // produce on the failure path is discarded.
966 let inv_exists = inv_raw.is_some();
967 let raw_inv = inv_raw.unwrap_or(T::zero());
968 let m1 = wide_montgomery_mul_ct(raw_inv, self.r2_mod_n, self.modulus, self.n_prime);
969 let mont = wide_montgomery_mul_ct(m1, self.r2_mod_n, self.modulus, self.n_prime);
970 let residue = Residue {
971 mont,
972 _brand: PhantomData,
973 _p: PhantomData,
974 };
975 subtle::CtOption::new(residue, inv_exists)
976 }
977}
978
979// ---------------------------------------------------------------------------
980// Tests
981// ---------------------------------------------------------------------------
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use fixed_bigint::FixedUInt;
987 use subtle::Choice;
988 #[cfg(feature = "zeroize")]
989 use zeroize::Zeroize;
990
991 // Field<T, P> requires the right combination of T-bounds for the chosen
992 // P (CiosMontMul for Nct, CiosMontMulCt for Ct), which in practice means
993 // T must be a FixedUInt of the matching personality. Small tests use
994 // FixedUInt<u8, 2> aliases for tight ranges; larger tests use the U128
995 // family. Cross-personality tests bridge values via `.into()` (Nct → Ct)
996 // and `.forget_ct()` (explicit Ct → Nct).
997 type U16 = FixedUInt<u8, 2>;
998 type U16Ct = FixedUInt<u8, 2, Ct>;
999 type U128Ct = FixedUInt<u32, 4, Ct>;
1000
1001 fn u16(n: u16) -> U16 {
1002 U16::from(n)
1003 }
1004
1005 fn u16ct(n: u16) -> U16Ct {
1006 U16Ct::from(n)
1007 }
1008
1009 #[test]
1010 fn round_trip_small() {
1011 let f: Field<U16> = Field::new(u16(13)).unwrap();
1012 for raw in 0u16..13 {
1013 let r = f.reduce(&u16(raw));
1014 assert_eq!(f.into_raw(&r), u16(raw), "round trip failed for {raw}");
1015 }
1016 }
1017
1018 #[test]
1019 fn new_odd_matches_new() {
1020 // The infallible Odd-typestate constructor and the runtime-checked
1021 // `Option`-returning one must agree on the precompute (modulus,
1022 // n_prime, r_mod_n, r2_mod_n) for the same modulus value.
1023 let m = u16(13);
1024 let modulus_odd = Odd::new(m).expect("13 is odd");
1025 let from_odd: Field<U16> = Field::new_odd(modulus_odd);
1026 let from_opt: Field<U16> = Field::new(m).unwrap();
1027 assert_eq!(from_odd.modulus(), from_opt.modulus());
1028 // Round-trip through Field::mul under each to confirm the precompute
1029 // tables match observably.
1030 let a = from_odd.reduce(&u16(7));
1031 let b = from_odd.reduce(&u16(5));
1032 let via_odd = from_odd.into_raw(&from_odd.mul(&a, &b));
1033 let a2 = from_opt.reduce(&u16(7));
1034 let b2 = from_opt.reduce(&u16(5));
1035 let via_opt = from_opt.into_raw(&from_opt.mul(&a2, &b2));
1036 assert_eq!(via_odd, via_opt);
1037 assert_eq!(via_odd, u16(35 % 13));
1038 }
1039
1040 #[test]
1041 fn new_rejects_even_and_zero() {
1042 // Wrapper preserves the rejection semantics of the old API.
1043 assert!(Field::<U16>::new(u16(0)).is_none());
1044 assert!(Field::<U16>::new(u16(12)).is_none()); // even
1045 assert!(Field::<U16>::new(u16(13)).is_some()); // odd
1046 }
1047
1048 /// `try_new_odd_ct` produces a `CtOption<Field<T, Ct>>` whose
1049 /// `Some`-ness tracks `T::ct_is_odd`. The precompute runs
1050 /// unconditionally; the parity check is masked, not branched. Test
1051 /// on `u32` (which impls `CtParity` directly) since that's the
1052 /// straightforward case — the RSA-CRT consumer pattern will be on a
1053 /// bigint type, but the contract we're pinning here is the
1054 /// modmath-side adapter.
1055 #[test]
1056 fn try_new_odd_ct_masks_parity() {
1057 // Even modulus → `None`-masked.
1058 let even = Field::<u32, Ct>::try_new_odd_ct(12);
1059 assert_eq!(even.is_some().unwrap_u8(), 0);
1060
1061 // Zero is even → `None`-masked.
1062 let zero = Field::<u32, Ct>::try_new_odd_ct(0);
1063 assert_eq!(zero.is_some().unwrap_u8(), 0);
1064
1065 // Odd modulus → `Some` with a usable Field.
1066 let odd = Field::<u32, Ct>::try_new_odd_ct(13);
1067 assert_eq!(odd.is_some().unwrap_u8(), 1);
1068 let field: Field<u32, Ct> = odd.unwrap();
1069 // Same precompute as the infallible boundary constructor:
1070 let baseline = Field::<u32, Ct>::new_odd(Odd::new(13u32).unwrap());
1071 assert_eq!(field.modulus(), baseline.modulus());
1072 }
1073
1074 /// `Field::new_odd_ct` (the CT precompute path) must produce
1075 /// identical precompute values to `Field::new_odd` (the
1076 /// variable-time path) for every modulus. Pins the contract that
1077 /// `mod_double_ct` / `mod_exp2_ct` are CT-equivalent, not just
1078 /// "CT but different output."
1079 #[test]
1080 fn new_odd_ct_precompute_matches_new_odd() {
1081 for m in [3u32, 5, 7, 11, 13, 97, 65521, 0x7FFF_FFE7] {
1082 let modulus = Odd::new(m).unwrap();
1083 let f_nct = Field::<u32, Ct>::new_odd(modulus);
1084 let f_ct = Field::<u32, Ct>::new_odd_ct(modulus);
1085 assert_eq!(f_nct.modulus(), f_ct.modulus(), "modulus mismatch at m={m}");
1086 assert_eq!(f_nct.n_prime, f_ct.n_prime, "n_prime mismatch at m={m}");
1087 assert_eq!(f_nct.r_mod_n, f_ct.r_mod_n, "r_mod_n mismatch at m={m}");
1088 assert_eq!(f_nct.r2_mod_n, f_ct.r2_mod_n, "r2_mod_n mismatch at m={m}");
1089 }
1090 }
1091
1092 /// CT precompute on multi-limb FixedUInt produces identical
1093 /// output to the variable-time precompute. The actual RSA-CRT
1094 /// shape — the precompute is what would silently produce
1095 /// wrong results if `mod_double_ct` had a bug.
1096 #[test]
1097 fn new_odd_ct_precompute_matches_new_odd_fixed_bigint() {
1098 // 128-bit odd modulus (composite, RSA-CRT-shape)
1099 let m = U128Ct::from(0xFFFF_FFFF_FFFF_FFE7u64);
1100 let modulus = Odd::new(m).unwrap();
1101 let f_nct = Field::<U128Ct, Ct>::new_odd(modulus);
1102 let f_ct = Field::<U128Ct, Ct>::new_odd_ct(modulus);
1103 assert_eq!(f_nct.modulus(), f_ct.modulus());
1104 assert_eq!(f_nct.n_prime, f_ct.n_prime);
1105 assert_eq!(f_nct.r_mod_n, f_ct.r_mod_n);
1106 assert_eq!(f_nct.r2_mod_n, f_ct.r2_mod_n);
1107 }
1108
1109 #[test]
1110 fn add_sub_mul_small() {
1111 let f: Field<U16> = Field::new(u16(13)).unwrap();
1112 for a_raw in 0u16..13 {
1113 for b_raw in 0u16..13 {
1114 let a = f.reduce(&u16(a_raw));
1115 let b = f.reduce(&u16(b_raw));
1116 assert_eq!(f.into_raw(&f.add(&a, &b)), u16((a_raw + b_raw) % 13));
1117 assert_eq!(
1118 f.into_raw(&f.sub(&a, &b)),
1119 u16((a_raw + 13 - b_raw) % 13),
1120 "sub failed for {a_raw}, {b_raw}"
1121 );
1122 assert_eq!(f.into_raw(&f.mul(&a, &b)), u16((a_raw * b_raw) % 13));
1123 }
1124 }
1125 }
1126
1127 #[test]
1128 fn zero_one_identity_small() {
1129 let f: Field<U16> = Field::new(u16(13)).unwrap();
1130 let z = f.zero();
1131 let o = f.one();
1132 assert_eq!(f.into_raw(&z), u16(0));
1133 assert_eq!(f.into_raw(&o), u16(1));
1134 // a + 0 = a, a * 1 = a
1135 for raw in 0u16..13 {
1136 let a = f.reduce(&u16(raw));
1137 assert_eq!(f.into_raw(&f.add(&a, &z)), u16(raw));
1138 assert_eq!(f.into_raw(&f.mul(&a, &o)), u16(raw));
1139 }
1140 }
1141
1142 #[test]
1143 fn exp_small() {
1144 let f: Field<U16> = Field::new(u16(13)).unwrap();
1145 // 7^5 mod 13 = 11
1146 let base = f.reduce(&u16(7));
1147 let result = f.exp(&base, &u16(5));
1148 assert_eq!(f.into_raw(&result), u16(11));
1149 // x^0 = 1
1150 let r0 = f.exp(&base, &u16(0));
1151 assert_eq!(f.into_raw(&r0), u16(1));
1152 }
1153
1154 #[test]
1155 fn ct_round_trip_small() {
1156 let f = FieldCt::new(u16ct(13)).unwrap();
1157 for raw in 0u16..13 {
1158 let r = f.reduce(&u16ct(raw));
1159 assert_eq!(f.into_raw(&r), u16ct(raw));
1160 }
1161 }
1162
1163 #[test]
1164 fn ct_matches_nct_small() {
1165 let f: Field<U16> = Field::new(u16(13)).unwrap();
1166 let fc = FieldCt::new(u16ct(13)).unwrap();
1167 for a_raw in 0u16..13 {
1168 for b_raw in 0u16..13 {
1169 let a = f.reduce(&u16(a_raw));
1170 let b = f.reduce(&u16(b_raw));
1171 let ac = fc.reduce(&u16ct(a_raw));
1172 let bc = fc.reduce(&u16ct(b_raw));
1173
1174 assert_eq!(
1175 f.into_raw(&f.add(&a, &b)),
1176 fc.into_raw(&fc.add(&ac, &bc)).forget_ct()
1177 );
1178 assert_eq!(
1179 f.into_raw(&f.sub(&a, &b)),
1180 fc.into_raw(&fc.sub(&ac, &bc)).forget_ct()
1181 );
1182 assert_eq!(
1183 f.into_raw(&f.mul(&a, &b)),
1184 fc.into_raw(&fc.mul(&ac, &bc)).forget_ct()
1185 );
1186 }
1187 }
1188 // exp cross-check
1189 let base = f.reduce(&u16(7));
1190 let base_ct = fc.reduce(&u16ct(7));
1191 for e in 0u16..20 {
1192 assert_eq!(
1193 f.into_raw(&f.exp(&base, &u16(e))),
1194 fc.into_raw(&fc.exp(&base_ct, &u16ct(e))).forget_ct()
1195 );
1196 }
1197 }
1198
1199 #[test]
1200 fn ct_cswap_small() {
1201 let f = FieldCt::new(u16ct(13)).unwrap();
1202 let mut a = f.reduce(&u16ct(3));
1203 let mut b = f.reduce(&u16ct(7));
1204 // choice = 0: no swap
1205 ResidueCt::cswap(Choice::from(0), &mut a, &mut b);
1206 assert_eq!(f.into_raw(&a), u16ct(3));
1207 assert_eq!(f.into_raw(&b), u16ct(7));
1208 // choice = 1: swap
1209 ResidueCt::cswap(Choice::from(1), &mut a, &mut b);
1210 assert_eq!(f.into_raw(&a), u16ct(7));
1211 assert_eq!(f.into_raw(&b), u16ct(3));
1212 }
1213
1214 /// Under personality, the safe NCT → CT bridge requires converting the
1215 /// underlying T's personality (free `.into()` from fixed-bigint), then
1216 /// constructing a fresh `Field<_, Ct>` on the Ct-typed modulus. Same-T
1217 /// `Field<T, Nct> -> Field<T, Ct>` conversion is degenerate (the per-P
1218 /// impl blocks have disjoint trait bounds, so the result is a methodless
1219 /// variant); this test documents the actual bridge pattern.
1220 #[test]
1221 fn nct_to_ct_upgrade_small() {
1222 let f: Field<U16> = Field::new(u16(13)).unwrap();
1223 let modulus_ct: U16Ct = (*f.modulus()).into();
1224 let fc = FieldCt::new(modulus_ct).unwrap();
1225 let a = fc.reduce(&u16ct(7));
1226 let b = fc.reduce(&u16ct(5));
1227 assert_eq!(fc.into_raw(&fc.mul(&a, &b)), u16ct(9)); // 35 mod 13 = 9
1228 }
1229
1230 #[test]
1231 fn exp_public_exp_matches_ct_exp_small() {
1232 // For every (base, exp) pair, exp_public_exp must produce the same
1233 // result as the fixed-iteration ladder exp.
1234 let f = FieldCt::new(u16ct(13)).unwrap();
1235 let base = f.reduce(&u16ct(7));
1236 for e in 0u16..32 {
1237 let via_ladder = f.exp(&base, &u16ct(e));
1238 let via_pub = f.exp_public_exp(&base, &u16ct(e));
1239 assert_eq!(
1240 f.into_raw(&via_ladder),
1241 f.into_raw(&via_pub),
1242 "exp_public_exp mismatch at e={e}"
1243 );
1244 }
1245 }
1246
1247 #[test]
1248 fn exp_public_exp_matches_ct_exp_u128() {
1249 // Same cross-check at FixedUInt<u32, 4> sizes against a few
1250 // characteristic exponents: 0, 1, small, a value with both low and
1251 // high set bits.
1252 let modulus = !U128Ct::from(0u64) - U128Ct::from(58u64);
1253 let f = FieldCt::new(modulus).unwrap();
1254 let base = f.reduce(&U128Ct::from(0xDEAD_BEEF_u64));
1255 let exps = [
1256 U128Ct::from(0u64),
1257 U128Ct::from(1u64),
1258 U128Ct::from(7u64),
1259 U128Ct::from(65537u64), // RSA-style public exponent
1260 U128Ct::from(0xCAFE_BABEu64),
1261 ];
1262 for e in &exps {
1263 let via_ladder = f.exp(&base, e);
1264 let via_pub = f.exp_public_exp(&base, e);
1265 assert_eq!(
1266 f.into_raw(&via_ladder),
1267 f.into_raw(&via_pub),
1268 "exp_public_exp mismatch at e={e:?}"
1269 );
1270 }
1271 }
1272
1273 #[test]
1274 fn brand_round_trip_fixed_bigint_u128() {
1275 // A larger odd modulus.
1276 let modulus = !U128Ct::from(0u64) - U128Ct::from(58u64);
1277 let f = FieldCt::new(modulus).unwrap();
1278 let raw = U128Ct::from(0xDEAD_BEEF_u64);
1279 let r = f.reduce(&raw);
1280 assert_eq!(f.into_raw(&r), raw);
1281 }
1282
1283 /// `inv_safegcd_ct` round-trip on a prime modulus. The CT
1284 /// composite-modulus inverse is the load-bearing primitive for RSA
1285 /// blinding; here we test on a prime (smaller test surface) and
1286 /// verify `inv * value ≡ 1 mod modulus`.
1287 #[test]
1288 fn inv_safegcd_ct_round_trip_prime_modulus() {
1289 let f = FieldCt::new(u16ct(13)).unwrap();
1290 for raw_val in 1u16..13 {
1291 let r = f.reduce(&u16ct(raw_val));
1292 let inv = f.inv_safegcd_ct(&r);
1293 assert_eq!(
1294 inv.is_some().unwrap_u8(),
1295 1,
1296 "expected inverse for {raw_val} mod 13"
1297 );
1298 let inv_residue = inv.unwrap();
1299 let product = f.mul(&r, &inv_residue);
1300 assert_eq!(
1301 f.into_raw(&product),
1302 u16ct(1),
1303 "{raw_val} * inv != 1 mod 13"
1304 );
1305 }
1306 }
1307
1308 /// `inv_safegcd_ct` on a composite modulus — the RSA blinding case.
1309 /// Confirms the algorithm works when the modulus is `p·q`, not
1310 /// prime, where Fermat inversion would fail.
1311 #[test]
1312 fn inv_safegcd_ct_composite_modulus() {
1313 // n = 3 * 5 = 15. Coprime values: 1, 2, 4, 7, 8, 11, 13, 14.
1314 let f = FieldCt::new(u16ct(15)).unwrap();
1315 for &raw_val in &[1u16, 2, 4, 7, 8, 11, 13, 14] {
1316 let r = f.reduce(&u16ct(raw_val));
1317 let inv = f.inv_safegcd_ct(&r);
1318 assert_eq!(
1319 inv.is_some().unwrap_u8(),
1320 1,
1321 "expected inverse for {raw_val} mod 15"
1322 );
1323 let product = f.mul(&r, &inv.unwrap());
1324 assert_eq!(
1325 f.into_raw(&product),
1326 u16ct(1),
1327 "{raw_val} * inv != 1 mod 15"
1328 );
1329 }
1330 // Non-coprime values: safegcd returns None.
1331 for &raw_val in &[3u16, 5, 6, 9, 10, 12] {
1332 let r = f.reduce(&u16ct(raw_val));
1333 let inv = f.inv_safegcd_ct(&r);
1334 assert_eq!(
1335 inv.is_some().unwrap_u8(),
1336 0,
1337 "expected None for non-coprime {raw_val} mod 15"
1338 );
1339 }
1340 }
1341
1342 /// `inv_safegcd_ct` with a modulus that occupies the full carrier
1343 /// width (MSB set) — the exact-width-carrier case (a 2048-bit RSA
1344 /// modulus in a 2048-bit `T`), scaled down to a 16-bit carrier.
1345 #[test]
1346 fn inv_safegcd_ct_full_width_modulus() {
1347 // U16Ct = FixedUInt<u8, 2, Ct> — 16-bit carrier holding the
1348 // odd 16-bit modulus 0xFFFD = 13 · 71² (MSB set, composite).
1349 let modulus = u16ct(0xFFFD);
1350 let f = FieldCt::new(modulus).unwrap();
1351 for raw_val in [1u16, 2, 7, 0xBEEF, 0xFFFC] {
1352 let r = f.reduce(&u16ct(raw_val));
1353 let inv = f.inv_safegcd_ct(&r);
1354 assert_eq!(
1355 inv.is_some().unwrap_u8(),
1356 1,
1357 "expected inverse for {raw_val:#x} mod 0xFFFD"
1358 );
1359 let product = f.mul(&r, &inv.unwrap());
1360 assert_eq!(
1361 f.into_raw(&product),
1362 u16ct(1),
1363 "{raw_val:#x} * inv != 1 mod 0xFFFD"
1364 );
1365 }
1366 // Non-coprime (shares factor 13) still masks to None.
1367 let r = f.reduce(&u16ct(13));
1368 assert_eq!(f.inv_safegcd_ct(&r).is_some().unwrap_u8(), 0);
1369 }
1370
1371 /// `inv_safegcd_ct` on a larger RSA-CRT-shaped composite modulus.
1372 /// n = p · q with small primes p, q. Confirms the algorithm runs
1373 /// correctly on a multi-limb FixedUInt and at sizes more
1374 /// representative of the RSA blinding workload than the toy
1375 /// `mod 15` case (still small enough that we can exhaustively
1376 /// check inv * value ≡ 1).
1377 #[test]
1378 fn inv_safegcd_ct_composite_modulus_u128() {
1379 // n = (2^32 + 7) · (2^24 + 7) — RSA-CRT-shape two-prime
1380 // composite, ~52 bits. safegcd handles composites; the result
1381 // works for any coprime value.
1382 let n_raw: u64 = 0x1_0000_0007 * 0x100_0007u64; // = 4503599644606465
1383 let modulus = U128Ct::from(n_raw);
1384 let f = FieldCt::new(modulus).unwrap();
1385
1386 // A handful of values coprime to n. (0xDEAD_BEEF deliberately
1387 // omitted — it shares factor 11 with this n.)
1388 let test_vals = [
1389 U128Ct::from(1u64),
1390 U128Ct::from(2u64),
1391 U128Ct::from(3u64),
1392 U128Ct::from(0xCAFE_BABEu64),
1393 U128Ct::from(0xFEED_FACEu64),
1394 ];
1395 for v in test_vals {
1396 let r = f.reduce(&v);
1397 let inv = f.inv_safegcd_ct(&r);
1398 assert_eq!(
1399 inv.is_some().unwrap_u8(),
1400 1,
1401 "expected inverse to exist for v={:?}",
1402 v
1403 );
1404 let product = f.mul(&r, &inv.unwrap());
1405 assert_eq!(f.into_raw(&product), U128Ct::from(1u64));
1406 }
1407 }
1408
1409 #[cfg(feature = "zeroize")]
1410 #[test]
1411 fn residue_zeroize_wipes_mont_small() {
1412 fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>(_: &T) {}
1413 let f = FieldCt::new(u16ct(13)).unwrap();
1414 let mut r = f.reduce(&u16ct(7));
1415 assert_zeroize_on_drop(&r);
1416 assert_ne!(*r.mont_value(), u16ct(0));
1417 r.zeroize();
1418 assert_eq!(*r.mont_value(), u16ct(0));
1419 }
1420
1421 #[test]
1422 fn residue_from_mont_escape_hatch_small() {
1423 // Round-trip via the escape hatch: reduce -> mont_value -> residue_from_mont.
1424 let f: Field<U16> = Field::new(u16(13)).unwrap();
1425 for raw in 0u16..13 {
1426 let r = f.reduce(&u16(raw));
1427 let mont = *r.mont_value();
1428 let r2 = f.residue_from_mont(mont);
1429 assert_eq!(f.into_raw(&r2), u16(raw));
1430 }
1431 }
1432
1433 /// Documented limitation: covariance allows mixing residues across two
1434 /// distinct Field instances built in the same scope. Asserts current
1435 /// behavior (the compiler does NOT reject this) so a future generative
1436 /// brand can be observed as a hardening change.
1437 #[test]
1438 fn covariance_mixes_residues_documented_limitation() {
1439 let f1: Field<U16> = Field::new(u16(13)).unwrap();
1440 let f2: Field<U16> = Field::new(u16(13)).unwrap();
1441 let r1 = f1.reduce(&u16(5));
1442 // f2 accepting r1 compiles today. This is a documented limitation; a
1443 // generative brand would make this a type error.
1444 let _ = f2.into_raw(&r1);
1445 }
1446
1447 /// Personality demonstration: the same `Field` type signature
1448 /// parameterized differently (`<_, Nct>` vs `<_, Ct>`) computes the
1449 /// same modular arithmetic, with the personality choice driving which
1450 /// algorithm (variable-time branch vs CT conditional-select finalize)
1451 /// the compiler routes to via the per-P impl blocks.
1452 ///
1453 /// Also exercises the residue type discipline: a `Residue<_, _, Nct>`
1454 /// passed to a `Field<_, Ct>` method would be a compile error
1455 /// (different `P` parameter), and vice versa. Cross-personality
1456 /// comparison goes through `.forget_ct()` rather than a same-type
1457 /// `assert_eq!`.
1458 #[test]
1459 fn field_p_personality_cross_check_small() {
1460 // Same modulus value, two personalities.
1461 let m_nct = u16(13);
1462 let m_ct: U16Ct = m_nct.into();
1463
1464 let f_nct: Field<U16, Nct> = Field::new(m_nct).unwrap();
1465 let f_ct: Field<U16Ct, Ct> = Field::new(m_ct).unwrap();
1466
1467 // Pick a non-trivial product and exponentiation.
1468 let a_nct = f_nct.reduce(&u16(7));
1469 let b_nct = f_nct.reduce(&u16(5));
1470 let a_ct = f_ct.reduce(&u16ct(7));
1471 let b_ct = f_ct.reduce(&u16ct(5));
1472
1473 // Multiplication agrees across personalities.
1474 let mul_nct = f_nct.into_raw(&f_nct.mul(&a_nct, &b_nct));
1475 let mul_ct = f_ct.into_raw(&f_ct.mul(&a_ct, &b_ct));
1476 assert_eq!(mul_nct, mul_ct.forget_ct());
1477
1478 // Exponentiation agrees (with different algorithms underneath:
1479 // f_nct.exp is variable-time square-and-multiply, f_ct.exp is
1480 // fixed-iteration ladder).
1481 let exp_nct = f_nct.into_raw(&f_nct.exp(&a_nct, &u16(11)));
1482 let exp_ct = f_ct.into_raw(&f_ct.exp(&a_ct, &u16ct(11)));
1483 assert_eq!(exp_nct, exp_ct.forget_ct());
1484 }
1485
1486 /// The `FieldNct<T>` alias side-steps the construction-site type-
1487 /// ambiguity that bare `Field::new(modulus)` hits — no type annotation
1488 /// or turbofish required, because the alias fixes `P = Nct` at the
1489 /// type level (mirroring how `FieldCt::new` fixes `P = Ct`).
1490 ///
1491 /// Symmetric `ResidueNct` alias also exists for downstream consumers
1492 /// who want symmetric naming. Used here just for the type spelling.
1493 #[test]
1494 fn field_nct_alias_resolves_without_annotation() {
1495 let f = FieldNct::new(u16(13)).unwrap();
1496 let r: ResidueNct<'_, U16> = f.reduce(&u16(7));
1497 assert_eq!(f.into_raw(&r), u16(7));
1498 let two = f.reduce(&u16(2));
1499 assert_eq!(f.into_raw(&f.mul(&r, &two)), u16(14 % 13));
1500 }
1501
1502 /// `from_precomputed` is `const fn` and usable in a const initializer.
1503 /// This is the constructor static-modulus consumers (PQC, embedded RSA
1504 /// with a baked key, etc.) reach for when they want to expose a `Field`
1505 /// as a `const` associated item rather than paying the runtime
1506 /// `Field::new` precompute.
1507 ///
1508 /// Demonstrated here over `u32` (primitive) — `Field<u32, Nct>` is
1509 /// methodless because `u32` doesn't impl `CiosMontMul` (MulAccOps is
1510 /// FixedUInt-only), but `from_precomputed` itself works for any
1511 /// `T: Copy`. The intended consumer path is a downstream Mont-newtype
1512 /// wrapper that calls modmath's standalone `wide_montgomery_mul[_ct]`
1513 /// free functions, using `f.modulus()` to read the static modulus.
1514 #[test]
1515 fn from_precomputed_const_construction_u32() {
1516 // Hand-computed Montgomery params for modulus 13 at word width 32:
1517 // n_prime = -13^-1 mod 2^32 = 0x4EC4EC4F
1518 // r_mod_n = 2^32 mod 13 = 9
1519 // r2_mod_n = (2^32)^2 mod 13 = 3
1520 const F: Field<u32, Nct> = Field::from_precomputed(13u32, 0x4EC4EC4F, 9, 3);
1521 assert_eq!(*F.modulus(), 13u32);
1522 // The struct fields are accessible to anyone in the same crate
1523 // through Field::modulus(); downstream consumers driving the Mont
1524 // newtype pattern will pull modulus + n_prime + r/r2 via a Modulus
1525 // trait extension on their own side and call modmath's standalone
1526 // primitives. This test just proves the const-context construction
1527 // path is real.
1528 }
1529
1530 /// `Field::mul_acc` + `Field::wide_redc` from a zero accumulator
1531 /// must equal `Field::mul` on the same operands.
1532 #[test]
1533 fn field_mul_acc_round_trip_small() {
1534 let f: Field<U16> = Field::new(u16(13)).unwrap();
1535 for a_raw in 0u16..13 {
1536 for b_raw in 0u16..13 {
1537 let a = f.reduce(&u16(a_raw));
1538 let b = f.reduce(&u16(b_raw));
1539 let direct = f.mul(&a, &b);
1540 let via_acc = f.wide_redc(f.mul_acc((u16(0), u16(0)), &a, &b));
1541 assert_eq!(f.into_raw(&direct), f.into_raw(&via_acc));
1542 }
1543 }
1544 }
1545
1546 /// Dot product through `Field::mul_acc` + single `Field::wide_redc`
1547 /// must equal the direct residue-domain sum of products.
1548 #[test]
1549 fn field_mul_acc_dot_product_small() {
1550 let f: Field<U16> = Field::new(u16(13)).unwrap();
1551 let pairs: &[(u16, u16)] = &[(2, 3), (5, 7), (11, 4), (1, 12)];
1552 let mut acc = (u16(0), u16(0));
1553 for &(a_raw, b_raw) in pairs {
1554 let a = f.reduce(&u16(a_raw));
1555 let b = f.reduce(&u16(b_raw));
1556 acc = f.mul_acc(acc, &a, &b);
1557 }
1558 let result = f.wide_redc(acc);
1559 let expected: u16 = pairs
1560 .iter()
1561 .fold(0u16, |s, &(a, b)| (s + (a * b) % 13) % 13);
1562 assert_eq!(f.into_raw(&result), u16(expected));
1563 }
1564
1565 /// `a * inv_fermat(a) == 1` for every nonzero residue at prime
1566 /// modulus 13; zero returns `None`.
1567 #[test]
1568 fn field_inv_fermat_small() {
1569 let f: Field<U16> = Field::new(u16(13)).unwrap();
1570 for raw in 1u16..13 {
1571 let a = f.reduce(&u16(raw));
1572 let inv = f.inv_fermat(&a).unwrap();
1573 assert_eq!(
1574 f.into_raw(&f.mul(&a, &inv)),
1575 u16(1),
1576 "fermat fails at {raw}"
1577 );
1578 }
1579 assert!(f.inv_fermat(&f.zero()).is_none());
1580 }
1581
1582 /// Same contract as inv_fermat but via EEA path; cross-checks the
1583 /// two methods agree at every nonzero residue.
1584 #[test]
1585 fn field_inv_eea_small() {
1586 let f: Field<U16> = Field::new(u16(13)).unwrap();
1587 for raw in 1u16..13 {
1588 let a = f.reduce(&u16(raw));
1589 let inv_e = f.inv_eea(&a).unwrap();
1590 let inv_f = f.inv_fermat(&a).unwrap();
1591 assert_eq!(f.into_raw(&f.mul(&a, &inv_e)), u16(1), "eea fails at {raw}");
1592 assert_eq!(
1593 f.into_raw(&inv_e),
1594 f.into_raw(&inv_f),
1595 "fermat/eea disagree at {raw}"
1596 );
1597 }
1598 assert!(f.inv_eea(&f.zero()).is_none());
1599 }
1600
1601 /// Ct variant of `Field::mul_acc` + `Field::wide_redc` must agree
1602 /// with `Field::mul`.
1603 #[test]
1604 fn field_mul_acc_ct_round_trip_small() {
1605 let f = FieldCt::new(u16ct(13)).unwrap();
1606 for a_raw in 0u16..13 {
1607 for b_raw in 0u16..13 {
1608 let a = f.reduce(&u16ct(a_raw));
1609 let b = f.reduce(&u16ct(b_raw));
1610 let direct = f.mul(&a, &b);
1611 let via_acc = f.wide_redc(f.mul_acc((u16ct(0), u16ct(0)), &a, &b));
1612 assert_eq!(f.into_raw(&direct), f.into_raw(&via_acc));
1613 }
1614 }
1615 }
1616
1617 /// Ct `inv_fermat` must satisfy `a * inv(a) == 1` at prime modulus.
1618 #[test]
1619 fn field_inv_fermat_ct_small() {
1620 let f = FieldCt::new(u16ct(13)).unwrap();
1621 for raw in 1u16..13 {
1622 let a = f.reduce(&u16ct(raw));
1623 let inv = f.inv_fermat(&a).into_option().unwrap();
1624 assert_eq!(
1625 f.into_raw(&f.mul(&a, &inv)),
1626 u16ct(1),
1627 "ct fermat fails at {raw}"
1628 );
1629 }
1630 assert!(f.inv_fermat(&f.zero()).into_option().is_none());
1631 }
1632
1633 /// `ResidueCt::ct_eq` matches `PartialEq` outcomes on representative
1634 /// inputs (true and false cases).
1635 #[test]
1636 fn residue_ct_eq_small() {
1637 let f = FieldCt::new(u16ct(13)).unwrap();
1638 let a = f.reduce(&u16ct(7));
1639 let b = f.reduce(&u16ct(7));
1640 let c = f.reduce(&u16ct(8));
1641 let eq_ab: bool = a.ct_eq(&b).into();
1642 let eq_ac: bool = a.ct_eq(&c).into();
1643 assert!(eq_ab);
1644 assert!(!eq_ac);
1645 }
1646}