On Fri, Apr 28, 2023 at 10:23:15AM +0200, Theo Buehler wrote:
> 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;
Nit: you call this a pseudo-prime - which I agree with, but the variable you
have added
is "is_prime" - maybe call it "maybe-prime" or "pseudo-prime"
>
> 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.
You are now pre-checking with miller rabin to reduce the likelyhood of
a composite number of any size, but you are not explaining this here.
The point of the miller-rabin pre-test is to cheaply reduce the likelyhood
of a composite number being fed to Ballie-PSW to "very small" so that even
if the future hypothesis finds such numbers we are still protected,
and this should be explained here.
> +.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