Skip to main content

class_groups/crypto_bigint/
sqrt.rs

1use crypto_bigint::{
2  CtEq as _, CtAssign, Zero, One, Odd, JacobiSymbol, NegMod, MulMod, SquareMod, BitOps, Limb,
3  UintRef,
4};
5
6/// The required view over a collection of limbs to calculate a square root.
7trait SquareRoot:
8  Clone
9  + AsRef<[Limb]>
10  + AsMut<[Limb]>
11  + Zero
12  + One
13  + NegMod<Output = Self>
14  + MulMod<Output = Self>
15  + SquareMod<Output = Self>
16  + BitOps
17{
18  /// Compute $self^{exp} \mod modulus$.
19  ///
20  /// This MUST support $self \ge mmodlus$.
21  fn pow_mod(self, exp: Self, modulus: &Odd<Self>) -> Self;
22}
23
24impl<
25  T: Clone
26    + AsRef<[Limb]>
27    + AsMut<[Limb]>
28    + CtAssign
29    + Zero
30    + One
31    + NegMod<Output = Self>
32    + MulMod<Output = Self>
33    + SquareMod<Output = Self>
34    + BitOps,
35> SquareRoot for T
36{
37  fn pow_mod(self, exp: Self, modulus: &Odd<Self>) -> Self {
38    let mut result = Self::one_like(modulus);
39    let mut square = self.clone();
40    for i in 0 .. modulus.as_ref().bits_precision() {
41      result.ct_assign(
42        &result.mul_mod(&square, modulus.as_nz_ref()),
43        u8::from(exp.bit_vartime(i)).ct_eq(&1),
44      );
45      square = square.square_mod(modulus.as_nz_ref());
46    }
47    result
48  }
49}
50
51/// Calculate the Legendre symbol of `a` over the odd prime `p`.
52///
53/// This is equivalent to the Jacobi symbol given the requirement `p` is an odd prime (a bound
54/// required for the Legendre symbol which the Jacobi symbol is a generalization of).
55///
56/// This assumes `p` is an odd prime. This does not require `a < p`.
57///
58/// This function runs in constant time (solely variable to the precision of its inputs).
59#[expect(private_bounds)]
60pub(crate) fn legendre_symbol<U: SquareRoot>(a: U, p: &Odd<U>) -> JacobiSymbol {
61  /*
62    The following is premised on Euler's criterion which states that for an odd prime `p`, where
63    `a` is coprime to `p`, then $a^{(p - 1) / 2} \cong 1 \mod p$ if $a$ is quadratic residue modulo
64    `p`.
65  */
66
67  let p_minus_one_div_two = {
68    let mut p_minus_one_div_two = p.as_ref().clone();
69    // This is known to be correct as `p` is bound to be an odd prime, meaning the
70    // least-significant bit is `1` (causing the subtraction to be contained to the bit we shift
71    // out)
72    UintRef::new_mut(p_minus_one_div_two.as_mut()).shr1_assign();
73    p_minus_one_div_two
74  };
75
76  let exponentation = a.pow_mod(p_minus_one_div_two, p);
77  let (exponentation_is_zero, exponentation_is_one) = {
78    let exponentation = <_ as AsRef<[Limb]>>::as_ref(&exponentation);
79    let mut exponentation_is_zero = Limb::ZERO;
80    for limb in exponentation.iter().skip(1) {
81      exponentation_is_zero |= limb;
82    }
83    let mut exponentation_is_zero = exponentation_is_zero.is_zero();
84    let mut exponentation_is_one = exponentation_is_zero;
85    exponentation_is_zero &= exponentation[0].is_zero();
86    exponentation_is_one &= exponentation[0].is_one();
87    (exponentation_is_zero, exponentation_is_one)
88  };
89
90  /*
91    `JacobiSymbol` does not implement `CtAssign`, so we transmute it to its underlying
92    representation, `i8`, and use that instead. Note `core::mem::transmute` asserts the types are
93    the same size and we do not have to consider alignment as these aren't references.
94  */
95  let (one, zero, minus_one) = unsafe {
96    (
97      core::mem::transmute::<JacobiSymbol, i8>(JacobiSymbol::One),
98      core::mem::transmute::<JacobiSymbol, i8>(JacobiSymbol::Zero),
99      core::mem::transmute::<JacobiSymbol, i8>(JacobiSymbol::MinusOne),
100    )
101  };
102
103  let mut result = minus_one;
104  result.ct_assign(&one, exponentation_is_one);
105  // The exponent was non-zero (as the first odd prime is `3` and `(3 - 1) / 2 = 1`) so if
106  // $a \cong 0 \mod p$, then $a^{(p - 1) / 2} \cong 0 \mod p$.
107  result.ct_assign(&zero, exponentation_is_zero);
108
109  // This is safe we know this value is valid as a `JacobiSymbol` (it being transmuted from one)
110  unsafe { core::mem::transmute::<i8, JacobiSymbol>(result) }
111}
112
113/// Calculate the square root of `n` modulo the odd prime `p`.
114///
115/// This assumes `p` is an odd prime. This does not require `n >= p`.
116///
117/// The returned square root will always be the _odd_ square root.
118///
119/// This function runs in variable time. This function finds the least quadratic non-residue for
120/// $p$, which is fixed to $p$, and a non-trivial part of the computation. This function SHOULD NOT
121/// be used for square roots when the prime $p$ is consistent across multiple calls to calculate a
122/// square root.
123/*
124  TODO: For this use case, should we implement Cipolla's algorithm? It also requires finding a
125  quadratic non-residue, one bespoke to the number we're taking the square root of, so it _can't_
126  be preprocessed yet we don't support preprocessing anyways. The remainder of the algorithm is
127  one modular exponentation in an extension field, which is annoying but avoids a loop re: `S`.
128
129  Cipolla's algorithm is bounded by `log_2(p)` the amount of bits in the exponent, not
130  `log_2(p)^2`, the product of the complexities for the outer and inner loop which each iterate
131  over `S`. It also just may be more straightforward.
132
133  It should be noted the stronger bound doesn't matter too much when this is variable-time, and
134  Tonelli-Shanks is reasonable in variable-time, and a constant time variant would need a
135  termination bound for finding a quadratic non-residue (which Tonelli-Shanks can do under the
136  Generalized Riemann Hypothesis, but Cipolla's algorithm could only do to a statistical bound).
137*/
138#[expect(private_bounds)]
139pub(crate) fn sqrt_mod_p_vartime<U: SquareRoot>(n: U, p: &Odd<U>) -> Option<U> {
140  match legendre_symbol(n.clone(), p) {
141    JacobiSymbol::One => {}
142    JacobiSymbol::Zero => return Some(U::zero_like(p.as_ref())),
143    JacobiSymbol::MinusOne => return None,
144  }
145
146  /*
147    The following is an implementation of the Tonelli-Shanks algorithm for finding square roots
148    modulo an odd prime `p`. While the cases $p \cong 3 \mod 4$ and $p \cong 5 mod 4$ do enable
149    much simpler algorithms, the following is implemented for being universal.
150  */
151
152  /*
153    `S` is the amount of zero bits before the first (from least-significant to
154    most-significant) set bit in `p - 1`. As `p` is an odd prime, we know the least-significant
155    bit of `p - 1` is zero.
156  */
157  let mut S = 1;
158  while !p.as_ref().bit_vartime(S) {
159    S += 1;
160  }
161
162  // `Q` is $(p - 1) / 2^S$, which is an integer by the definition of `S`
163  let mut Q = p.as_ref().clone();
164  UintRef::new_mut(Q.as_mut()).shr_assign(S);
165
166  // $(Q + 1) / 2$ where $Q$ is odd by construction
167  let mut Q_plus_1_div_2 = Q.clone();
168  {
169    let limbs = <_ as AsMut<[Limb]>>::as_mut(&mut Q_plus_1_div_2);
170    let mut carry = UintRef::new_mut(limbs).add_assign_limb(Limb::ONE);
171    for i in (0 .. limbs.len()).rev() {
172      let new_limb = (carry << (Limb::BITS - 1)) | (limbs[i] >> 1);
173      carry = limbs[i] & Limb::ONE;
174      limbs[i] = new_limb;
175    }
176    // An odd number plus one will be even, as allowing the division by two
177    debug_assert!(bool::from(carry.is_zero()));
178  }
179  let mut R = n.clone().pow_mod(Q_plus_1_div_2, p);
180  let mut t = n.pow_mod(Q.clone(), p);
181
182  /*
183    Find the least quadratic non-residue within the odd-prime field $p$.
184
185    Every odd prime has `(p - 1) / 2` quadratic non-residues, so every odd prime has _a_ quadratic
186    non-residue. This ensures the following calculation terminates and doesn't have capacity
187    exceeding the capacity of the container for `p`.
188
189    Assuming the Generalized Riemann Hypothesis, $2 \le z \le log_e(q)^2$ for $q \ge 5$.
190
191    Corollary 1.1 of "Conditional Bounds for the Least Quadratic Non-Residue and Related Problems"
192    by Youness Lamzouri, Xiannan Li, and Kannan Soundararajan.
193    https://pubs.ams.org/journals/mcom/2015-84-295/S0025-5718-2015-02925-1/home.html
194
195    As $e > 2$, $log_e(q) \le log_2(q)$, letting us replace the upper bound as so. Then, for the
196    sole exceptional odd prime $q = 3$ (where $log_e(3)^2 < 2$), we do have $log_2(3)^2 \ge 2$
197    (where $2$ is the least quadratic non-residue modulo $3$).
198
199    This lets us bound (under the Generalized Riemann Hypothesis) $2 \le z \le log_2(q)^2$, where
200    $z$ is the least quadratic non-residue (as we want) and $q$ is an odd prime (as our input is
201    bound).
202
203    Alternatively, if one assumes the distribution of quadratic non-residues uniform over the prime
204    field, this achieves a statistical bound on termination within $(1/2)^z$ iterations.
205
206    NOTE: We could make this function constant-time assuming the Generalized Riemann Hypothesis.
207    It'd have $log_2(p)^2$ complexity however, not only to find `z` but also as the following loop
208    has a runtime of $(S^2) / 2$ (where $S \le log_2(p)$).
209  */
210  let z = {
211    let mut z = U::zero_like(p.as_ref());
212    <_ as AsMut<[Limb]>>::as_mut(&mut z)[0] = Limb::from(2u8);
213    while legendre_symbol(z.clone(), p) != JacobiSymbol::MinusOne {
214      let _carry = UintRef::new_mut(z.as_mut()).add_assign_limb(Limb::ONE);
215    }
216    z
217  };
218
219  let mut M = S;
220  let mut c = z.pow_mod(Q, p);
221
222  /*
223    Each iteration of this loop decreases `M` by at least one, where this loop runs for values
224    $1 < M \le S$. This loop will accordingly run for $S \le log_2(p)$ iterations at most.
225  */
226  while !bool::from(t.is_one()) {
227    let i = {
228      let mut t = t.clone();
229      let mut i = 0;
230      // This loop will run for `M` times at most where $2 \le M \le S$ and $S \le log_2(p)$
231      while !bool::from(t.is_one()) {
232        i += 1;
233        assert!(i < M);
234        t = t.square_mod(p.as_nz_ref());
235      }
236      i
237    };
238
239    let mut b = c;
240    for _ in 0 .. (M - i - 1) {
241      b = b.square_mod(p.as_nz_ref());
242    }
243
244    M = i;
245    c = b.square_mod(p.as_nz_ref());
246    t = t.mul_mod(&c, p.as_nz_ref());
247
248    R = R.mul_mod(&b, p.as_nz_ref());
249  }
250
251  // Normalize to the odd square root
252  if (<_ as AsRef<[Limb]>>::as_ref(&R)[0] & Limb::ONE) != Limb::ONE {
253    R = R.neg_mod(p.as_nz_ref());
254  }
255
256  Some(R)
257}