Arnault's method
by neovymp · 21/07/2026
Introduction
This is a (very) shortened version of my Bachelor thesis in Computer Science.
Miller-Rabin primality test
The Miller-Rabin primality test checks for a number's primality by ensuring that there an non-trivial square roots of modulo this number. For a positive odd integer and number of rounds the test works like this:
- Factor as .
- For each round repeat this steps:
- Select a random value in the interval called base.
- Calculate , if this is equal to modulo go back to step 1, otherwise proceed to step 3.
- If for any go back to step 1 (if it is not the last round), otherwise exit and declare composite.
- If no round deemed composite the test deems it prime.
Arnault's method
Arnault's method constructs an odd integer product of an odd number of primes such that is a strong pseudoprimes to a given set of prime bases . The method construct via a system of equations and then each as for randomly chosen to be coprime to each base (except for ) for . For to be a strong pseudoprimes to bases it is sufficient that each of its prime factors satisfies (so is a quadratic nonresidue modulo ) for , where is the Legendre symbol of modulo .
Arnault's method starts by generating sets for each base such that:
Since has to satisfy the conditions of each set and so do stricter conditions can be derived:
From each of these sets random residues are chosen to solve a system of equations:
The CRT tells us a solution is not guaranteed since moduli are not coprime so we may have to try several times (giving up after a few iterations to not fall into the Sunk cost fallacy).
To restrict the field of search a few more conditions can be added, resulting in polynomials for which has to be a root:
After getting a solution we look for values of that make prime (by simply bruteforcing) and calculate as described above, if all are prime their product will be a strong pseudoprimes to the bases .
Optimizations
Some sections of Arnault's method slow it down significantly, but with a few clever tricks some optimizations can be found.
Finding
To get we must solve the system of equations we saw above, which might take several iterations of the random selection of residues, but a faster strategy is in front of us: instead of picking all residues at once we can select them one at a time, repicking only when there is no solution and continuing like so until the end [1].
Constructing
While implementing Arnault's method I noticed a pattern I desperately had to confirm: for each set contains exactly elements. After some testing which seemed to confirm my hypothesis I wanted to prove it, which eventually resulted in a two part proof, but first some lemmas:
- Dirichlet's theorem an arithmetic progressions states that there are infinitely many primes of the form for coprime integers and .
- The law of quadratic reciprocity states that .
The proof is split in two cases, depending on the value of modulo :
-
If we have and since is fixed for the set it is easier to work with the right-hand side. In there are exactly quadratic residues and quadratic nonresidues, we define sets and with containing the odd residues and the even ones. Since contains primes reduced modulo we do not cannot allow even values, meaning elements from the sets , leaving to fill : , which are trivially disjoint. Therefore .
-
For we do something similar by splitting the set into two disjoint sets: , with trivially. Notice that both sets contain all residues modulo exactly once and: elements of are congruent to modulo , implying , meaning in there are quadratic nonresidues; meanwhile elements of are congruent to modulo , implying , meaning we need the quadratic residues modulo which are once again . Combining and to get we finally get .
The steps used in the proof can be made into an algorithm which constructs each set in time:
import gmpy2
def find_Sa(a):
assert gmpy2.is_prime(a)
if a % 4 == 1:
X = set(p for p in range(3, a, 2) if gmpy2.legendre(p, a) == -1)
Y1 = set(p for p in range(2 + a, 2*a, 2) if gmpy2.legendre(p, a) == -1)
X1 = set(x + 2*a for x in X)
Y2 = set(y + 2*a for y in Y1)
Sa = X | Y1 | X1 | Y2
else:
remove = 1 + 4*(-1 * pow(4, -1, a) % a)
X = set((1 + 4*t) for t in range(a)) - set((remove,))
X_qnr = set(filter(lambda x: gmpy2.legendre(x, a) == -1, X))
Y_qr = set((x + 2*a) % (4*a) for x in (X - X_qnr))
Sa = X_qnr | Y_qr
return Sa
The naive strategy tests the condition for primes in increasing fashion until some arbitrary threshold, which may not find all of them; stopping instead once have been found is what the figure is showcasing, which is (empirically).
Conclusions
Fooling a primality test has huge consequences, for example a discrete logarithm can be solved with a much faster method if the modulus used is a composite with known factorization, jeopardizing protocols such as the Diffie-Hellman key exchange which is ubiquitous in public-key cryptography. My implementation of Arnault's method can be found on GitHub.