土法炼钢 · 系统与基础设施

素性测试与素数生成:Miller-Rabin、BPSW 与 OpenSSL/FIPS 186-5

文章导航

分类入口
algorithmscryptography
标签入口
#miller-rabin#primality-test#strong-pseudoprime#baillie-psw#aks#openssl#fips-186-5#rsa-keygen#carmichael

目录

关于素性测试,流传着三种说法:Miller-Rabin 要跑 64 轮才能把误判率压到 \(2^{-128}\);竞赛里常用的 7 个底 \(\{2,3,5,7,11,13,37\}\) 覆盖全部 64 位整数;OpenSSL 生成 RSA 素数时用的是 Baillie-PSW 加增量搜索。三句话都不对。OpenSSL 3.6.2 生成 2048 位 RSA 密钥时,每个 1024 位素数只做 5 轮 Miller-Rabin,依据是 FIPS 186-5 的平均情况误差分析;那 7 个底在 \(2^{64}\) 以下有 52 个合数漏网,最小的是 4498414682539051;OpenSSL 的 BN_check_prime 里没有 Lucas 测试,素数生成循环在一次 Miller-Rabin 失败后会重新抽一个随机数,而不是继续往后找。

本文先用实验把 Miller-Rabin 的两种误差讲清楚:对任意合数成立的最坏情况 \(4^{-t}\),和只对随机候选成立、小得多的平均情况界;再用 \(2^{64}\) 以下全部 2-伪素数表验证确定性底数集合;然后梳理 BPSW、AKS 的谱系;最后逐行对照 OpenSSL 3.6.2 源码和 FIPS 186-5,说明工业级素数生成到底做了什么。

复现程序都在文章目录的 reproduce/ 下,编译运行命令和外部数据的下载地址见其中的 README.md:

一、试除与小素数筛:能排除多少候选

试除法(trial division)检查 \(2\) 到 \(\lfloor\sqrt n\rfloor\) 之间有没有因子。若 \(n=ab\) 且 \(1<a\le b\),则 \(a\le\sqrt n\),所以检查到 \(\sqrt n\) 就够了。代价是 \(O(\sqrt n)\) 次除法;对 \(k\) 位整数这是 \(2^{k/2}\) 量级,对输入长度是指数的。1024 位整数要试到 \(2^{512}\),完全不可行。

试除在大数素性测试里的作用是预过滤:先用前几十到几千个小素数排除掉大部分合数,再对剩下的候选跑昂贵的模幂。能排除多少,有精确答案。随机奇数不被任何奇素数 \(p\le B\) 整除的概率是

\[ \prod_{3\le p\le B}\left(1-\frac1p\right)\approx\frac{2e^{-\gamma}}{\ln B}, \]

右边来自 Mertens 第三定理 \(\prod_{p\le B}(1-1/p)\sim e^{-\gamma}/\ln B\),乘 2 是因为奇数已经排除了因子 2,\(\gamma\approx0.5772\) 是 Euler 常数。reproduce/prime_gen_sim.c 按乘积精确计算:

试除用的素数 最大素数 \(B\) 奇数中被排除 Mertens 估计 存活率
OpenSSL,\(\le512\) 位(64 个) 311 80.63% 80.44% 0.1937
OpenSSL,\(\le1024\) 位(128 个) 719 83.03% 82.93% 0.1697
OpenSSL,\(\le2048\) 位(384 个) 2657 85.78% 85.76% 0.1422
OpenSSL,\(\le4096\) 位(1024 个) 8161 87.54% 87.53% 0.1246
OpenSSL,\(>4096\) 位(2048 个) 17863 88.54% 88.53% 0.1146
前 100 个素数 541 82.25% 82.16% 0.1775
前 500 个素数 3571 86.30% 86.27% 0.1370
前 2000 个素数 17389 88.51% 88.50% 0.1149
前 10000 个素数 104729 90.29% 90.29% 0.0971

“\(k\) 个素数”包含 2,所以奇数实际只用 3 到 \(B\) 试除,与 OpenSSL 从 primes[1] 开始循环的写法一致。对 \(10^6\) 个随机 1024 位奇数做实测(用它们与 3 到 719 之积的最大公约数判断),存活 169,993 个,即 0.1700,与精确值 0.1697 一致。

表里能看到收益递减:从 128 个素数加到 2048 个,存活率只从 0.1697 降到 0.1146,筛的工作量却涨了 16 倍。存活率按 \(1/\ln B\) 衰减,再多的小素数也只能把候选压到一成左右,剩下的必须交给概率测试。

二、Fermat 测试与 Carmichael 数

Fermat 小定理:\(p\) 为素数且 \(\gcd(a,p)=1\) 时,\(a^{p-1}\equiv1\pmod p\)。逆否命题给出一个测试:若 \(a^{n-1}\not\equiv1\pmod n\),则 \(n\) 必为合数,\(a\) 称为 Fermat 证人(witness);若合数 \(n\) 满足 \(a^{n-1}\equiv1\),\(a\) 称为 Fermat 说谎者(liar),\(n\) 称为以 \(a\) 为底的伪素数(pseudoprime)。

