The behavior of BPSW for numbers > 2^64 is not very well understood.
While there is no known composite that passes the test, there are
heuristics that indicate that there are likely many. Therefore it seems
appropriate to harden the test. Having a settable number of MR rounds
before doing a version of BPSW is also the approach taken by Go's
primality check in math/big.

This is a first step towards incorporating some of the considerations in
"A performant misuse-resistant API for Primality Testing" by Massimo and
Paterson. It should be noted that it is based on this paper (and due to
FIPS, I suppose) that OpenSSL 3 made their primality check so slow that
the bn_prime.c regress test took over 10 minutes on an M3000 before the
libcrypto bump (it took less than a minute with ours).

This basically adds back an equivalent of the old inadequate prime number
test before running the strong Lucas test, with slightly cleaner code.
While I don't think performance matters a lot here, we're then effectively
at about twice the cost of what we had a year ago. I also think that having
some non-determinism in the algorithm is a plus.

The implementation is straightforward. It could easily be tweaked to use
the additional gcds in the "enhanced" MR test of FIPS 186-5, but as long
as we are only going to throw away the additional info, this doesn't add
all that much.

I'd like to land this and then do further work in tree. We probably want
to crank the number of MR rounds done by default considerably. I will
also need to undo some of the clean-up done in BN_generate_prime_ex()
after BPSW landed.

Index: bn/bn_bpsw.c
===================================================================
RCS file: /cvs/src/lib/libcrypto/bn/bn_bpsw.c,v
retrieving revision 1.8
diff -u -p -r1.8 bn_bpsw.c
--- bn/bn_bpsw.c        26 Nov 2022 16:08:51 -0000      1.8
+++ bn/bn_bpsw.c        28 Apr 2023 07:56:01 -0000
@@ -301,23 +301,94 @@ bn_strong_lucas_selfridge(int *is_prime,
 }
 
 /*
- * Miller-Rabin primality test for base 2.
+ * Fermat criterion in Miller-Rabin test.
+ *
+ * Check whether 1 < base < n - 1 witnesses that n is composite. For prime n:
+ *
+ *  * Fermat's little theorem: base^(n-1) = 1 (mod n).
+ *  * The only square roots of 1 (mod n) are 1 and -1.
+ *
+ * Calculate base^((n-1)/2) by writing n - 1 = k * 2^s with odd k: iteratively
+ * compute (base^k)^(2^(s-1)) by successive squaring of base^k.
+ *
+ * If -1 is ever reached, base^(n-1) works out to 1 and n is a pseudoprime
+ * for base. If 1 is reached before -1, we have an unexpected square root of
+ * unity and n is composite. Otherwise base^(n-1) != 1, so n is composite.
  */
 
 static int
