开门见山:一秒钟的选型结论
加密一律 RSA-OAEP(MGF1 + SHA-256),签名一律 RSA-PSS,密钥长度最低 2048 位、长期场景用 3072 位,公钥指数固定 e=65537;永远不要用"教科书 RSA"(裸模幂),永远不要自己实现填充与校验,永远不要用 e=3 或让 d 过小;新系统如果要保证前向保密,直接放弃 RSA 密钥交换,改用 ECDHE + RSA/ECDSA 签名或干脆迁移到 Ed25519/X25519。
如果你赶时间,把上面这段话贴到代码评审模板里就够了。如果你接着往下读,这篇文章会告诉你:为什么 RSA 这个 1977 年发明、几乎半个世纪没被"正面攻破"的算法,在生产环境里却常年是安全事故的重灾区——答案几乎从不在大数分解本身,而在开发者对填充、指数、密钥长度这三个"外围参数"的处理上。
我会沿三条主线展开:填充攻击(Bleichenbacher 家族,从 1998 年打到 2017 年的 ROBOT,再打到 2026 年还在出的变种)、小指数攻击(Håstad 广播、Franklin-Reiter、Coppersmith、Wiener、共模攻击这一整套"利用结构"的武器库)、以及密钥长度选择(NIST/FIPS 186-5 的现行标准 + 后量子迁移的现实考量)。每一节都会配真实历史事件、可复现的代码和工程检查清单。
一、先看清 RSA 的数学骨架:它到底在算什么
RSA 的全部数学可以浓缩成四行:
选两个大素数 p, q
n = p × q ← 公开的模数
φ(n) = (p-1)(q-1) ← 私密
选 e,使 gcd(e, φ(n)) = 1(常用 65537 = 2^16 + 1)
求 d,使 e·d ≡ 1 (mod φ(n))
公钥 = (n, e)
私钥 = (n, d) 等价于 (p, q, d_p, d_q, q_inv) 的 CRT 形式
加密:c = m^e mod n
解密:m = c^d mod n
签名:s = H(m)^d mod n
验签:H(m) ==? s^e mod n
安全性公设只有一条:已知 n,求 p、q(即大数分解)在计算上不可行。最好的经典算法是通用数域筛(GNFS),复杂度是亚指数级 exp(O((ln n)^(1/3) (ln ln n)^(2/3)))——这意味着 RSA-2048 大约相当于 112 位对称安全强度,RSA-3072 大约相当于 128 位。 但公设之外还有一长串"隐藏假设",每一个都可能被攻破:
- 假设攻击者看不到填充结构的校验结果(→ Bleichenbacher 攻击);
- 假设攻击者不能让两个密文对应同一明文(→ 广播攻击、共模攻击);
- 假设攻击者不知道明文的结构(→ Coppersmith 类攻击);
- 假设 d 足够大(→ Wiener、Boneh-Durfee 攻击);
- 假设 e 不会过小导致 m^e 不跨模(→ 直接开方攻击);
- 假设素数生成器没有偏倚(→ 共享素数、短肢模数攻击);
- 假设签名验证严格(→ Bleichenbacher '06 签名伪造)。 Boneh 在经典综述《Twenty Years of Attacks on the RSA Cryptosystem》里给出的结论至今依然成立——没有一种攻击能"正面攻破"标准 RSA,但每一种攻击都能在错误使用下把 RSA 打得粉碎。这句话是整篇文章的总纲。
1.1 "教科书 RSA"为什么绝对不能用
所谓"教科书 RSA"(Textbook RSA),就是不做任何填充,直接对消息做模幂:
c = m^e mod n
它至少有四个致命缺陷:
二、填充攻击:Bleichenbacher 家族二十九年连环惨案
如果说 AES 的重灾区是 CBC 的 Padding Oracle,那 RSA 的重灾区就是 PKCS#1 v1.5 填充。这套填充方案从 1998 年被 Daniel Bleichenbacher 打穿开始,衍生出一条绵延二十多年的攻击谱系,影响范围横跨 SSL/TLS、XML 加密、PKCS#11、Java JSSE、OpenSSL、Cisco、Citrix、F5、Palo Alto、Fortinet 等几乎所有主流密码栈。
2.1 PKCS#1 v1.5 填充的结构
要理解攻击,先要理解填充的格式。PKCS#1 v1.5 加密填充(RFC 2313 / 早期 RFC 3447)长这样:
EM = 0x00 || 0x02 || PS || 0x00 || M
其中:
– 0x00 0x02:固定的两字节头
– PS:至少 8 字节的非零随机填充字节(Padding String)
– 0x00:分隔符
– M:要加密的消息
关键约束:整个 EM 的长度必须等于模数的字节长度 k(如 RSA-2048 的 k=256)。这意味着消息 M 最长只能是 k – 11 字节(2 字节头 + 至少 8 字节 PS + 1 字节分隔符 = 11 字节开销)。 解密方收到密文后要做的事:解出 EM,检查前两个字节是否是 0x00 0x02,扫描 PS 是否全非零,找到 0x00 分隔符,取出 M。任何一步不符合,就报"填充错误"。 Bleichenbacher 的洞察极其简单而致命:"报不报填充错误"本身就是一个预言机。
2.2 Bleichenbacher 1998:自适应选择密文攻击(CCA)
Bleichenbacher 在 CRYPTO '98 发表了这篇传奇论文,攻击思路如下: Step 1 — 设定目标。攻击者截获一段密文 c,想解出对应的明文 m。RSA 满足乘法同态:如果攻击者发送 c' = c × s^e mod n,服务器解密后会得到 m' = m × s mod n。攻击者可以通过调节 s 来"探测" m 的取值范围。 Step 2 — 利用预言机。攻击者把构造的密文发给服务器,观察响应:
- 如果服务器报填充错误(PKCS#1 v1.5 解析失败),说明 m × s mod n 的开头不是 0x00 0x02;
- 如果服务器没有报填充错误(进入了下一步处理,比如证书解析、密钥计算),说明 m × s mod n 的开头恰好是 0x00 0x02——这就把 m 锁定在一个已知区间内。 Step 3 — 区间收缩。通过不断调整 s(一次约几千到上百万次查询),攻击者可以用自适应区间算法把 m 的范围从 [0, n) 收缩到单一值。原始论文估计平均需要约 100 万次查询就能解出任意 PKCS#1 v1.5 密文。 Step 4 — 实战意义。1998 年这个攻击直接打穿了 SSL 3.0 / TLS 1.0 的 RSA 密钥交换——攻击者坐在网络里,截获一次握手,然后向服务器发起上百万次伪造的 ClientKeyExchange,每次观察服务器的 Alert 消息(是 decrypt_error 还是其他错误),最终恢复出本次握手的预主密钥,进而解密整条会话。 这是一个"无需破解 RSA、无需分解 n、无需拿到私钥"就能完成解密的攻击——它只依赖服务器行为的不一致性。RSA 本身完好无损,被打穿的是 PKCS#1 v1.5 填充在协议实现里的处理方式。
2.3 二十九年连环惨案时间线
这条攻击谱系值得每个工程师看一遍,因为它不是理论威胁,而是真实地打了全世界一遍又一遍:
| 1998 | Bleichenbacher 首次提出 CCA 攻击 | SSL 3.0 / TLS 1.0 RSA 密钥交换 |
| 2003 | Klíma-Rosa 等改进,降低查询数 | 实战化门槛进一步降低 |
| 2012 | Rizzo-Duong "ROBOT 前身"攻击 XML 加密 | Apache Axis2、XML 加密栈 |
| 2014 | Bardou 等改进 PKCS#1 v1.5 攻击 PKCS#11 HSM | 硬件安全模块也沦陷 |
| 2014 | POODLE(针对 SSL 3.0 的 CBC padding oracle) | SSL 3.0 彻底废除 |
| 2016 | DROWN 攻击(SSLv2 + Bleichenbacher 变种) | 影响 HTTPS 服务器中约 33% |
| 2017-12 | ROBOT (Return Of Bleichenbacher’s Oracle Threat) | F5、Citrix、Radware、Cisco、Palo Alto、Fortinet 等至少 8 家厂商 |
| 2017-12 | CVE-2017-6168(F5 BIG-IP) | TLS RSA 密钥交换可被解密 |
| 2017-12 | CVE-2017-17382 / CVE-2017-17427(Citrix NetScaler ADC) | 企业级负载均衡器受影响 |
| 2017-12 | CVE-2017-17841(Palo Alto PAN-OS) | 防火墙 SSL 卸载功能受影响 |
| 2018 | Böck 等发表 USENIX 论文系统化 ROBOT | 标准化防御方案 |
| 2018+ | TLS 1.3 彻底移除 RSA 密钥交换 | 结构性根除 |
| ROBOT 这个名字起得很讽刺——Return Of Bleichenbacher’s Oracle Threat,“Bleichenbacher 预言机威胁的回归”。1998 年就该死掉的东西,在 2017 年又活了一遍,而且是在像 F5、Citrix、Cisco 这种世界最顶级的网络安全厂商的产品里。Böck 团队扫描显示,当时互联网上仍有大量 TLS 服务器在接受 RSA 密钥交换且存在可区分的填充错误响应。 | ||
| 根本原因不是算法难,而是工程上"报错"太容易泄露信息。F5 的官方通告承认这一点:ROBOT 攻击"可以对任何使用 RSA 签名的 TLS 会话发起,前提是服务器启用了 RSA 密钥交换的密码套件"。 |
2.4 攻击的工程细节:响应如何"区分"
Bleichenbacher 攻击能成立的前提是攻击者能区分两种响应。在真实系统里,这种"区分"往往来自非常不起眼的工程细节:
- 不同的 TLS Alert 类型:decrypt_error vs bad_record_mac vs handshake_failure;
- 不同的 HTTP 状态码:500 vs 400 vs 200;
- 不同的响应时间:填充失败提前返回 vs 完整处理(时间侧信道);
- 是否关闭连接:直接 reset vs 继续握手;
- 日志差异:偶尔的错误日志反而暴露了内部状态。 ROBOT 论文里专门指出:一些产品在两种错误场景下都返回了"形式上一致"的 Alert,但TCP 层的连接行为不同——一种会立即关闭连接,另一种会等待。攻击者用连接时序就完成了区分。 这给我们的启示:统一错误响应不只是返回值,而是整个可观察行为——返回码、响应体、TCP 行为、时序、日志量、重试逻辑,全部都不能区分。工程上真正能"完全统一"的方法只有一个:不实现 PKCS#1 v1.5 解密。
2.5 防御方案:从"修修补补"到"结构性根除"
针对 Bleichenbacher 攻击的防御经历了几代演进: 第一代:常数时间统一响应。让"填充错误"和"后续处理错误"返回完全一致的错误码和完全一致的时序。这要求在填充失败时也生成一个随机的"假"会话密钥继续握手流程,让攻击者无法区分。TLS 1.2 的 RFC 5246 就规定了这种做法。但历史证明这条路太脆弱——OpenSSL、BoringSSL、LibreSSL、Java JSSE、GnuTLS 都曾在这里出过补丁(包括 CVE-2016-2107 “LuckyNegative20”,OpenSSL 修 Lucky13 时反而引入了新的侧信道)。 第二代:直接禁用 RSA 密钥交换。TLS 1.3(RFC 8446)做了一个非常干净的决定——从协议层彻底删除 RSA 密钥交换,只保留(EC)DHE 做密钥协商,RSA 仅用于签名验证证书。这是结构性根除:没有 RSA 加密操作,就没有 PKCS#1 v1.5 解密,就没有 Bleichenbacher 预言机,攻击面整体消失。Salesforce 在 2026 年 6 月的公告里明确宣布将在其平台上"turning off RSA key exchanges"。 第三代:用 RSA-OAEP 替代 PKCS#1 v1.5。如果业务必须用 RSA 加密(比如某些 HSM 只支持 RSA 加密密钥),必须用 OAEP。OAEP 的填充校验在数学上被证明可以对抗选择密文攻击(CCA2)——只要实现正确,Bleichenbacher 类预言机就不存在。
2.6 RSA-OAEP 详解与实战代码
OAEP(Optimal Asymmetric Encryption Padding,RFC 8017)由 Bellare 和 Rogaway 在 1994 年提出,PKCS#1 v2.0 引入,是目前 RSA 加密的唯一正确选择。 结构:OAEP 用两次 MGF1(Mask Generation Function,基于哈希)做两轮"遮罩",把消息随机化嵌入到模数空间中。关键参数是 hash 算法和 MGF1 的 hash 算法(通常相同):
最大消息长度 = k – 2×hLen – 2
其中:
– k = 模数字节数(RSA-2048 时 k = 256)
– hLen = 哈希输出长度(SHA-256 时 hLen = 32)
RSA-2048 + SHA-256 → 最大 190 字节明文
RSA-3072 + SHA-256 → 最大 318 字节明文
RSA-4096 + SHA-256 → 最大 446 字节明文
这个长度限制非常重要——很多工程问题(“为什么我加密 200 字节会报错?”)都源于此。OAEP 的定位是加密一个对称密钥(比如 32 字节 AES-256 密钥),而不是加密业务数据。业务数据应该用 AES-GCM 加密,然后 OAEP 只加密 AES 密钥——这就是下一节要讲的"混合加密"。 Python 实战代码(pyca/cryptography):
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
import os
# ============ 密钥生成 ============
private_key = rsa.generate_private_key(
public_exponent=65537, # 永远用 65537,不要用 3
key_size=3072, # 生产环境推荐 3072(128 位安全)
)
public_key = private_key.public_key()
# ============ 混合加密:OAEP 包 AES 密钥,AES-GCM 加密数据 ============
def rsa_encrypt(plaintext: bytes, pub_key) –> dict:
"""用 RSA-OAEP 加密任意长度的数据"""
# 1. 生成一次性 AES-256 密钥
aes_key = os.urandom(32)
# 2. 用 AES-256-GCM 加密业务数据
nonce = os.urandom(12)
ct = AESGCM(aes_key).encrypt(nonce, plaintext, None)
# 3. 用 RSA-OAEP 加密 AES 密钥
wrapped_key = pub_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
# 4. 擦除内存中的 AES 密钥
del aes_key
return {'wrapped_key': wrapped_key, 'nonce': nonce, 'ct': ct}
def rsa_decrypt(blob: dict, priv_key) –> bytes:
aes_key = priv_key.decrypt(
blob['wrapped_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
try:
return AESGCM(aes_key).decrypt(blob['nonce'], blob['ct'], None)
except InvalidTag:
raise ValueError("Decryption failed") # 统一错误,不区分原因
OAEP 的工程注意事项:
2.7 签名端也有填充问题:Bleichenbacher '06 攻击
填充攻击不止发生在加密端。2006 年 Bleichenbacher 在 CRYPTO '06 Rump Session 上做了一个 5 分钟的演讲,指出 PKCS#1 v1.5 签名验证如果实现不严格,在使用小公钥指数(e=3)时可以完全伪造签名。 攻击思想:PKCS#1 v1.5 签名的格式是:
EM = 0x00 0x01 || FF…FF || 0x00 || DigestInfo || Hash
一个严格的验证必须检查:
- 头是 0x00 0x01;
- 中间全 FF 且足够长;
- 分隔符 0x00;
- DigestInfo 完全符合标准;
- 哈希值匹配。 但一些实现只检查了"开头是 0x00 0x01,中间有一段 FF,最后有正确哈希",没检查 FF 的长度和 DigestInfo 的严格格式。这给了攻击者构造空间:当 e=3 时,攻击者可以构造一个格式非常宽松的"伪签名"块,其立方恰好是一个符合宽松校验的 EM。Bleichenbacher 展示了如何在 2006 年直接伪造出能通过 Firefox(当时用 NSS)证书校验的签名。 防御:
- 签名一律用 RSA-PSS(RFC 8017 / 8018),它有概率性的随机盐,被证明可以安全对抗这类攻击;
- 如果必须支持 PKCS#1 v1.5 签名(例如某些遗留系统),验证必须严格到字节级——libsecp256k1、BoringSSL 等都提供了"严格模式"的实现;
- 永远不要用 e=3 做签名公钥。e=65537 已经是行业标准,几乎没有理由偏离。 Filippo Valsorda 在 2026 年 7 月的文章里再次提到 python-rsa 库中这个漏洞的回归,提醒签名验证的实现审查至今不能放松。
三、小指数攻击:一整套"利用结构"的武器库
第二类攻击和填充无关,和公钥指数 e 与私钥指数 d 的选择有关。这套攻击的数学性更强,但工程含义同样清晰——用错 e 和 d,RSA 会被打得很彻底。
3.1 直接开方攻击:e=3 + 短消息 = 完蛋
最极端的情况:如果用 e=3 且消息很短,使得 m^3 < n,那么密文 c = m^3 mod n = m^3(没有发生模约减)。此时直接对 c 做整数开立方:
import gmpy2
# 攻击:m^3 < n 的情况下
c = ... # 截获的密文
m = gmpy2.iroot(c, 3)[0] # 直接整数开立方
这就是为什么不能用 e=3 加密短消息、不能用 e=3 且无填充签名短消息。哪怕 m^3 > n,只要消息高度结构化(比如知道前 N 字节是固定协议头),仍有 Coppersmith 类攻击可打(见 3.4)。 工程铁律:e=65537。它有两个优点:一是费马数 2^16 + 1,二进制只有 2 个 1,模幂运算极快;二是足够大,使得 m^e 几乎必然跨模,直接开方攻击失效。没有任何理由偏离这个选择。
3.2 Håstad 广播攻击:同一消息发给 e 个接收者
这是小指数攻击家族里最优雅的一招,由 Håstad 在 1988 年提出。 场景:一个系统里,服务器用不同的接收者公钥(不同 n1, n2, n3,但都是 e=3)加密同一份明文 m,分别发给 Alice、Bob、Charlie。攻击者截获了三份密文:
c1 = m^3 mod n1
c2 = m^3 mod n2
c3 = m^3 mod n3
攻击步骤:
from sympy.ntheory.modular import crt
import gmpy2
def hastad_broadcast_attack(ciphertexts, moduli, e=3):
"""ciphertexts: [c1, c2, …, ce],moduli: [n1, n2, …, ne]"""
assert len(ciphertexts) >= e, "至少需要 e 份密文"
r, _ = crt(moduli[:e], ciphertexts[:e])
m, exact = gmpy2.iroot(int(r), e)
assert exact, "CRT 后不是精确的 e 次方根——可能消息太长"
return int(m)
防御:
- 每次加密使用OAEP 填充——填充的随机性让"同一明文"变成了"不同明文",CRT 组合出来的不再是 m 的精确 e 次方,攻击失效;
- 如果必须裸 RSA(不推荐),至少给每份消息加独立随机填充——但这也只是缓解,正确做法是 OAEP。
3.3 Franklin-Reiter 相关消息攻击
场景:同一接收者(同一 n、同一 e),攻击者拿到了两个有已知代数关系的密文:
c1 = m^e mod n
c2 = (a·m + b)^e mod n (a, b 已知或可枚举)
比如 m 是 Transfer 100,m’ 是 Transfer 101,两者只差 1。 攻击:把 m 视为多项式环 Z_n[x] 上的未知数,构造两个多项式:
g1(x) = x^e – c1
g2(x) = (a·x + b)^e – c2
在 Z_n[x] 里对 g1、g2 做多项式最大公约数,结果是 (x – m) 的线性式,直接读出 m。整个攻击是多项式时间。 防御:用 OAEP,让两条消息即使只差 1 字节,填充后的实际加密输入也完全独立。Franklin-Reiter 攻击是 OAEP 必须使用的另一个理由。
3.4 Coppersmith 攻击:已知部分信息的"终极武器"
Coppersmith 在 1996 年提出的方法是现代 RSA 攻击分析的基石,它解决的问题可以概括为:在多项式方程 f(x) ≡ 0 (mod n) 中求小根。当根 x 足够小(相对于 n 的规模)时,通过 LLL 格基规约可以在多项式时间找到它。 Coppersmith 的几个经典应用: (1)Coppersmith Short-Pad 攻击。即使发送方对同一消息加了随机填充再发送给两个接收者(仍然 e=3),如果填充长度有限(如只在消息尾部加了 k 位随机数),攻击者仍可通过 CRT + Coppersmith 恢复消息。这说明"自己发明随机填充"是危险的,必须用标准化的 OAEP。 (2)已知明文高位恢复低位。如果已知明文 m 的高 2/3 位(比如消息模板是 "From: Alice\\nTo: Bob\\nSubject: ???\\n\\n"),Coppersmith 可以恢复出低位的未知部分。 (3)部分私钥泄露攻击。如果攻击者通过侧信道(如缓存时序、功耗分析)获得了 d 的低 1/4 比特,就能在多项式时间内完整恢复 d。 (4)分解短肢模数。一些有 bug 的随机数生成器产生的 p、q 在某些位段上极度稀疏,Coppersmith 类方法可以从 n 的多项式表示中直接分解。 实战工具:RsaCtfTool 把几乎所有已知 RSA 攻击(Håstad、Wiener、Franklin-Reiter、Coppersmith、共模、共享素数等)做成了自动化工具,CTF 场景一行命令就能尝试所有攻击。SageMath 是更通用的底层工具。
3.5 Wiener 与 Boneh-Durfee:私钥指数 d 不能太小
很多开发者为了让 RSA 解密更快,会想到"用小的 d"。这条路被两条定理彻底封死: Wiener 攻击(1990):如果 d < (1/3)·N^(1/4),可以通过对 e/n 做连分数展开在多项式时间恢复 d。直觉:由 ed ≡ 1 (mod φ(n)) 可知存在 k 使 ed – kφ(n) = 1,即 e/φ(n) ≈ k/d。由于 d 很小,k/d 是 e/n 的一个非常好的有理逼近,连分数展开能找到它。 Boneh-Durfee 攻击(1999):把边界从 N^(1/4) 提升到 N^0.292,用的是格基规约(LLL)的精细分析。这是二十多年来的最优边界,最新的研究还在继续逼近 N^0.292 上限。 工程含义:d 必须"很大"——具体来说,要接近 n 的一半大小。标准做法不是选小 d,而是选小 e(65537)让 d 自然就大。如果某些系统为了"快速签名"而强制设置小的 d(比如某些智能卡场景),必须用 CRT 形式(d_p, d_q)并确认 d 满足安全边界,否则整个密钥就是脆弱的。
3.6 共模攻击(Common Modulus):一个系统设计错误
场景:同一系统里,两个用户共享同一个 n(这通常来自糟糕的密钥生成流程),但有不同的 e1、e2 且 gcd(e1, e2) = 1。攻击者截获同一明文的两份密文:
c1 = m^e1 mod n
c2 = m^e2 mod n
攻击:用扩展欧几里得算法求 a、b 使 a·e1 + b·e2 = 1,然后:
c1^a × c2^b ≡ m^(a·e1 + b·e2) ≡ m^1 ≡ m (mod n)
直接得到明文,不需要分解 n,不需要任何私钥信息。 根本原因:RSA 的安全性假设每个用户有独立的 n。任何两个用户的公钥绝对不能共享 n——即使他们各自的私钥保密,这种设计就已经破坏了整个系统的安全前提。
3.7 小指数攻击家族速查表
| 直接开方 | e 小且 m^e < n | 整数开方 | e=65537、OAEP |
| Håstad 广播 | 同一明文发给 ≥ e 个接收者 | CRT + 整数开方 | OAEP(独立随机化) |
| Franklin-Reiter | 同一 n、e 下两条相关消息 | 多项式 GCD | OAEP |
| Coppersmith Short-Pad | 自定义随机填充 + 多接收者 | LLL 格规约 | 标准 OAEP |
| Coppersmith 已知高位 | 已知明文/素数高 2/3 位 | LLL 格规约 | 不泄露部分信息、保护侧信道 |
| Wiener | d < N^(1/4) | 连分数 | 保证 d 足够大(用小 e 反推) |
| Boneh-Durfee | d < N^0.292 | LLL 格规约 | 同上 |
| 共模攻击 | 两个用户共享 n | 扩展欧几里得 | 密钥生成严格独立 |
| 核心防御思想:只要使用标准 OAEP + e=65537 + 足够大的 n + 独立生成的素数,这一整套攻击全部失效。 |
四、密钥长度选择:NIST 标准与后量子现实
第三条主线是最"枯燥"但也最容易被忽略的——密钥长度。这部分把 NIST 的现行标准、各行业标准、以及后量子迁移的现实一起讲清楚。
4.1 RSA 密钥长度与安全强度对照
| 1024 位 | ~80 位 | 已可在国家级资源下攻击 | ❌ FIPS 186-5 已禁用 | 任何场景都不应使用 |
| 1536 位 | ~96 位 | 临界 | ❌ 不推荐 | 遗留系统识别后迁移 |
| 2048 位 | ~112 位 | 计算不可行 | ✅ FIPS 186-5 最低要求 | 大多数场景的默认选择 |
| 3072 位 | ~128 位 | 计算不可行 | ✅ 推荐 | 长期数据保护、合规要求高 |
| 4096 位 | ~152 位 | 计算不可行 | ✅ 可用 | 极高安全需求、CA 根证书 |
| 8192 位 | ~200+ 位 | 计算不可行 | ⚠️ 过度 | 性能代价不成比例 |
| 关键依据: |
- FIPS 186-5(NIST 数字签名标准,2023 年发布)规定 RSA 签名的最低密钥长度为 2048 位,推荐 3072 位及以上;
- NIAP PP_OS v4.3 等操作系统保护轮廓标准明确:要满足 FIPS 186-5,RSA 密钥必须 ≥ 3072 位;
- NIST SP 800-57 密钥管理建议:112 位安全强度(RSA-2048)可保护到约 2030 年,128 位(RSA-3072)可保护到 2030 年以后。
4.2 2048 还是 3072?——一个决策框架
用 RSA-2048 的场景:
- TLS 证书(终端实体证书,1 年有效期)——业界 95%+ 都用 2048;
- 短生命周期签名(如 JWT 的 RSA 签名);
- 与遗留系统的兼容;
- 性能敏感的高 QPS 服务。 用 RSA-3072 的场景:
- CA 根证书和中间 CA 证书(有效期 10 年以上);
- 长期归档数据的加密密钥封装;
- 政府、金融、医疗等合规要求高的场景;
- 涉及"现在加密、多年后解密"的任何数据。 NIST SP 800-57 的指导原则:保护数据所需的安全强度,应该等于数据需要保密的时间所对应的强度。一份需要保密 20 年的医疗档案,用 RSA-2048(112 位)是不够的——应该用 RSA-3072(128 位)甚至 ECC-384。
4.3 2048 位会不会被"破解"?
2026 年的"破解"含义有三层: (1)经典算法(GNFS)。RSA-2048 需要 ~2^112 次操作。截至 2026 年,公开最大分解纪录是 RSA-250(829 位,2020 年)。距离 2048 位还有 4 个数量级的差距。短期内(5-10 年)RSA-2048 在经典计算下依然是安全的。 (2)分布式/集群攻击。想象整个互联网的算力被集中用于分解一个 n——理论上有可能,但需要异常庞大的资源。仍属于"国家级威胁"范畴,普通商业系统不必过度担心。 (3)量子计算(Shor 算法)。这是真正的颠覆性威胁。一个具备足够量子比特的容错量子计算机可以在多项式时间分解任意 RSA 模数——一旦达到,RSA-4096 也一样不安全。 现实进度(2026 年 9 月):目前最大的量子分解纪录仍然是极小的整数(百位数量级)。NIST IR 8547 给出了后量子迁移时间表:2030 年前停用 112 位安全强度的算法,2035 年前完成向 PQC 的全面迁移。对大多数企业而言,现在开始评估、2028 年前后开始试点、2032 年前完成关键系统迁移是一个现实的节奏。
4.4 后量子时代,RSA 怎么办
NIST 在 2024 年发布了三个 PQC 标准:
- FIPS 203(ML-KEM,基于 Kyber):密钥封装机制,替代 RSA/ECDH 的密钥交换;
- FIPS 204(ML-DSA,基于 Dilithium):数字签名,替代 RSA/ECDSA 签名;
- FIPS 205(SLH-DSA,基于 SPHINCS+):基于哈希的签名,作为备选方案。 实际迁移策略是混合模式(Hybrid):同时使用经典算法和 PQC 算法,任何一个被破解仍然安全。例如 X25519+ML-KEM-768 的混合密钥交换、Ed25519+ML-DSA-65 的混合签名。Chrome、Cloudflare、OpenSSH 9.0+ 都已支持混合模式。 对 RSA 的现实建议:
- 新系统不要再用 RSA 做密钥交换,直接用 X25519 或 ML-KEM;
- 签名优先考虑 Ed25519(性能和安全性都优于 RSA);
- 必须用 RSA 的场景(如某些企业 PKI),选 3072 位 + PSS,并规划 2030 年前的迁移路径。
4.5 ECC vs RSA:一个无法回避的对比
工程师经常问:既然 ECC 更小更快,为什么还要用 RSA?下表给出对比:
| 128 位安全所需密钥 | 3072 位 | 256 位 |
| 签名速度 | 快(验签慢) | 慢(验签快) |
| 验签速度 | 较慢 | 非常快 |
| 密钥/签名大小 | 大(384 字节签名 @ 3072 位) | 小(64 字节签名 @ Ed25519) |
| 协议兼容性 | 极广(遗留友好) | 现代 TLS/SSH/Signal 默认 |
| 量子抗性 | 无(Shor) | 无(Shor) |
| 实现陷阱 | 填充、指数、长度 | 侧信道、曲线选择 |
建议:新系统优先 Ed25519 / X25519;RSA 只在必须兼容遗留系统、或某些企业 PKI 强制要求时使用。
五、签名 vs 加密:两套完全不同的填充方案
这里要澄清一个常被混淆的点:RSA 加密和 RSA 签名用的是完全不同的填充方案,绝对不能混用。
| 加密 | RSA-OAEP(RFC 8017) | PKCS#1 v1.5 加密(仅遗留) | 教科书 RSA、ECB-like 裸 RSA |
| 签名 | RSA-PSS(RFC 8017) | PKCS#1 v1.5 签名(严格验证下勉强可用) | 教科书 RSA、用加密填充做签名 |
为什么不能混用:
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.asymmetric.utils import (
Prehashed
)
import hashlib
# ============ RSA-PSS 签名 ============
def sign_message(message: bytes, priv_key) –> bytes:
return priv_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH, # 推荐:盐长 = 哈希输出长度
),
hashes.SHA256(),
)
def verify_signature(message: bytes, signature: bytes, pub_key) –> bool:
try:
pub_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
return True
except Exception:
return False # 统一失败,不区分具体原因
PSS 的关键参数:
- salt_length:推荐 MAX_LENGTH(即哈希输出长度),这是 RFC 8017 的默认建议;
- mgf:MGF1 + SHA-256;
- hash:SHA-256 或更高,绝不用 SHA-1。
六、FAQ:最常见的 5 个问题
Q1:RSA-2048 还能用多久? 在经典计算下至少能用到 2030 年(NIST SP 800-57 的官方评估)。但有两个"提前终结"的风险:一是量子计算突破(Shor 算法),二是更大的集群攻击。NIST IR 8547 的官方时间表是 2030 年停用 112 位算法(包括 RSA-2048),2035 年全面迁移。对于长期数据(需要保密 10 年以上),现在就该用 3072 位。 Q2:我能在自己的协议里用 RSA 做端到端加密吗? 可以,但要遵守完整的工程实践:RSA-OAEP + SHA-256 + e=65537 + 3072 位 + 混合加密(RSA 只包 AES-256-GCM 密钥)+ 严格的错误处理。更简单的做法是直接使用 libsodium 的 crypto_box_seal 或.age 格式——这些库已经处理好了所有细节,你不需要自己踩任何坑。 Q3:为什么 OpenSSL 还在用 PKCS#1 v1.5 签名? 历史包袱。X.509 证书体系、TLS 1.2 之前、大量企业 PKI 都建立在 PKCS#1 v1.5 签名上。在严格验证实现下,PKCS#1 v1.5 签名是可用的——但前提是验证代码严格到字节级。新协议、新系统一律用 PSS。 Q4:RSA 和 ECC 该选哪个? 现代系统优先 ECC(特别是 Ed25519/X25519)。RSA 只在两种情况下用:一是必须兼容遗留系统(某些老 PKI、某些企业集成),二是某些合规场景明确要求 RSA。ECC 在密钥大小、性能、协议适配性上都优于 RSA。 Q5:怎么检测我的系统是否有这些漏洞? 工具化检查:
- TLS 扫描:testssl.sh、sslscan 检查是否启用了 RSA 密钥交换、是否支持 TLS 1.3;
- 证书审计:检查证书链中是否有 RSA-1024/2048 老证书;
- 代码审计:搜索 PKCS1v15、MD5withRSA、SHA1withRSA、RSA/ECB/PKCS1Padding、e=3、key_size=1024 等关键词;
- 密钥审计:用 badkeys 工具扫描公钥是否属于已知的弱密钥族;
- CTF 复现:用 RsaCtfTool 对自己的公钥跑一遍所有已知攻击。
结语:RSA 没被攻破,但你的代码可能已经被打穿了
写到这里,你可能已经发现一个反复出现的主题—— RSA 这个算法本身,从 1977 年发明至今,在标准参数下(e=65537、2048+ 位、OAEP/PSS)从未被"正面攻破"。但围绕 RSA 的每一次工程失误——一个错误的填充、一个过小的指数、一个过短的密钥、一个不一致的响应——都会让 RSA 在实际系统里毫无防护力。 Bleichenbacher 攻击打了 29 年还在出新变种,不是因为 RSA 弱,而是因为"统一错误响应"这件事在工程上比"发明 RSA"还难。小指数攻击家族之所以屡试不爽,不是因为数学多高深,而是因为总有人觉得"e=3 也没什么"。1024 位密钥至今仍在某些老系统里跑,不是因为没人知道它弱,而是因为迁移成本总被推到下一个季度。
密码学工程的第一铁律:不要相信"我用了 RSA 所以安全"。RSA 只是提供了一个数学上的硬度假设,把假设变成安全需要的是每一行代码的正确性——填充、指数、长度、错误处理、密钥管理,缺一不可。






