An Introduction to Primality Testing
Fred Akalin
I will explain two commonly-used primality tests: Fermat and
Miller-Rabin. Along the way, I will cover the basic concepts of
primality testing. I won't be assuming any background in number
theory, but familiarity
with modular
arithmetic will be helpful. I will also be providing
implementations in Javascript,
so familiarity
with it will also be helpful. Finally, since Javascript doesn't
natively support arbitrary-precision arithmetic, I wrote a simple
natural number class
(SNat
) that
represents a number as an array of decimal digits. All algorithms
used are the simplest possible, except when a more efficient one is
needed by the algorithms we discuss.
Primality testing is the problem of determining whether a given natural number is prime or composite. Compared to the problem of integer factorization, which is to determine the prime factors of a given natural number, primality testing turns out to be easier; integer factorization is in NP and thought to be outside P and NP-complete, whereas primality testing is now known to be in P.
Most primality tests are actually compositeness tests; they involve finding composite witnesses, which are numbers that, along with a given number to be tested, can be fed to some easily-computable function to prove that the given number is composite. (The composite witness, along with the function, is a certificate of compositeness of the given number.) A primality test can either check each possible witness or, like the Fermat and Miller-Rabin tests, it can randomly sample some number of possible witnesses and call the number prime if none turn out to be witnesses. In the latter case, there is a chance that a composite number can erroneously be called prime; ideally, this chance goes to zero quickly as the sample size increases.
The simplest possible witness type is, of course, a factor of the given number, which we'll call a factor witness. If the number to be tested is \(n\) and the possible factor witness is \(a\), then one can simply test whether \(a\) divides \(n\) (written as \(a \mid n\)) by evaluating \(n \bmod a = 0\); that is, whether the remainder of \(n\) divided by \(a\) is zero. This doesn't yield a feasible deterministic primality test, though, since checking all possible witnesses is equivalent to factoring the given number. Nor does it yield a feasible probabilistic primality test, since in the worst case the given number has very few factors, which random sampling would miss.
Thus, a Fermat witness is a number \(1 \lt a \lt n\) such that \(a^{n-1} \not\equiv 1 \pmod{n}\). Conversely, if \(n\) is composite and \(a^{n-1} \equiv 1 \pmod{n}\), then \(a\) is a Fermat liar.
Let n = and a = .
If \(n\) has at least one Fermat witness that is relatively prime, then we can show that at least half of all possible witnesses are Fermat witnesses. (Roughly, if \(a\) is the Fermat witness and \(a_1, a_2, \dotsc, a_s\) are Fermat liars, then all \(a \cdot a_i\) are also Fermat witnesses.) Therefore, for a sample of \(k\) possible witnesses of \(n\), the probability of all of them being Fermat liars is \(\le 2^{-k}\), which goes to zero quickly enough to be practical.
However, there is the possibility that \(n\) is a composite number with no relatively prime Fermat witnesses. These are called Carmichael numbers. Even though Carmichael numbers are rare, their existence still makes the Fermat primality test unsuitable for some situations, as when the numbers to be tested are provided by some adversary.
// Runs the Fermat compositeness test given n > 2 and 1 < a < n.
// Calculates r = a^{n-1} mod n and whether a is a Fermat witness to n
// (i.e., r != 1, which means n is composite). Returns a dictionary
// with a, n, r, and isCompositeByFermat, which is true iff a is a
// Fermat witness to n.
function testCompositenessByFermat(n, a) {
n = SNat.cast(n);
a = SNat.cast(a);
if (n.le(2)) {
throw new RangeError('n must be > 2');
}
if (a.le(1) || a.ge(n)) {
throw new RangeError('a must satisfy 1 < a < n');
}
var r = a.powMod(n.minus(1), n);
var isCompositeByFermat = r.ne(1);
return {
a: a,
n: n,
r: r,
isCompositeByFermat: isCompositeByFermat
};
}
Note that the algorithm depends on the efficiency
of modular
exponentiation when calculating \(a^{n-1} \pmod{n}\). The
naive method is unsuitable since it requires \(Θ(n)\) \(b\)-bit
multiplications, where \(b = \lceil \lg n \rceil\). SNat
uses repeated
squaring, which requires only \(Θ(\lg n)\) \(b\)-bit
multiplications.Another useful witness type is a non-trivial square root of unity \(\operatorname{mod} n\); that is, a number \(a ≠ \pm 1 \pmod{n}\) such that \(a^2 \equiv 1 \pmod{n}\). It is a theorem of number theory that if \(n\) is prime, there are no non-trivial square roots of unity \(\operatorname{mod} n\). Therefore, if we do find one, that means \(n\) is composite. In fact, finding one leads directly to factors of \(n\). By definition, a non-trivial square root of unity \(a\) satisfies \(a \pm 1 ≠ 0 \pmod{n}\) and \(a^2 - 1 \equiv 0 \pmod{n}\). Factoring the latter leads to \((a+1)(a-1) \equiv 0 \pmod{n}\), which means that \(n\) divides \((a+1)(a-1)\). But the first condition says that \(n\) divides neither \(a+1\) nor \(a-1\), so it must be a product of two numbers \(p \mid a+1\) and \(q \mid a-1\). Then \(\gcd(a+1, n)\)[1] and \(\gcd(a-1, n)\) are factors of \(n\).
Finding non-trivial square roots of unity by itself doesn't give a useful primality testing algorithm, but combining it with the Fermat primality test does. \(a^{n-1} \bmod n\) either equals \(1\) or not. If it doesn't, you're done since you have a Fermat witness. If it does equal \(1\), and \(n-1\) is even, then consider the square root of \(a^{n-1}\), i.e. \(a^{(n-1)/2}\). If it is not \(\pm 1\), then it is a non-trivial square root of unity. If it is \(-1\), then you can't do anything else. But if it is \(1\), and \((n-1)/2\) is even, you can then take another square root and repeat the test, stopping when the exponent of \(a\) becomes odd or when you get a result not equal to \(1\).
To turn this into an algorithm, you simply start from the bottom up: find the greatest odd factor of \(n-1\), call it \(t\), and keep squaring \(a^t\) mod \(n\) until you find a non-trivial square root of \(n\) or until you can deduce the value of \(a^{n-1}\). In fact, this is almost as fast as the original Fermat primality test, since the exponentiation by \(n-1\) has to do the same sort of squaring, and we're just adding comparisons to \(±1\) in between squarings.
The original idea for the test above is from Artjuhov, although it is usually credited to Miller. Therefore, we call \(a\) an Artjuhov witness[2] of \(n\) if it shows \(n\) composite by the above test.
Let n = and a = .
If \(n\) is an odd composite, then it can be shown (originally by Rabin) that at least three quarters of all possible witnesses are Artjuhov witnesses. Therefore, for a sample of \(k\) possible witnesses of \(n\), the probability of all of them being Artjuhov liars is \(\le 4^{-k}\), which is stronger than the bound for the Fermat primality test. Furthermore, this bound is unconditional; there is nothing like Carmichael numbers for the Artjuhov test.
// Runs the Artjuhov compositeness test given n > 2 and 1 < a < n-1.
// Finds the largest s such that n-1 = t*2^s, calculates r = a^t mod
// n, then repeatedly squares r (mod n) up to s times until r is
// congruent to -1, 0, or 1 (mod n). Then, based on the value of s
// and the final value of r and i (the number of squarings),
// determines whether a is an Artjuhov witness to n (i.e., n is
// composite).
//
// Returns a dictionary with, a, n, s, t, i, r, rSqrt = sqrt(r) if i >
// 0 and null otherwise, and isCompositeByArtjuhov, which is true iff
// a is an Artjuhov witness to n.
function testCompositenessByArtjuhov(n, a) {
n = SNat.cast(n);
a = SNat.cast(a);
if (n.le(2)) {
throw new RangeError('n must be > 2');
}
if (a.le(1) || a.ge(n)) {
throw new RangeError('a must satisfy 1 < a < n');
}
var nMinusOne = n.minus(1);
// Find the largest s and t such that n-1 = t*2^s.
var t = nMinusOne;
var s = new SNat(0);
while (t.isEven()) {
t = t.div(2);
s = s.plus(1);
}
// Find the smallest 0 <= i < s such that a^{t*2^i} = 0/-1/+1 (mod
// n).
var i = new SNat(0);
var rSqrt = null;
var r = a.powMod(t, n);
while (i.lt(s) && r.gt(1) && r.lt(nMinusOne)) {
i = i.plus(1);
rSqrt = r;
r = r.times(r).mod(n);
}
var isCompositeByArtjuhov = false;
if (s.isZero()) {
// If 0 = i = s, then this reduces to the Fermat primality test.
isCompositeByArtjuhov = r.ne(1);
} else if (i.isZero()) {
// If 0 = i < s, then:
//
// * r = 0 (mod n) -> a^{n-1} = 0 (mod n), and
// * r = +/-1 (mod n) -> a^{n-1} = 1 (mod n).
isCompositeByArtjuhov = r.isZero();
} else if (i.lt(s)) {
// If 0 < i < s, then:
//
// * r = 0 (mod n) -> a^{n-1} = 0 (mod n),
// * r = +1 (mod n) -> a^{t*2^{i-1}} is a non-trivial square root of
// unity mod n, and
// * r = -1 (mod n) -> a^{n-1} = 1 (mod n).
//
// Note that the last case means r = n - 1 > 1.
isCompositeByArtjuhov = r.le(1);
} else {
// If 0 < i = s, then:
//
// * r = 0 (mod n) can't happen,
// * r = +1 (mod n) -> a^{t*2^{i-1}} is a non-trivial square root of
// unity mod n, and
// * r > +1 (mod n) -> failure of the Fermat primality test.
isCompositeByArtjuhov = true;
}
return {
a: a,
n: n,
t: t,
s: s,
i: i,
r: r,
rSqrt: rSqrt,
isCompositeByArtjuhov: isCompositeByArtjuhov
};
}
With the two compositeness tests above, we can now write a
probabilistic primality test:
// Returns true iff a is a Fermat witness to n, and thus n is
// composite. a and n must satisfy the same conditions as in
// testCompositenessByFermat.
function hasFermatWitness(n, a) {
return testCompositenessByFermat(n, a).isCompositeByFermat;
}
// Returns true iff a is an Arjuhov witness to n, and thus n is
// composite. a and n must satisfy the same conditions as in
// testCompositenessByArtjuhov.
function hasArtjuhovWitness(n, a) {
return testCompositenessByArtjuhov(n, a).isCompositeByArtjuhov;
}
// Returns true if n is probably prime, based on sampling the given
// number of possible witnesses and testing them against n. If false
// is returned, then n is definitely composite.
//
// By default, uses the Artjuhov test for witnesses with 20 samples
// and Math.random for the random number generator. This gives an
// error bound of 4^-20 if true is returned.
function isProbablePrime(n, hasWitness, numSamples, rng) {
n = SNat.cast(n);
hasWitness = hasWitness || hasArtjuhovWitness;
rng = rng || Math.random;
numSamples = numSamples || 20;
if (n.le(1)) {
return false;
}
if (n.le(3)) {
return true;
}
if (n.isEven()) {
return false;
}
for (var i = 0; i < numSamples; ++i) {
var a = SNat.random(2, n.minus(2), rng);
if (hasWitness(n, a)) {
return false;
}
}
return true;
}
isProbablePrime
called
with hasFermatWitness
is the Fermat primality
test, and isProbablePrime
called
with hasArtjuhovWitness
is the Miller-Rabin primality
test. The latter is the current general primality test of
choice, replacing
the Solovay-Strassen
primality test.
We can also use isProbablePrime
to randomly generate
probable primes, which is useful
for cryptographic
applications:
// Returns a probable b-bit prime that is at least 2^b. All
// parameters but b are passed to isProbablePrime.
function findProbablePrime(b, hasWitness, rng, numSamples) {
b = SNat.cast(b);
var lb = (new SNat(2)).pow(b.minus(1));
var ub = lb.times(2);
while (true) {
var n = SNat.random(lb, ub);
if (isProbablePrime(n, hasWitness, rng, numSamples)) {
return n;
}
}
}
In this case, for sufficiently large \(b\), the Fermat primality test is acceptable, since Carmichael numbers are so rare and we're the ones generating the possible primes to be tested.[3]
There are other primality tests, but they're less often used in practice because they're either less efficient or more sophisticated than the algorithms above, or they require \(n\) to have special properties. Perhaps the most interesting of these tests is the AKS primality test, which proved once and for all that primality testing is in P.
Like this post? Subscribe to my feed or follow me on Twitter .
Footnotes
[1] \(\gcd\) is the greatest common divisor function. ↩
[2] “Artjuhov witness” is an idiosyncratic name on my part; a more common name is strong witness, which I don't like. ↩
[3] According to Wikipedia, PGP uses the Fermat primality test. ↩