问题在于 Carmichael 数(Carmichael number):对所有与 \(n\) 互素的 \(a\) 都有 \(a^{n-1}\equiv1\pmod n\) 的合数。Korselt(1899)给出了判据:合数 \(n\) 是 Carmichael 数,当且仅当 \(n\) 无平方因子,且对每个素因子 \(p\) 都有 \((p-1)\mid(n-1)\)。Alford、Granville、Pomerance(Annals of Mathematics, 1994)证明了 Carmichael 数有无穷多个。

reproduce/liars.c 对 20000 以下的每个奇合数枚举全部底数,找到 9 个 Carmichael 数:561、1105、1729、2465、2821、6601、8911、10585、15841,与 Korselt 判据逐个吻合。对它们,只有与 \(n\) 不互素的底才能揭穿合数身份。如果 Carmichael 数的素因子都很大,这样的底极少,多跑几轮 Fermat 测试也无济于事。

三、Miller-Rabin:强伪素数与 1/4 界

从 1 的平方根到强可能素数

Miller-Rabin 在 Fermat 测试上加了一条约束:模素数 \(p\) 时,\(x^2\equiv1\) 只有 \(x\equiv\pm1\) 两个解,因为 \(p\mid(x-1)(x+1)\) 意味着 \(p\) 整除其中一个因子。

把 \(n-1\) 写成 \(2^s d\)(\(d\) 为奇数)。

命题:若 \(n\) 为奇素数,则对任意 \(1\le a\le n-1\),要么 \(a^d\equiv1\pmod n\),要么存在 \(0\le r<s\) 使 \(a^{2^r d}\equiv-1\pmod n\)。

证明:考察序列 \(a^d, a^{2d}, \dots, a^{2^s d}=a^{n-1}\),每一项是前一项的平方,最后一项由 Fermat 小定理等于 1。若 \(a^d\equiv1\),命题成立;否则设 \(a^{2^r d}\) 是序列中第一个等于 1 的项,\(r\ge1\),则 \(x=a^{2^{r-1}d}\) 满足 \(x^2\equiv1\) 且 \(x\not\equiv1\),只能 \(x\equiv-1\)。\(\square\)

满足上述条件的奇合数称为以 \(a\) 为底的强伪素数(strong pseudoprime),\(a\) 称为强说谎者(strong liar)。测试过程就是算出 \(x_0=a^d\bmod n\),再反复平方至多 \(s-1\) 次:遇到 \(n-1\) 判通过;遇到 1 而前一项不是 \(\pm1\),或者平方到头也没出现 \(n-1\),判合数。

Miller-Rabin 平方链的三个例子:素数 97 以 5 为底时,序列 28、8、64、22 之后出现 96 即 -1,判为强可能素数;Carmichael 数 561 以 2 为底时,序列 263、166、67、1 中 67 的平方为 1 但 67 不是正负 1,判为合数,并由 gcd(66,561)=33 和 gcd(68,561)=17 得到因子;2047 以 2 为底时 2 的 1023 次方直接等于 1,2 是强说谎者,而以 3 为底得到 1565,既不是正负 1 且 s=1,判为合数

图中第二行是 Fermat 测试漏掉的情形:561 对底 2 满足 \(2^{560}\equiv1\),但平方链在 67 处出现了 1 的非平凡平方根。非平凡平方根还直接给出因子:\(67^2-1=(67-1)(67+1)\equiv0\),于是 \(\gcd(66,561)=33\)、\(\gcd(68,561)=17\)。第三行说明强伪素数确实存在:\(2^{11}=2048\equiv1\pmod{2047}\),而 \(1023=11\times93\),所以 \(2^{1023}\equiv1\),最小的以 2 为底的强伪素数就是 \(2047=23\times89\)。

Monier–Rabin 界:至多四分之一的底会说谎

定理(Monier 1980;Rabin 1980):奇合数 \(n>9\) 的强说谎者个数 \(S(n)\) 满足 \(S(n)\le\varphi(n)/4\),其中 \(\varphi\) 是 Euler 函数。

Monier 给出了 \(S(n)\) 的精确公式。设 \(n=\prod_{i=1}^{\omega}p_i^{e_i}\),\(p_i-1=2^{s_i}d_i\),\(\nu=\min_i s_i\),\(d\) 为 \(n-1\) 的奇数部分,则

\[ S(n)=\left(1+\frac{2^{\omega\nu}-1}{2^{\omega}-1}\right)\prod_{i=1}^{\omega}\gcd(d,d_i). \]

以 \(561=3\cdot11\cdot17\) 为例:\(\omega=3\),\(s_i=1,1,4\),所以 \(\nu=1\);\(d=35\),\(d_i=1,5,1\),\(\prod\gcd=5\)。代入得 \(S(561)=2\times5=10\),而 \(\varphi(561)=320\),即只有 3.1% 的底会说谎;作为对比,561 的 Fermat 说谎者是全部 320 个与它互素的底。reproduce/monier_check.py 用这个公式核对了 liars.c 暴力枚举的全部 7738 个奇合数,没有一个不符。定理的证明就是按 \(\omega=1\)(素数幂)、\(\omega=2\)、\(\omega\ge3\) 分情况估计这个公式。

20000 以下奇合数中说谎底数占 phi(n) 的比例:上图是 Fermat 说谎者,9 个 Carmichael 数都在 1.0 处,其余合数大多接近 0;下图是强说谎者,所有点都在 1/4 虚线以下,只有 n=9 为 1/3,n=15 和 n=8911 恰好落在 1/4 上,Carmichael 数的强说谎者比例在 0.03 到 0.25 之间