-bn_miller_rabin_base_2(int *is_prime, const BIGNUM *n, BN_CTX *ctx)
+bn_fermat(int *is_prime, const BIGNUM *n, const BIGNUM *n_minus_one,
+    const BIGNUM *k, int s, BIGNUM *base, BN_CTX *ctx, BN_MONT_CTX *mctx)
 {
-       BIGNUM *n_minus_one, *k, *x;
-       int i, s;
+       int ret = 0;
+       int i;
+
+       /* Sanity check: ensure that 1 < base < n - 1. */
+       if (BN_cmp(base, BN_value_one()) <= 0 || BN_cmp(base, n_minus_one) >= 0)
+               goto err;
+
+       if (!BN_mod_exp_mont_ct(base, base, k, n, ctx, mctx))
+               goto err;
+
+       if (BN_is_one(base) || BN_cmp(base, n_minus_one) == 0) {
+               *is_prime = 1;
+               goto done;
+       }
+
+       /* Loop invariant: base is neither 1 nor -1 (mod n). */
+       for (i = 1; i < s; i++) {
+               if (!BN_mod_sqr(base, base, n, ctx))
+                       goto err;
+
+               /* n is a pseudoprime for base. */
+               if (BN_cmp(base, n_minus_one) == 0) {
+                       *is_prime = 1;
+                       goto done;
+               }
+
+               /* n is composite: there's a square root of unity != 1 or -1. */
+               if (BN_is_one(base)) {
+                       *is_prime = 0;
+                       goto done;
+               }
+       }
+
+       /*
+        * If we get here, n is definitely composite: base^(n-1) != 1.
+        */
+
+       *is_prime = 0;
+
+ done:
+       ret = 1;
+
+ err:
+       return ret;
+}
+
+/*
+ * Miller-Rabin primality test for base 2 and for |rounds| of random bases.
+ * On success: is_prime == 0 implies n is composite - the converse is false.
+ */
+
+static int
+bn_miller_rabin(int *is_prime, const BIGNUM *n, BN_CTX *ctx, size_t rounds)
+{
+       BN_MONT_CTX *mctx = NULL;
+       BIGNUM *base, *k, *n_minus_one, *three;
+       size_t i;
+       int s;
        int ret = 0;
 
        BN_CTX_start(ctx);
 
-       if ((n_minus_one = BN_CTX_get(ctx)) == NULL)
+       if ((base = BN_CTX_get(ctx)) == NULL)
                goto err;
        if ((k = BN_CTX_get(ctx)) == NULL)
                goto err;
-       if ((x = BN_CTX_get(ctx)) == NULL)
+       if ((n_minus_one = BN_CTX_get(ctx)) == NULL)
+               goto err;
+       if ((three = BN_CTX_get(ctx)) == NULL)
                goto err;
 
        if (BN_is_word(n, 2) || BN_is_word(n, 3)) {
@@ -344,43 +415,56 @@ bn_miller_rabin_base_2(int *is_prime, co
                goto err;
 
        /*
-        * If 2^k is 1 or -1 (mod n) then n is a 2-pseudoprime.
+        * Montgomery setup for n.
         */
 
-       if (!BN_set_word(x, 2))
+       if ((mctx = BN_MONT_CTX_new()) == NULL)
                goto err;
-       if (!BN_mod_exp_ct(x, x, k, n, ctx))
+
+       if (!BN_MONT_CTX_set(mctx, n, ctx))
                goto err;
 
-       if (BN_is_one(x) || BN_cmp(x, n_minus_one) == 0) {
-               *is_prime = 1;
+       /*
+        * Perform a Miller-Rabin test for base 2 as required by BPSW.
+        */
+
+       if (!BN_set_word(base, 2))
+               goto err;
+
+       if (!bn_fermat(is_prime, n, n_minus_one, k, s, base, ctx, mctx))
+               goto err;
+       if (!*is_prime)
                goto done;
-       }
 
        /*
-        * If 2^{2^i k} == -1 (mod n) for some 1 <= i < s, then n is a
-        * 2-pseudoprime.
+        * Perform Miller-Rabin tests with 3 <= base < n - 1 to reduce risk of
+        * false positives in BPSW.
         */
 
-       for (i = 1; i < s; i++) {
-               if (!BN_mod_sqr(x, x, n, ctx))
+       if (!BN_set_word(three, 3))
+               goto err;
+
+       for (i = 0; i < rounds; i++) {
+               if (!bn_rand_interval(base, three, n_minus_one))
                        goto err;
-               if (BN_cmp(x, n_minus_one) == 0) {
-                       *is_prime = 1;
+
+               if (!bn_fermat(is_prime, n, n_minus_one, k, s, base, ctx, mctx))
+                       goto err;
+               if (!*is_prime)
                        goto done;
-               }
        }
 
        /*
-        * If we got here, n is definitely composite.
+        * If we got here, we have a Miller-Rabin pseudoprime.
         */
 
-       *is_prime = 0;
+       *is_prime = 1;
 
  done:
        ret = 1;
 
  err:
+       BN_MONT_CTX_free(mctx);
        BN_CTX_end(ctx);
 
        return ret;
@@ -392,7 +476,7 @@ bn_miller_rabin_base_2(int *is_prime, co
  */
 
 int
-bn_is_prime_bpsw(int *is_prime, const BIGNUM *n, BN_CTX *in_ctx)
+bn_is_prime_bpsw(int *is_prime, const BIGNUM *n, BN_CTX *in_ctx, size_t rounds)
 {
        BN_CTX *ctx = NULL;
        BN_ULONG mod;
@@ -424,12 +508,10 @@ bn_is_prime_bpsw(int *is_prime, const BI
        if (ctx == NULL)
                goto err;
 
-       if (!bn_miller_rabin_base_2(is_prime, n, ctx))
+       if (!bn_miller_rabin(is_prime, n, ctx, rounds))
                goto err;
        if (!*is_prime)
                goto done;
-
-       /* XXX - Miller-Rabin for random bases? See FIPS 186-4, Table C.1. */
 
        if (!bn_strong_lucas_selfridge(is_prime, n, ctx))
                goto err;
Index: bn/bn_local.h
===================================================================
RCS file: /cvs/src/lib/libcrypto/bn/bn_local.h,v
retrieving revision 1.21
diff -u -p -r1.21 bn_local.h
--- bn/bn_local.h       25 Apr 2023 17:59:41 -0000      1.21
+++ bn/bn_local.h       28 Apr 2023 07:56:01 -0000
@@ -324,7 +324,7 @@ int bn_copy(BIGNUM *dst, const BIGNUM *s
 int bn_isqrt(BIGNUM *out_sqrt, int *out_perfect, const BIGNUM *n, BN_CTX *ctx);
 int bn_is_perfect_square(int *out_perfect, const BIGNUM *n, BN_CTX *ctx);
 
-int bn_is_prime_bpsw(int *is_prime, const BIGNUM *n, BN_CTX *in_ctx);
+int bn_is_prime_bpsw(int *is_prime, const BIGNUM *n, BN_CTX *ctx, size_t 
rounds);
 
 __END_HIDDEN_DECLS
 #endif /* !HEADER_BN_LOCAL_H */
Index: bn/bn_prime.c
===================================================================
RCS file: /cvs/src/lib/libcrypto/bn/bn_prime.c,v
retrieving revision 1.31
diff -u -p -r1.31 bn_prime.c
--- bn/bn_prime.c       25 Apr 2023 19:57:59 -0000      1.31
+++ bn/bn_prime.c       28 Apr 2023 07:56:01 -0000
@@ -195,12 +195,12 @@ BN_generate_prime_ex(BIGNUM *ret, int bi
                goto err;
 
        if (!safe) {
-               if (!bn_is_prime_bpsw(&is_prime, ret, ctx))
+               if (!bn_is_prime_bpsw(&is_prime, ret, ctx, 1))
                        goto err;
                if (!is_prime)
                        goto loop;
        } else {
-               if (!bn_is_prime_bpsw(&is_prime, ret, ctx))
+               if (!bn_is_prime_bpsw(&is_prime, ret, ctx, 1))
                        goto err;
                if (!is_prime)
                        goto loop;
@@ -213,7 +213,7 @@ BN_generate_prime_ex(BIGNUM *ret, int bi
                if (!BN_rshift1(p, ret))
                        goto err;
 
-               if (!bn_is_prime_bpsw(&is_prime, p, ctx))
+               if (!bn_is_prime_bpsw(&is_prime, p, ctx, 1))
                        goto err;
                if (!is_prime)
                        goto loop;
@@ -243,8 +243,14 @@ BN_is_prime_fasttest_ex(const BIGNUM *a,
 {
        int is_prime;
 
+       if (checks < 0)
+               return -1;
+
+       if (checks == BN_prime_checks)
+               checks = BN_prime_checks_for_size(BN_num_bits(a));
+
        /* XXX - tickle BN_GENCB in bn_is_prime_bpsw(). */
-       if (!bn_is_prime_bpsw(&is_prime, a, ctx_passed))
+       if (!bn_is_prime_bpsw(&is_prime, a, ctx_passed, checks))
                return -1;
 
        return is_prime;
Index: man/BN_generate_prime.3
===================================================================
RCS file: /cvs/src/lib/libcrypto/man/BN_generate_prime.3,v
retrieving revision 1.20
diff -u -p -r1.20 BN_generate_prime.3
--- man/BN_generate_prime.3     24 Nov 2022 19:06:38 -0000      1.20
+++ man/BN_generate_prime.3     28 Apr 2023 07:56:01 -0000
@@ -84,7 +84,7 @@
 .Nm BN_is_prime ,
 .Nm BN_is_prime_fasttest
 .\" Nm BN_prime_checks_for_size is intentionally undocumented
-.\" because it is no longer used by LibreSSL.
+.\" because it should not be used outside of libcrypto.
 .Nd generate primes and test for primality
 .Sh SYNOPSIS
 .In openssl/bn.h
@@ -178,18 +178,33 @@ test whether the number
 .Fa a
 is prime.
 In LibreSSL, both functions behave identically,
-use the Baillie-Pomerance-Selfridge-Wagstaff algorithm,
-and ignore the
+use the Baillie-Pomerance-Selfridge-Wagstaff algorithm
+combined with
 .Fa checks
-and
+rounds of the Miller-Rabin test.
+They ignore the
 .Fa do_trial_division
-arguments.
+argument.
 .Pp
 It is unknown whether any composite number exists that the
 Baillie-PSW algorithm misclassifies as a prime.
 Some suspect that there may be infinitely many such numbers,
 but not a single one is currently known.
 It is known that no such number exists below 2\(ha64.
+.Pp
+The number of Miller-Rabin rounds performed by
+.Fn BN_is_prime_fasttest_ex
+and
+.Fn BN_is_prime_ex
+is determined by
+.Fa checks :
+If it is positive, it is used as the number of rounds.
+If
+.Fa checks
+is zero or the special value
+.Dv BN_prime_checks ,
+a suitable number of rounds is calculated from the bit length of
+.Fa a .
 .Pp
 If
 .Dv NULL

Reply via email to