实测结果(liars 20000):

所以 \(1/4\) 是一个很少被取到的最坏情况界。对固定的合数 \(n\),独立随机选 \(t\) 个底全部说谎的概率不超过 \(4^{-t}\),这对任何输入都成立,包括对手精心构造的合数。

谱系

今天的”Miller-Rabin”指的就是 Rabin 的随机化版本。

四、平均情况:为什么 FIPS 只要 4 到 5 轮

\(4^{-t}\) 回答的是”一个给定的合数通过 \(t\) 轮的概率”。生成素数时关心的是另一个量:一个随机的 \(k\) 位奇数通过了 \(t\) 轮,它仍是合数的概率 \(p_{k,t}\)。FIPS 186-5 附录 C.1 专门用 WARNING 段区分这两者:前者对验证他人提供的数重要,后者对生成者重要。

两者差距很大,原因有二:随机合数的强说谎者比例通常远低于 1/4(上一节 95% 的合数不到 1%);而且通过测试的数里素数占了绝大多数。Damgård、Landrock、Pomerance(Math. Comp., 1993,下称 DLP)给出了显式上界,FIPS 186-5 附录 C.1 把它写成公式 (2):对 \(3\le M\le 2\sqrt{k-1}-1\),

\[ p_{k,t}\le 2.00743\ln2\cdot k\cdot2^{-k}\left(2^{k-2-Mt}+\frac{8(\pi^2-6)}{3}\,2^{k-2}\sum_{m=3}^{M}2^{m-(m-1)t}\sum_{j=2}^{m}\frac{1}{2^{\,j+(k-1)/j}}\right), \]

对 \(M\) 取最小值,再找满足目标概率的最小 \(t\)。reproduce/fips_c1.py 用 log-sum-exp 形式实现这个算法,重算的轮数与 FIPS 186-5 Table B.1 全部一致:

候选位数 目标 \(2^{-100}\) 与安全强度匹配的目标
\(p,q\) 1024 位 4 \(2^{-112}\):5
\(p,q\) 1536 位 3 \(2^{-128}\):4
\(p,q\) 2048 位 2 \(2^{-144}\):4
辅助素数 \(>140\) 位 32 \(2^{-112}\):38
辅助素数 \(>170\) 位 27 \(2^{-128}\):41
辅助素数 \(>200\) 位 22 \(2^{-144}\):44
log2 p(k,t) 随轮数 t 的变化:灰色虚线是最坏情况 4 的 -t 次方,t=10 时只有 2 的 -20 次方;蓝、绿、紫三条实线分别是 512、1024、2048 位随机候选的平均情况界,1024 位时 t=5 已低于 2 的 -112 次方,2048 位时 t=4 已低于 2 的 -144 次方;橙色点线标出 2 的 -100、-112、-144 次方三个目标

1024 位时,\(t=1,2,3,4,5\) 对应的 \(\log_2 p_{1024,t}\) 分别是 \(-42.9\)、\(-72.8\)、\(-93.1\)、\(-109.8\)、\(-124.2\),而最坏情况界只有 \(-2\)、\(-4\)、\(-6\)、\(-8\)、\(-10\)。位数越大,平均情况越好:随机大整数的强说谎者比例更低。FIPS 186-5 还指出,在同样的随机候选假设下,\(k\ge51\) 时可以证明 \(p_{k,t}\le4^{-t}\)。所以取 \(t=-\log_2(p_{\text{target}})/2\) 轮,对生成和验证都够用,只是保守。

用同一程序还能复核 OpenSSL 文档(doc/man3/BN_generate_prime.pod)的说法。OpenSSL 生成素数时至少做 64 轮,文档声称 512、1024、2048 位素数的误判率分别为 \(2^{-287}\)、\(2^{-435}\)、\(2^{-648}\),更大的素数(128 轮)低于 \(2^{-882}\)。算出来是 \(2^{-287.5}\)、\(2^{-435.9}\)、\(2^{-648.2}\),2049 位 128 轮为 \(2^{-883.2}\),全部吻合。

这个平均情况在模拟里也看得到。第八节的生成实验一共测试了约 29 万个进入 Miller-Rabin 的 1024 位合数候选,没有一个通过哪怕第 1 轮。

五、64 位整数的确定性判定

最小强伪素数 \(\psi_k\) 与底数集合

对有界的 \(n\),可以选一组固定底,使测试变成确定性的。记 \(\psi_k\) 为以前 \(k\) 个素数为底都通过的最小强伪素数,\(\psi_k\) 以下用前 \(k\) 个素数做底就不会出错。

检验”某组底在 \(2^{64}\) 以下确定”有一个干净的办法:以 2 为底的强伪素数都是以 2 为底的 Fermat 伪素数,而 Jan Feitsma 计算、William Galway 整理发布了 \(2^{64}\) 以下全部 2-Fermat 伪素数的列表。只要底数集合含 2,逐个检查列表里的数有没有通过全部底即可。reproduce/psp64_verify.c 读入整张表:共 118,968,378 个 2-伪素数,其中 31,894,014 个是 2-强伪素数。算出的 \(\psi_k\) 与文献一致:

\(k\) 底 \(\psi_k\) 出处
1 2 2047 Pomerance–Selfridge–Wagstaff 1980
2 2, 3 1373653 同上
3 2 到 5 25326001 同上
4 2 到 7 3215031751 同上
5 2 到 11 2152302898747 Jaeschke 1993
6 2 到 13 3474749660383 同上
7、8 2 到 17、2 到 19 341550071728321 同上
9、10、11 2 到 23、29、31 3825123056546413051 Jiang–Deng 2014
12 2 到 37 318665857834031151167461(约 \(3.19\times10^{23}\)) Sorenson–Webster 2017
13 2 到 41 3317044064679887385961981(约 \(3.32\times10^{24}\)) 同上

最后两行超出 \(2^{64}\approx1.84\times10^{19}\),程序只能验证”\(2^{64}\) 以下不存在”;它们的精确值引自 Sorenson 与 Webster。常见的错误是把 \(3.3\times10^{24}\) 写成 12 个底的适用范围,那其实是 13 个底的界;12 个底的界是 \(\psi_{12}\approx3.19\times10^{23}\),依然远大于 \(2^{64}\)。

几组常见底数集合在 \(2^{64}\) 以下的漏判数:

底数集合 漏判的合数 最小漏判
\(\{2,7,61\}\)(Jaeschke) 145,327 4759123141
\(\{2,3,5,7,11,13,37\}\) 52 4498414682539051
前 11 个素数(2 到 31) 1 3825123056546413051
前 12 个素数(2 到 37) 0
Sinclair 的 7 个底 \(\{2, 325, 9375, 28178, 450775, 9780504, 1795265022\}\) 0

\(\{2,7,61\}\) 在 4759123141 以下是确定的,这个数大于 \(2^{32}\),所以它适合 32 位整数。\(\{2,3,5,7,11,13,37\}\) 的最小反例 \(4498414682539051=46411\times232051\times417691\) 通过了全部 7 个底,它不能用于 64 位。Jim Sinclair 在 2011 年找到的 7 底集合(发布在 miller-rabin.appspot.com)经整表验证没有漏判,比前 12 个素数少 5 次模幂。

实现要点

reproduce/mr64.h 的核心是 20 来行:

typedef unsigned __int128 u128;

static inline uint64_t mulmod(uint64_t a, uint64_t b, uint64_t m)
{
    return (uint64_t)((u128)a * b % m);
}

static inline bool sprp(uint64_t n, uint64_t a)   /* n odd, n > 2 */
{
    uint64_t d = n - 1;
    int s = __builtin_ctzll(d);
    d >>= s;

    a %= n;
    if (a == 0)
        return true;

    uint64_t x = powmod(a, d, n);
    if (x == 1 || x == n - 1)
        return true;
    for (int r = 1; r < s; r++) {
        x = mulmod(x, x, n);
        if (x == n - 1)
            return true;
        if (x == 1)
            return false;
    }
    return false;
}

有两处容易写错,reproduce/mr64_test.c 各自构造了反例:

交叉测试结果:

在 GRH 下的确定性

Miller(JCSS, 1976)证明:若扩展黎曼猜想成立,最小的 Miller-Rabin 证人是 \(O((\ln n)^2)\),由此得到 \(O((\log n)^4)\) 的确定性判定,但他没有给出常数。Bach(Math. Comp., 1990)给出显式界:在 ERH 下,每个奇合数都有小于 \(2(\ln n)^2\) 的证人。对 \(n<2^{64}\),这个界约为 3936 个底,远不如上面 7 个底的整表验证结果。它的意义是理论上的:如果 ERH 成立,素性判定就有简单的确定性多项式算法。

六、Baillie-PSW:两类伪素数几乎不相交

组合方式

Pomerance、Selfridge、Wagstaff(Math. Comp. 35(151), 1980)与 Baillie、Wagstaff(Math. Comp. 35(152), 1980)提出把两种结构不同的测试组合起来,后人称为 Baillie-PSW(BPSW):

  1. 以 2 为底的强可能素数测试;
  2. 强 Lucas 可能素数测试(strong Lucas probable prime test)。

Lucas 测试用的是 Lucas 序列 \(U_k,V_k\),它们由 \(x^2-Px+Q\) 的两个根 \(\alpha,\beta\) 定义:\(U_k=(\alpha^k-\beta^k)/(\alpha-\beta)\),\(V_k=\alpha^k+\beta^k\)。取判别式 \(D=P^2-4Q\) 满足 Jacobi 符号 \(\left(\frac{D}{n}\right)=-1\),此时对素数 \(n\) 有 \(n\mid U_{n+1}\)。把 \(n+1\) 写成 \(2^s d\),强 Lucas 测试要求 \(U_d\equiv0\),或存在 \(0\le r<s\) 使 \(V_{2^r d}\equiv0\pmod n\)。参数按 Selfridge 的方法 A 选取:\(D\) 依次取 \(5,-7,9,-11,\dots\) 中第一个满足 \(\left(\frac{D}{n}\right)=-1\) 的值,\(P=1\),\(Q=(1-D)/4\)。完全平方数找不到这样的 \(D\),要先排除。

直观上,Miller-Rabin 检验的是乘法群 \((\mathbb Z/n\mathbb Z)^*\) 里与 \(n-1\) 有关的结构,Lucas 测试检验的是二次扩张里与 \(n+1\) 有关的结构。Baillie 与 Wagstaff(1980,第 1404–1405 页)观察到,2-伪素数和 Lucas 伪素数对小模数分别倾向于落在 \(+1\) 和 \(-1\) 的剩余类里,两类伪素数因此很难重合。

mr64.h 实现了这个测试。强 Lucas 伪素数单独来看并不少:100000 以下有 12 个(5459、5777、10877、16109、18971、22499、24569、25199、40309、58519、75077、97439,与 SymPy 的 is_strong_lucas_prp 一致),\(10^8\) 以下有 505 个。但 \(2^{64}\) 以下 31,894,014 个 2-强伪素数中,没有一个同时通过强 Lucas 测试,即 BPSW 在 \(2^{64}\) 以下没有反例。Baillie、Fiori、Wagstaff(2021)报告了更强的结论:118,968,378 个 2-伪素数中没有一个是 Lucas 伪素数。他们用的参数方法 A* 在 \(Q=-1\) 时把 \(P,Q\) 都改成 5,并证明方法 A 与 A* 产生的 Lucas 伪素数和强 Lucas 伪素数完全相同,所以两者的结果可以直接对照。

反例:没找到,但预计存在

BPSW 的作者为反例设立的 620 美元悬赏,40 年后仍无人领取(Baillie–Fiori–Wagstaff 2021)。另一方面,Pomerance 在 1984 年的短文 “Are there counterexamples to the Baillie-PSW primality test?” 中改造了 Erdős 构造 Carmichael 数的启发式论证:反例应当有无穷多个,只是没有构造方法,也不知道最小的有多大。这是素性测试里最有名的开放问题之一,第十节再谈。

谁在用

七、确定性证明:AKS、ECPP 与 Lucas–Lehmer

AKS

Agrawal、Kayal、Saxena 在 2002 年发布预印本 “PRIMES is in P”,正式论文发表于 Annals of Mathematics 160(2), 2004,给出第一个无条件的确定性多项式时间素性判定,并获得 2006 年 Gödel 奖和 Fulkerson 奖。出发点是多项式恒等式:对与 \(n\) 互素的 \(a\),\(n\) 为素数当且仅当

\[ (X+a)^n\equiv X^n+a\pmod n. \]

直接验证要展开 \(n+1\) 个系数。AKS 改为在模 \(X^r-1\) 和 \(n\) 的意义下,对 \(O(\sqrt r\log n)\) 个 \(a\) 验证,其中 \(r=O(\log^5 n)\) 量级,从而得到多项式时间。

复杂度的说法常被写错:

AKS 的价值在于理论,密码学库生成或验证素数时并不用它。

ECPP

需要可验证的素性证书时用的是椭圆曲线素性证明(elliptic curve primality proving,ECPP,Atkin–Morain, Math. Comp., 1993)。它输出一串证书,别人可以比生成快得多地验证;运行时间的分析依赖启发式假设。本文不展开。

Lucas–Lehmer

Mersenne 数 \(M_p=2^p-1\)(\(p\) 为奇素数)有专用判定:令 \(S_0=4\),\(S_i=S_{i-1}^2-2\),则 \(M_p\) 为素数当且仅当 \(S_{p-2}\equiv0\pmod{M_p}\)。

它快的原因常被说错。Lucas–Lehmer 需要 \(p-2\) 次模 \(M_p\) 平方;对 \(M_p\) 做一次 Fermat 或 Miller-Rabin 测试,模幂的指数有 \(p\) 位,同样约需 \(p\) 次平方。两者的平方次数相当,Lucas–Lehmer 的优势是确定性,而不是更少的运算。真正让 Mersenne 数好算的是:

GIMPS 当前的流程也说明了这一点。客户端先做 Fermat 可能素数(PRP)测试,出现候选后再做确认。已知最大的 Mersenne 素数 \(M_{136279841}\) 有 41,024,320 位十进制数字:2024 年 10 月 11 日,一次 PRP 测试(NVIDIA A100 上)报告它可能是素数;10 月 12 日,Lucas–Lehmer 测试(H100 上)确认,官方发现日期记为 10 月 12 日(mersenne.org 公告)。

八、OpenSSL 3.6.2 怎么判素、怎么生成素数

以下源码引自 OpenSSL 3.6.2(tag openssl-3.6.2),路径相对于源码根目录。

BN_check_prime:按最坏情况设计

BN_is_prime_ex 与 BN_is_prime_fasttest_ex 在 3.0 被标为弃用(CHANGES.md,Kurt Roeckx),取代它们的是 3.0 新增的 BN_check_prime(p, ctx, cb),它没有轮数参数。crypto/bn/bn_prime.c 中决定规模的两个函数是:

static int calc_trial_divisions(int bits)
{
    if (bits <= 512)
        return 64;
    else if (bits <= 1024)
        return 128;
    else if (bits <= 2048)
        return 384;
    else if (bits <= 4096)
        return 1024;
    return NUMPRIMES;           /* 2048 */
}

/*
 * Use a minimum of 64 rounds of Miller-Rabin, which should give a false
 * positive rate of 2^-128. If the size of the prime is larger than 2048
 * the user probably wants a higher security level than 128, so switch
 * to 128 rounds giving a false positive rate of 2^-256.
 */
static int bn_mr_min_checks(int bits)
{
    if (bits > 2048)
        return 128;
    return 64;
}

BN_check_prime 调用 ossl_bn_check_prime(p, 0, ctx, 1, cb),后者把轮数钳到至少 bn_mr_min_checks(),然后进入 bn_is_prime_int():先用 primes[1] 到 primes[K-1] 试除(\(K\) 取自 calc_trial_divisions),再调用 ossl_bn_miller_rabin_is_prime(),每轮用 BN_priv_rand_range_ex 在 \([2, w-2]\) 中取随机底。弃用的两个函数也走同一个 ossl_bn_check_prime,传入更小的轮数同样会被钳到 64 或 128。注释里的 \(2^{-128}\) 就是 \(4^{-64}\),是最坏情况界。检查外来的数(例如对方提供的 DH 参数)需要的正是这一种。

include/openssl/bn.h 里还保留着弃用的宏 BN_prime_checks_for_size(b):3747 位以上 3 轮,1345 位以上 4 轮,476 位以上 5 轮,依次到 55 位以上 27 轮、更小的 34 轮。注释说这张表按 FIPS 186-4 附录 F.1 的算法生成,是平均情况的轮数,3.x 的判素函数已经不再使用它。

BN_generate_prime_ex2:筛,失败就重抽

生成循环(crypto/bn/bn_prime.c,删去了 safe prime 与 add/rem 分支):

    int checks = bn_mr_min_checks(bits);
    /* ... */
loop:
    /* make a random number and set the top and bottom bits */
    if (!probable_prime(ret, bits, safe, mods, ctx))
        goto err;
    if (!BN_GENCB_call(cb, 0, c1++))
        goto err;
    i = bn_is_prime_int(ret, checks, ctx, 0, cb);   /* no trial division */
    if (i == -1)
        goto err;
    if (i == 0)
        goto loop;

probable_prime() 用 BN_priv_rand_ex(rnd, bits, BN_RAND_TOP_TWO, BN_RAND_BOTTOM_ODD, ...) 取一个最高两位为 1 的随机奇数,算出它对每个小素数的余数 mods[i],然后让 delta 从 0 起每次加 2,直到所有 (mods[i] + delta) % primes[i] 都不为 0,返回 rnd + delta。初始化 mods[i] 时,每个小素数要做一次大数除以机器字;之后每一步只做机器字运算。Miller-Rabin 失败后,goto loop 会重新调用 probable_prime(),也就是重新抽一个随机数。增量搜索只发生在筛的内部,不跨越 Miller-Rabin 失败。文件里的注释称这种快速筛法来自 PGP 中 Philip Zimmermann 的实现。

OpenSSL probable_prime 的增量筛示意:随机起点 1000021,用 3、5、7、11、13 五个素数筛,表格列出 delta 从 0 到 10 时各余数,余数为 0 的格子标红:1000021 被 11 整除,1000023 被 3 整除,1000025 被 5 和 13 整除,1000027 被 7 整除,1000029 被 3 整除,1000031 所有余数非零,进入 Miller-Rabin;它等于 41 乘 24391,被 Miller-Rabin 拒绝后,OpenSSL 回到 loop 重新抽随机数,而不是继续检查 1000033

图里的例子是真实数据:1000031 的最小素因子 41 超出了筛的范围,只能靠 Miller-Rabin 拒绝,它的 1000030 个底中只有 50 个是强说谎者。

reproduce/prime_gen_sim.c 按这个流程(GMP 大数,随机底,固定种子)生成 1024 位素数,统计每得到一个素数的开销:

筛用的素数个数 进入 MR 的候选 估计 \(s\cdot\ln N/2\) 筛步数(delta += 2) 模幂次数
1(不筛,200 个素数) 359.1 354.8 0 422.1
64 69.9 68.7 249.6 132.9
128(OpenSSL 1024 位默认) 63.7 60.2 272.3 126.7
384 47.5 50.5 256.2 110.5
2048 40.9 40.7 285.5 103.9

除第一行外每行生成 1000 个素数,每个素数做 64 轮。估计值的来历:\(k\) 位随机奇数是素数的概率约为 \(2/\ln N\),筛后存活的比例是 \(s\)(第一节的存活率),取 \(N\approx0.875\cdot2^{1024}\)(最高两位为 1 时的平均值)。实测与估计相差最多 6%,约为两倍抽样标准误(每组候选数近似几何分布,1000 个样本的标准误约为均值的 3%)。常见的”1024 位素数平均要试约 710 个候选”把偶数也算进去了;只取奇数时约为 355 个。

模幂次数揭示了轮数的真实代价。几乎所有合数候选在第 1 轮就被拒绝,所以每个素数的开销约为(合数候选数)+(轮数)。同样用 128 个素数筛:5 轮时每个素数 64.0 次模幂,64 轮时 125.6 次。64 轮比 5 轮贵约一倍,而不是 13 倍。

RSA 密钥生成走另一条路

openssl genrsa 2048 并不调用 BN_generate_prime_ex2。crypto/rsa/rsa_gen.c 的 rsa_keygen() 在非 FIPS 构建中这样分派:

    if (primes == 2
        && bits >= 2048
        && (e_value == NULL || BN_num_bits(e_value) > 16))
        ok = ossl_rsa_sp800_56b_generate_key(rsa, bits, e_value, cb);
    else
        ok = rsa_multiprime_keygen(rsa, bits, primes, e_value, cb);

\(65537=2^{16}+1\) 有 17 位,所以常见的 2048 位、\(e=65537\) 密钥走 SP 800-56B 路径(FIPS 构建中则总是走这条路径)。CHANGES.md 在 3.0 条目中记载,双素数 RSA 的默认生成方法改为 FIPS 186-4 B.3.6(Shane Lontis)。

flowchart TD
    A["rsa_keygen()"] --> B{"2 primes, bits >= 2048, e longer than 16 bits?"}
    B -- "yes, e.g. 2048-bit key with e = 65537" --> C["ossl_rsa_sp800_56b_generate_key()"]
    B -- "no" --> D["rsa_multiprime_keygen()"]
    D --> E["BN_generate_prime_ex2() per prime: sieve, 64 MR rounds (128 above 2048 bits)"]
    C --> F["aux primes p1, p2: random odd start of 141 / 171 / 201 bits, step +2"]
    F --> G["each step: trial division, then 38 / 41 / 44 MR rounds"]
    G --> H["Y = X + ((R - X) mod 2*r1*r2), then step Y by 2*r1*r2, at most 20 * nlen/2 steps"]
    H --> I["each step: gcd(Y - 1, e) = 1, trial division, 5 MR rounds (4 if nlen >= 3072)"]
    I --> J["repeat for q; require |Xp - Xq| and |p - q| > 2^(nlen/2 - 100)"]

crypto/bn/bn_rsa_fips186_4.c 中的轮数直接写着出处:

/*
 * Refer to FIPS 186-5 Table B.1 for minimum rounds of Miller Rabin
 * required for generation of RSA primes (p and q)
 */
static int bn_rsa_fips186_5_prime_MR_rounds(int nbits)
{
    if (nbits >= 3072)
        return 4;
    if (nbits >= 2048)
        return 5;
    return 0; /* Error */
}

辅助素数的轮数由 bn_rsa_fips186_5_aux_prime_MR_rounds() 给出,nlen 为 2048、3072、4096 位时分别是 38、41、44 轮,最小位长分别是 141、171、201 位。候选检测调用的是 ossl_bn_check_generated_prime():它总做试除,轮数照原样使用,不钳到 64。注释还说明,候选上限 imax 从 FIPS 186-4 的 \(5\cdot\text{nlen}/2\) 提高到 FIPS 186-5 附录 B.9 的 \(20\cdot\text{nlen}/2\)。据 Allen Roginsky 的分析,旧上限约有两百万分之一的失败概率。

genrsa -verbose 的进度字符可以直接印证这些轮数。apps/lib/s_cb.c 的 progress_cb 按回调事件 0、1、2、3 分别输出 .、+、*、换行。在本机运行三次 openssl genrsa -verbose -out /dev/null 2048,每次输出中长串 + 的长度都是 39、39、6、39、39、6。下面是其中一次的开头(截断):

Generating RSA key with 2048 bits
............+...+........+++++++++++++++++++++++++++++++++++++++*.+.+..+.......+.....+.+...

对照源码:

所以 OpenSSL 3.6.2 生成 2048 位 RSA 密钥时,\(p\) 和 \(q\) 各只做 5 轮 Miller-Rabin,没有 Lucas 测试。

九、FIPS 186-5 对 RSA 素数的要求

FIPS 186-5(2023)附录 A.1.1 对 IFC(整数分解类)密钥规定:

\(p,q\) 的生成方法有两大类:随机素数(A.1.2 可证素数、A.1.3 可能素数),以及带条件的素数(A.1.4 至 A.1.6)。带条件的素数要求 \(p-1\)、\(p+1\)、\(q-1\)、\(q+1\) 分别有辅助素因子 \(p_1,p_2,q_1,q_2\),其最小长度见 Table A.1:nlen 在 2048 到 3071 之间时大于 140 位,3072 到 4095 之间时大于 170 位,4096 及以上时大于 200 位。OpenSSL 走的是 A.1.6(全部为可能素数)。

A.1.3 的过程值得对照 OpenSSL:

也就是说,标准的”随机可能素数”方法每个候选都重新抽样,不做增量搜索。

附录 B.3 规定:

与 FIPS 186-4(2013)相比,186-4 Table C.2、C.3 还列有 512 位 \(p,q\) 的行(例如 \(2^{-80}\) 目标下 5 轮,辅助素数 28 轮),186-5 的生成过程则直接拒绝 2048 位以下的 nlen。

附录 C.2 解释了辅助素数的用意:选择它们的长度,是为了让”资源可观但不无限”的攻击者用 Lenstra 的椭圆曲线分解法(ECM)比用 Pollard \(p-1\)、Williams \(p+1\) 或各种循环攻击更划算;ECM 本身除了把 \(p,q\) 选大之外无法防御。

十、争论与开放问题

BPSW 会不会有反例

一方是 \(2^{64}\) 以下没有反例,四十多年来 620 美元的悬赏无人领取,第六节的实验对全部 31,894,014 个 2-强伪素数复核了这一点;另一方是 Pomerance(1984)的启发式论证:反例有无穷多个。两者并不矛盾:启发式预测的反例可能极大,远超任何穷举范围。Baillie、Fiori、Wagstaff(Math. Comp., 2021;预印本 arXiv:2006.14425)因此提出一个加强版,额外检查 \(V_{n+1}\equiv2Q\) 与 \(Q^{(n+1)/2}\equiv Q\cdot\left(\frac{Q}{n}\right)\pmod n\) 两个同余。他们在 \(10^{15}\) 以下只找到 5 个满足前一个同余的合数(Lucas-V 伪素数),而满足 \(U_{n+1}\equiv0\) 的 Lucas 伪素数有两百多万个。Baillie 与 Wagstaff 各自为加强版的第一个反例,或第一篇证明不存在反例的同行评审论文,悬赏 1000 美元;他们也在文中复述了 Pomerance 的论证,认为加强版同样有无穷多个反例。这个问题可以被证伪:只要有人给出一个具体反例。

对抗性输入:平均情况界不能用来验证别人的数

第四节的 \(p_{k,t}\) 只对随机候选成立。Albrecht、Massimo、Paterson、Somorovsky 在 “Prime and Prejudice”(ACM CCS 2018)中研究了输入由对手构造的情形:

现在的 OpenSSL 3.6.2 在 BN_check_prime 里用至少 64 轮随机底(最坏情况 \(2^{-128}\)),GMP 6.3.0 在 Miller-Rabin 之前先做 BPSW。争论点在于:生成与验证是否应该用同一个函数、同一个轮数。OpenSSL 的选择是分开:验证按最坏情况,RSA 生成按 FIPS 的平均情况。

要不要”强素数”

Rivest 与 Silverman 在 “Are ‘Strong’ Primes Needed for RSA?”(IACR ePrint 2001/007)中认为,要求 \(p-1\)、\(p+1\) 有大素因子没有必要:对足够大的随机素数,Pollard \(p-1\) 和 Williams \(p+1\) 成功的概率本来就可以忽略,而 ECM 无法用这种条件防御。FIPS 186-5 保留了带辅助素数的生成方法,理由见附录 C.2;但它同时允许 A.1.2、A.1.3 的纯随机素数,也就是说标准本身没有把辅助素数当作必需。OpenSSL 选择了带条件的方法。

增量搜索的输出分布

从随机起点往后找第一个素数时,素数被选中的概率与它前面素数间隙的长度成正比,输出不是均匀分布。Brandt 与 Damgård(CRYPTO ’92)分析了这种增量搜索:给出了输出合数概率的显式上界;在 Hardy–Littlewood 素数 \(r\) 元组猜想下给出输出分布熵的下界,\(k\to\infty\) 时熵几乎达到最大值。允许重选起点的变体,他们只做了部分分析。OpenSSL 的两条路径都是混合流程:BN_generate_prime_ex2 只在筛内增量、Miller-Rabin 失败即重抽;FIPS 路径的辅助素数是从随机起点 +2 找到的第一个可能素数,\(p\) 则沿公差为 \(2r_1r_2\) 的等差数列搜索。它们的输出熵依赖一个未证明的猜想,本文没有测量。

其他开放问题

十一、工程陷阱

陷阱 后果 做法
64 位模乘用 a * b % m \(2^{64}\) 附近 447 个素数全被判为合数(第五节) unsigned __int128、Montgomery 乘法或大数库
64 位用 \(\{2,3,5,7,11,13,37\}\) 或前 11 个素数 分别漏判 52 个和 1 个合数 前 12 个素数,或 Sinclair 的 7 个底
大底数未处理 \(a\bmod n=0\) 73、193、407521、299210837 被判为合数 a %= n; if (a == 0) return true;,或先试除
只用 Fermat 测试 Carmichael 数对所有互素底都通过 用 Miller-Rabin 或 BPSW
用 FIPS Table B.1 的轮数验证对方提供的数 平均情况界不适用,对手可以构造高通过率合数(第十节) 验证外来数用 BPSW 或按 \(4^{-t}\) 选轮数;OpenSSL 用 BN_check_prime
以为 RSA 生成跑了 64 轮 对 OpenSSL 3.6.2 的行为产生误判 64/128 轮是 BN_check_prime 与 BN_generate_prime_ex2 的下限;nlen 至少 2048 位且 \(e>2^{16}\) 的双素数 RSA 走 FIPS 路径,\(p,q\) 按 Table B.1 只做 5 或 4 轮
熵不足的随机数 Heninger 等人(USENIX Security 2012)扫描发现,0.50% 的 TLS 主机和 0.03% 的 SSH 主机的 RSA 密钥因共用素数可被直接分解 使用已正确播种的 DRBG;设备首次启动时尤其要检查熵源
自创”快速”素数构造 ROCA(Nemec 等,CCS 2017,CVE-2017-15361):Infineon 库生成形如 \(p=kM+(65537^a\bmod M)\) 的素数,模数可用 Coppersmith 方法分解 按 FIPS 186-5 附录 A.1 的方法生成,不自行给素数加结构
不检查 \(\lvert p-q\rvert\) \(p,q\) 过近时 Fermat 分解法很快成功 按 A.1.1 要求 \(\lvert p-q\rvert>2^{\text{nlen}/2-100}\)

选型可以归结为三种场景:

十二、参考资料

规范与文档

源码

核心论文

其他论文

工程资料

实验


系列导航: - 上一篇:快速傅里叶变换 - 下一篇:扩展欧几里得与模逆元

相关阅读: - RSA 从原理到攻击 - 有限域算术 - 椭圆曲线算术 - 随机化算法

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-03-20 · cryptography / security

密码学工程中最容易犯的 7 个错误

密码学最危险的不是算法被破解,而是正确的算法被错误地使用。本文梳理 7 个真实 CVE 中的密码学工程错误,附代码与修复方案。


By .