在密码学的漫长历史中,填充(padding)这个看似不起眼的环节曾反复成为攻击者突破整个系统防线的关键入口。许多工程师初次接触 RSA 加密时,往往将注意力集中在大素数生成、模幂运算等数学层面,而忽略了一个根本问题:原始的数学运算本身并不构成安全的加密方案,填充方案才是将数学难题转化为可证明安全的密码系统的桥梁。从 1998 年 Bleichenbacher 发表针对 PKCS#1 v1.5 的百万消息攻击,到 2017 年 ROBOT 攻击证明同一类漏洞在近二十年后依然广泛存在,填充方案的设计与实现缺陷一次又一次给现实世界带来了深远影响。本文将从填充的必要性出发,系统地梳理对称与非对称加密中各类填充方案的设计理念、安全性证明与历史教训。
一、填充的必要性
为了理解填充的必要性,我们首先需要回顾”原始 RSA”(textbook RSA)存在的根本缺陷。
在教科书 RSA 中,加密过程被定义为 \(c = m^e \bmod n\),解密为 \(m = c^d \bmod n\)。这一过程是完全确定性的——同一明文在同一公钥下永远产生相同的密文。这意味着攻击者可以在不知道私钥的情况下判断两段密文是否对应相同的明文,从而彻底违反了现代密码学对加密方案的基本安全要求——不可区分性(IND-CPA,indistinguishability under chosen-plaintext attack)。
确定性加密的危害远不止于此。由于 RSA 的乘法同态性质(multiplicative homomorphism),攻击者可以对密文进行代数操作而无需解密。具体来说,如果攻击者截获密文 \(c = m^e \bmod n\),他可以构造 \(c' = c \cdot s^e \bmod n\),其中 \(s\) 是攻击者选定的任意值。解密 \(c'\) 将得到 \(m \cdot s \bmod n\),攻击者由此可以间接操控明文,这在数字签名场景中尤其危险——攻击者可以诱导签名者对经过篡改的消息进行签名。
此外,当明文空间较小时(例如加密一个 16 位的密码或一个布尔值),攻击者可以简单地穷举所有可能的明文,用公钥加密后与截获的密文逐一比对,从而完全恢复明文。这类攻击在教科书 RSA 下无法防御。
填充方案正是为解决这些问题而生。其核心思路是在明文被加密之前,通过引入随机性(randomness)和结构化格式(structured format)来改造明文。随机性确保同一明文每次加密产生不同的密文,从而满足不可区分性要求;结构化格式则为解密方提供验证手段,使其能够在解密后检测密文是否遭到篡改,从而抵御选择密文攻击(CCA,chosen-ciphertext attack)。
然而,填充方案的设计绝非在明文前后简单地拼接随机字节那么简单。方案的每一个细节——随机填充的长度、分隔标志的位置、错误消息的内容——都可能成为攻击者利用的侧信道。PKCS#1 v1.5 的历史深刻地印证了这一点。
二、PKCS#1 v1.5 加密填充
PKCS#1 v1.5(Public-Key Cryptography Standards #1, version 1.5)由 RSA 实验室于 1993 年发布,是最早被广泛采用的 RSA 填充标准。其加密填充格式如下:
0x00 || 0x02 || [至少 8 字节的非零随机填充] || 0x00 || [消息]
整个填充后的数据块长度等于 RSA 模数 \(n\) 的字节长度 \(k\)。其中:
- 0x00 0x02:两字节的类型标识,表明这是一个加密消息(类型 2)。第一个 0x00 确保填充后的整数值小于 \(n\)。
- 随机填充字节:至少 8 个字节,每个字节必须为非零值。这些随机字节提供了加密所需的随机性。对于 2048 位的 RSA 密钥(\(k = 256\) 字节),当消息长度为 \(l\) 字节时,随机填充的长度为 \(k - l - 3\) 字节。
- 0x00 分隔符:一个零字节,用于标识随机填充的结束和实际消息的开始。由于随机填充中不包含零字节,解密方可以通过从左向右扫描找到第一个 0x00 来定位消息边界。
- 消息:实际待加密的明文数据。
这个设计在当时看来是合理的:随机填充引入了不确定性,类型标识和分隔符提供了基本的格式验证。然而,1998 年 Daniel Bleichenbacher 在 CRYPTO 会议上发表的论文彻底改变了人们对这一方案安全性的认知。
Bleichenbacher 观察到,大多数 RSA 实现在解密后会首先检查填充格式是否正确:开头是否为 0x00 0x02、随机填充是否足够长、分隔符 0x00 是否存在。如果格式不正确,服务器会返回一个错误;如果格式正确,则继续处理消息。这种行为上的差异——无论是通过显式的错误代码、不同的响应时间,还是连接是否被提前关闭——构成了一个填充正确性预言机(padding oracle)。
攻击者利用这个预言机,可以在不知道私钥的情况下完全恢复任意密文对应的明文。攻击的核心原理基于 RSA 的乘法同态性:攻击者选择一个整数 \(s\),计算 \(c' = c \cdot s^e \bmod n\),然后将 \(c'\) 发送给服务器。服务器解密 \(c'\) 得到 \(m' = m \cdot s \bmod n\),然后检查 \(m'\) 的填充格式。如果预言机返回”格式正确”,攻击者就知道 \(m \cdot s \bmod n\) 的最高两个字节为 0x00 0x02,即 \(2 \cdot 256^{k-2} \le m \cdot s \bmod n < 3 \cdot 256^{k-2}\),这为 \(m\) 提供了一个取值范围的约束。
三、Bleichenbacher 攻击详解
Bleichenbacher 攻击的完整过程可以分为几个阶段。
阶段一:初始化。 攻击者获取目标密文 \(c\),并确认 \(c\) 本身解密后符合 PKCS#1 v1.5 格式(通常情况下,合法密文自然满足这一条件)。设 \(B = 256^{k-2}\),则合法填充意味着明文 \(m\) 落在区间 \([2B, 3B)\) 内。
阶段二:搜索符合条件的乘数。 攻击者从某个起始值开始逐一递增 \(s\),对每个 \(s\) 计算 \(c' = c \cdot s^e \bmod n\) 并发送给预言机,直到预言机返回”格式正确”。由于合法填充的概率大约为 \(1/2^{16}\)(最高两字节恰好为 0x00 0x02),攻击者平均需要约 \(2^{16} = 65536\) 次查询才能找到第一个有效的 \(s\)。
阶段三:区间收窄。 每当找到一个使预言机返回”正确”的 \(s\) 值,攻击者就获得了关于 \(m\) 的一个新的线性约束。利用这些约束,攻击者可以逐步缩小 \(m\) 可能的取值区间。随着收集到的约束越来越多,候选区间不断收窄,最终收敛到唯一值。
阶段四:明文恢复。 当候选区间缩小到只包含一个整数时,该整数即为明文 \(m\)。整个过程通常需要数百万次预言机查询(因此又被称为”百万消息攻击”,million-message attack)。
以下 Python 代码展示了 Bleichenbacher 攻击中区间收窄逻辑的简化版本:
def bleichenbacher_demo(n, e, c, oracle, k):
"""
简化的 Bleichenbacher 攻击演示。
oracle(ciphertext) -> bool 表示解密后是否符合 PKCS#1 v1.5 格式。
k 为 RSA 模数的字节长度。
"""
B = pow(256, k - 2)
# 初始区间:合法 PKCS#1 v1.5 填充的明文范围
intervals = [(2 * B, 3 * B - 1)]
s = n // (3 * B) + 1 # 起始搜索值
while True:
# 阶段二:搜索下一个有效的 s
while True:
c_prime = (c * pow(s, e, n)) % n
if oracle(c_prime):
break
s += 1
# 阶段三:根据新的 s 值收窄区间
new_intervals = []
for a, b in intervals:
# 计算 r 的范围:m*s = r*n + (2B 到 3B-1 之间的值)
r_min = (a * s - 3 * B + 1 + n - 1) // n
r_max = (b * s - 2 * B) // n
for r in range(r_min, r_max + 1):
# 对每个 r,计算 m 的可能区间
lo = max(a, (2 * B + r * n + s - 1) // s)
hi = min(b, (3 * B - 1 + r * n) // s)
if lo <= hi:
new_intervals.append((lo, hi))
# 合并重叠区间
new_intervals.sort()
merged = [new_intervals[0]]
for lo, hi in new_intervals[1:]:
if lo <= merged[-1][1] + 1:
merged[-1] = (merged[-1][0], max(merged[-1][1], hi))
else:
merged.append((lo, hi))
intervals = merged
# 检查是否收敛到唯一值
if len(intervals) == 1 and intervals[0][0] == intervals[0][1]:
plaintext = intervals[0][0]
return plaintext.to_bytes(k, byteorder='big')
s += 1这段代码虽然是简化版本,但忠实地展现了攻击的核心逻辑:通过反复查询预言机来逐步收窄明文的候选区间。在实际攻击中,还需要考虑多区间合并、搜索策略优化等细节。
Bleichenbacher 攻击对现实世界产生了深远的影响。在 SSL 3.0 和 TLS 1.0/1.1/1.2 中,RSA 密钥交换(RSA key exchange)使用 PKCS#1 v1.5 加密来传输预主密钥(pre-master secret)。为了缓解 Bleichenbacher 攻击,TLS 规范要求服务器在检测到填充错误时不返回错误,而是使用一个随机生成的假预主密钥继续协议流程。然而,这种”防御”极其脆弱——实现中的任何微妙差异,无论是时间侧信道、缓存行为还是错误处理路径的不同,都可能重新暴露预言机。
2017 年,Hanno Böck、Juraj Somorovsky 和 Craig Young 发表了 ROBOT(Return Of Bleichenbacher’s Oracle Threat)攻击研究,发现包括 Facebook、PayPal 等主要互联网服务在内的大量 TLS 服务器仍然存在 Bleichenbacher 类型的预言机漏洞。这些漏洞的根源各不相同:有的是因为错误处理路径的时间差异,有的是因为不同错误类型返回了不同的 TLS alert 代码,有的则是因为在填充检查失败后过早终止了连接。ROBOT 的发现充分说明,对 PKCS#1 v1.5 的正确防御几乎不可能在工程上完美实现——唯一彻底的解决方案是迁移到 OAEP 或完全放弃 RSA 密钥交换(TLS 1.3 已经做出了这个选择)。
四、PKCS#1 v1.5 签名填充
PKCS#1 v1.5 除了定义加密填充外,还定义了签名填充格式。其结构如下:
0x00 || 0x01 || [0xFF 填充] || 0x00 || [DigestInfo]
其中 DigestInfo 是一个 ASN.1(Abstract Syntax Notation One)编码的结构,包含哈希算法标识符(OID,Object Identifier)和消息的哈希值。0xFF 填充使得整个数据块的长度等于 RSA 模数的字节长度。
签名验证过程是:验证方使用签名者的公钥对签名值执行 RSA 公钥操作(即计算 \(s^e \bmod n\)),然后检查结果是否符合上述格式,并比对其中的哈希值与消息的实际哈希值是否一致。
2006 年,Daniel Bleichenbacher(同一位研究者)发现了针对此签名方案的另一类攻击。当验证实现对 ASN.1 结构的解析过于宽松时——例如不检查 0xFF 填充的长度是否正确、不验证 DigestInfo 后面是否有多余的数据——攻击者可以伪造签名。这类攻击的原理是:攻击者构造一个整数,使其 \(e\) 次方(通常 \(e = 3\))的结果恰好以正确的 0x00 0x01 开头、包含正确的哈希值,但在 DigestInfo 之后有大量被验证器忽略的垃圾数据。由于 \(e = 3\) 时攻击者只需控制结果的前若干字节,计算立方根即可构造出合法的”签名”。
2014 年,Intel 的网络安全库(Intel Network Security Library)中发现了类似的漏洞,被命名为 BERserk。该漏洞的根源在于库代码使用了 BER(Basic Encoding Rules)而非 DER(Distinguished Encoding Rules)来解析 ASN.1 结构。BER 允许对同一数据有多种编码方式,而 DER 要求唯一的规范编码。这种解析宽松性使得攻击者能够在 DigestInfo 中插入额外的 ASN.1 元素,从而绕过签名验证。BERserk 影响了 Mozilla NSS 库和基于 Intel 库的多个产品,再次证明了密码学实现中”严格解析”原则的重要性。
这些攻击给我们的教训是:签名验证必须严格匹配预期格式的每一个字节,不能容忍任何”额外”的数据;ASN.1 解析应当使用 DER 模式并拒绝一切非规范编码;低公钥指数(特别是 \(e = 3\))会显著降低伪造签名的难度。
五、OAEP
OAEP(Optimal Asymmetric Encryption Padding,最优非对称加密填充)由 Mihir Bellare 和 Phillip Rogaway 于 1994 年提出,旨在从根本上解决 PKCS#1 v1.5 的安全缺陷。OAEP 的设计被纳入 PKCS#1 v2.0(后续为 v2.1 和 v2.2),也称为 RSA-OAEP。
OAEP 的核心构造基于一个 Feistel 风格的双轮变换,使用两个哈希函数(在实际标准中通过掩码生成函数 MGF,Mask Generation Function 实现)来交织消息与随机种子。其填充过程如下:
构造数据块 DB:\(\text{DB} = \text{lHash} \| \text{PS} \| \text{0x01} \| \text{M}\)。其中 lHash 是标签(label)的哈希值(默认为空字符串的哈希),PS 是零字节填充使 DB 的长度达到 \(k - \text{hLen} - 1\) 字节(\(k\) 为 RSA 模数字节长度,hLen 为哈希输出长度),0x01 是分隔符,M 是实际消息。
生成随机种子:生成一个长度为 hLen 的随机字节串 seed。
第一轮掩码:计算 \(\text{dbMask} = \text{MGF}(\text{seed}, \text{len}(\text{DB}))\),然后计算 \(\text{maskedDB} = \text{DB} \oplus \text{dbMask}\)。
第二轮掩码:计算 \(\text{seedMask} = \text{MGF}(\text{maskedDB}, \text{hLen})\),然后计算 \(\text{maskedSeed} = \text{seed} \oplus \text{seedMask}\)。
组装编码消息:\(\text{EM} = \text{0x00} \| \text{maskedSeed} \| \text{maskedDB}\)。
最终将 EM 转换为整数后执行 RSA 加密。
这个双轮 Feistel 结构的精妙之处在于:它将随机种子的信息”扩散”到整个数据块中,同时也将数据块的信息”回馈”到种子的掩码中。解密时,任何对密文的篡改都会导致 maskedDB 或 maskedSeed 的变化,进而在去掩码后破坏 DB 的结构(lHash 不匹配、分隔符 0x01 丢失等),从而被检测到。
Bellare 和 Rogaway 证明,在随机预言机模型(ROM,Random Oracle Model)下,如果 RSA 问题是困难的,则 RSA-OAEP 满足 IND-CCA2(在自适应选择密文攻击下的不可区分性)安全性。IND-CCA2 是公钥加密的最强标准安全定义,它意味着即使攻击者可以获得任意密文的解密结果(除了目标密文本身),也无法区分目标密文加密的是两个候选明文中的哪一个。
需要注意的是,OAEP 的安全证明依赖于随机预言机模型——即假设 MGF 的行为与真正的随机函数不可区分。这在理论上是一个有争议的假设(标准模型下的安全证明更为可取),但在实践中,使用 SHA-256 等安全哈希函数实例化 MGF 被广泛认为是安全的。
OAEP 的解密过程中同样需要注意实现细节。为了防止类似 Bleichenbacher 的预言机攻击,OAEP 解密时的所有格式检查(lHash 是否匹配、分隔符 0x01 是否存在、填充零字节的长度是否正确)必须在统一的时间内完成,并且在任何检查失败时返回相同的错误消息。现代密码库普遍采用常量时间比较(constant-time comparison)和统一错误处理来实现这一要求。
六、Fujisaki-Okamoto 变换
在后量子密码学(post-quantum cryptography)时代,填充方案的概念进一步演化为更通用的变换框架。Fujisaki-Okamoto 变换(简称 FO 变换)是其中最具影响力的构造之一。
FO 变换的核心思想是:给定一个仅满足较弱安全性(如 IND-CPA 或 OW-CPA,即单向性)的公钥加密方案,通过在加密过程中引入哈希函数来”绑定”随机性与明文,从而将其提升为满足 IND-CCA2 安全性的方案。
具体而言,FO 变换的加密过程大致如下:
- 选择一个随机消息 \(m\)。
- 计算加密所用的随机数 \(r = H(m)\),其中 \(H\) 是哈希函数。
- 使用底层方案加密:\(c = \text{Enc}(pk, m; r)\)。
- 计算密钥:\(K = G(m, c)\),其中 \(G\) 是另一个哈希函数。
- 输出密文 \(c\) 和共享密钥 \(K\)。
解密时,接收方先用私钥解密得到 \(m'\),然后重新计算 \(r' = H(m')\),并用 \(r'\) 重新加密 \(m'\) 得到 \(c'\)。如果 \(c' = c\),说明解密正确,输出 \(K = G(m', c)\);否则输出一个随机密钥以防止预言机泄露。
这个”解密后重新加密并比对”的步骤是 FO 变换的精髓——它使得攻击者无法构造一个”部分正确”的密文来从解密预言机中提取信息,因为任何对密文的修改都会导致重新加密的结果不匹配。
FO 变换在后量子密码标准化中扮演了核心角色。NIST 选定的密钥封装机制 ML-KEM(Module-Lattice-Based Key-Encapsulation Mechanism,此前称为 CRYSTALS-Kyber)正是在底层的 IND-CPA 安全的格基加密方案上应用了 FO 变换的变体来实现 IND-CCA2 安全性。类似地,其他后量子 KEM 候选方案如 BIKE、HQC 等也依赖 FO 变换或其变种来达到所需的安全级别。
FO 变换可以被视为 OAEP 思想的推广——两者都通过哈希函数将随机性与消息绑定来实现选择密文安全。但 FO 变换更为通用,它不依赖于底层方案的具体代数结构(如 RSA 的乘法同态性),因此适用于各种后量子方案所基于的格、编码、同源等不同数学结构。
七、PSS
PSS(Probabilistic Signature Scheme,概率签名方案)是由 Bellare 和 Rogaway 于 1996 年提出的签名填充方案,旨在为 RSA 签名提供与 OAEP 为加密所提供的同等安全保证。PSS 被纳入 PKCS#1 v2.1 标准,也称为 RSASSA-PSS。
PKCS#1 v1.5 签名填充的一个根本缺陷是它是确定性的——同一消息在同一密钥下的签名始终相同。虽然确定性签名本身并不违反签名的基本安全要求(不可伪造性,EUF-CMA),但 PKCS#1 v1.5 签名在标准模型下缺乏紧致的安全归约(tight security reduction),这意味着它的安全性证明依赖于对底层 RSA 问题的更强假设。
PSS 通过在签名过程中引入随机盐值(salt)来解决这个问题。其签名过程如下:
- 哈希消息:计算 \(\text{mHash} = \text{Hash}(M)\)。
- 生成盐值:生成一个随机盐值 salt,长度为 sLen 字节。
- 构造 M’:\(M' = \text{0x00} \times 8 \| \text{mHash} \| \text{salt}\)(8 个零字节 || 消息哈希 || 盐值)。
- 计算 H:\(H = \text{Hash}(M')\)。
- 构造数据块 DB:\(\text{DB} = \text{PS} \| \text{0x01} \| \text{salt}\),其中 PS 为零字节填充。
- 掩码生成:\(\text{dbMask} = \text{MGF}(H, \text{len}(\text{DB}))\)。
- 掩码应用:\(\text{maskedDB} = \text{DB} \oplus \text{dbMask}\)。
- 组装编码消息:\(\text{EM} = \text{maskedDB} \| H \| \text{0xBC}\)。
- 签名:\(s = \text{EM}^d \bmod n\)。
验证时,验证方用公钥恢复 EM,从 EM 中提取 H 和 maskedDB,用 H 生成掩码恢复 DB,从 DB 中提取盐值,然后重新计算 \(M'\) 和 \(H'\),最后比较 \(H' = H\)。
PSS 的安全证明表明:在随机预言机模型下,如果 RSA 问题是困难的,则 RSASSA-PSS 满足 EUF-CMA(existential unforgeability under adaptive chosen-message attack,自适应选择消息攻击下的存在不可伪造性)安全性,并且归约是紧致的。这意味着 PSS 签名的安全性可以直接从 RSA 问题的困难性中得出,而不需要额外的安全损失。
在实际部署中,TLS 1.3 要求所有 RSA 签名使用 PSS 填充,不再支持 PKCS#1 v1.5 签名。这一决策反映了密码学社区对”有安全证明的方案优于经验性安全的方案”这一原则的共识。
八、对称加密填充
填充问题不仅存在于非对称加密中,在对称加密领域同样至关重要。当使用分组密码(block cipher)的 CBC(Cipher Block Chaining)等模式时,如果明文长度不是分组大小的整数倍,就需要填充来补齐最后一个分组。
PKCS#7 填充是最常用的对称加密填充方案(也称为 PKCS#5 填充,后者是前者在 8 字节分组下的特例)。其规则非常简单:如果需要填充 \(n\) 个字节(\(1 \le n \le \text{blockSize}\)),则填充 \(n\) 个值为 \(n\) 的字节。例如,对于 16 字节分组,如果明文最后一个分组缺少 5 个字节,则填充 5 个 0x05。如果明文恰好是分组大小的整数倍,则额外添加一个完整的分组,全部填充为 blockSize(如 16 个 0x10)。这个额外分组的存在确保了解填充时的无歧义性。
ISO/IEC 7816-4 填充使用不同的策略:在消息末尾添加一个 0x80 字节,然后用 0x00 字节填充至分组边界。这种方案的优点是不需要在明文恰好对齐时添加额外分组。
零填充(zero padding)是最简单但也最不安全的方案:直接在消息末尾添加零字节。其问题在于,如果消息本身以零字节结尾,解填充时无法准确确定消息的原始长度。
密文窃取(CTS,Ciphertext Stealing)是一种完全避免填充的技术:它通过在最后两个分组之间”借用”字节来处理不对齐的明文,使得密文长度与明文长度完全相同。CTS 在磁盘加密等对密文长度敏感的场景中尤为有用。
对称加密填充面临的最大威胁是填充预言机攻击(padding oracle attack)。2002 年,Serge Vaudenay 发表了针对 CBC 模式 PKCS#7 填充的经典预言机攻击。攻击原理与 Bleichenbacher 攻击在本质上异曲同工:攻击者修改密文中倒数第二个分组的字节,观察解密方是否报告填充错误。通过系统地尝试不同的修改值,攻击者可以逐字节恢复明文。
这类攻击在实践中造成了重大影响。2010 年的 POET(Padding Oracle Exploit Tool)和 2014 年的 POODLE(Padding Oracle On Downgraded Legacy Encryption)攻击分别利用了 ASP.NET 和 SSL 3.0 中的填充预言机漏洞。POODLE 攻击直接导致了 SSL 3.0 协议的废弃。
九、走向无填充时代
回顾填充方案的发展历程,一个清晰的趋势正在浮现:现代密码学正在系统性地消除填充这一容易出错的环节。
从 Bleichenbacher 到 KEM:填充方案的演化脉络
回顾本文所述的各类填充方案,可以清晰地看到一条从”修补”到”重构”的演化脉络:
- Bleichenbacher 攻击(1998) 揭示了 PKCS#1 v1.5 的根本缺陷——解密过程中泄露的一比特填充有效性信息,足以让攻击者通过百万级查询完全恢复明文。
- OAEP(1994 提出,后广泛部署) 通过双轮 Feistel 变换将随机性与消息交织,在随机预言模型下实现了 IND-CCA2 安全。OAEP 的设计思路是:让任何对密文的篡改都不可避免地破坏内部结构,从而被解密方检测到。
- Fujisaki-Okamoto 变换 将 OAEP 的思想推广到任意公钥加密方案:通过”解密后重新加密并比对”的机制,将仅满足 CPA 安全的底层方案提升为 CCA 安全。FO 变换不依赖底层方案的具体代数结构,因此适用于格基、编码基等各种后量子方案。
- 现代 KEM 设计(ML-KEM 等) 将 FO 变换内化为方案的核心组件,公钥加密不再用于传输任意消息,而仅用于封装一个随机密钥。这从根本上消除了”填充可变长度消息”这一容易出错的需求。
这条脉络的每一步都是对前一步缺陷的回应:Bleichenbacher 暴露了”格式检查泄露信息”的问题;OAEP 通过结构化随机性解决了它;FO 变换将解决方案从 RSA 推广到任意方案;KEM 范式则通过缩小公钥加密的任务范围,让填充问题不再出现。
笔者认为,填充方案是密码学中”可证明安全”与”安全实现”之间鸿沟最宽的领域。OAEP 在随机预言模型下有漂亮的 CCA2 安全证明,但这份证明的前提假设包括”解密失败时返回统一的错误信息”——而现实中的 TLS 实现通过不同的 alert 代码、不同的响应延迟、甚至不同的 TCP 连接行为,轻而易举地违反了这个假设。ROBOT 攻击(2017)证明,Bleichenbacher 最初发现的漏洞在近二十年后仍然广泛存在,不是因为人们不知道这个问题,而是因为在工程层面完美消除信息泄露几乎不可能。这也是为什么密码学界最终选择了”换赛道”而非”修补”——与其要求每个实现者都完美处理填充错误,不如设计一个根本不需要填充的体系。
从工程实践来看,Fujisaki-Okamoto(FO)变换是后量子密码学中最不为人知的”幕后英雄”。它的核心思想看似朴素——解密后重新加密并比对密文,如果不一致就拒绝——但这个简单的”解密-重加密-比对”三步操作,将任何仅满足 CPA 安全(即只能抵抗窃听者)的公钥加密方案,升级为 CCA 安全(即能抵抗主动篡改密文的攻击者)。FO 变换的真正威力在于其通用性:它不依赖底层方案的具体代数结构,因此可以无差别地应用于格基方案(ML-KEM)、编码基方案(Classic McEliece)、以及同源基方案。事实上,NIST 后量子标准化竞赛中的每一个 KEM 决赛方案都使用了 FO 变换的某种变体。如果说 OAEP 是经典公钥密码时代的安全升级工具,那么 FO 变换就是后量子时代的对应物——而且它的影响范围更广,因为后量子方案的原始 CPA 安全性普遍弱于经典方案,对 CCA 升级的依赖也更为关键。
在对称加密领域,AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)模式的普及从根本上消除了填充预言机的威胁。GCM(Galois/Counter Mode)、ChaCha20-Poly1305 等 AEAD 方案基于计数器模式(CTR mode)或流密码,不需要将明文填充到分组边界。更重要的是,AEAD 将加密与认证绑定在一起——在验证认证标签(authentication tag)之前,任何关于密文内容的信息都不会泄露。这意味着即使存在解密预言机,攻击者也无法构造能通过认证检查的伪造密文,从而在协议层面彻底封堵了预言机攻击的入口。
在非对称加密领域,KEM(Key Encapsulation Mechanism,密钥封装机制)范式正在取代传统的公钥加密。KEM 的设计理念是:公钥加密不应直接用于加密任意消息,而应仅用于协商一个共享密钥,实际消息的加密由对称加密完成。这一分离简化了公钥加密的任务——KEM 只需安全地传输一个固定长度的随机密钥,不需要处理可变长度消息的填充问题。TLS 1.3 完全移除了 RSA 密钥交换,仅支持基于 Diffie-Hellman 的密钥协商(ECDHE 和 DHE),从协议层面消除了 Bleichenbacher 类攻击的可能性。
NIST 后量子密码标准化进程更是将 KEM 确立为非对称密码的首选范式。ML-KEM 的标准化表明,未来的公钥密码系统将以”KEM + AEAD”为基本架构,而非”公钥加密 + 填充”。在这个架构中,FO 变换在 KEM 内部提供 CCA 安全性,AEAD 在对称层提供认证加密,两者协同工作,使得传统意义上的”填充”不再是系统设计者需要直接面对的问题。
然而,这并不意味着我们可以完全忘记填充方案。遗留系统的存在意味着 PKCS#1 v1.5 和 CBC+PKCS#7 仍将在未来很长一段时间内继续运行。理解这些方案的设计缺陷和攻击原理,对于维护遗留系统的安全、正确实施协议降级防护(downgrade protection)以及避免在新系统中重蹈覆辙,都是不可或缺的知识。填充方案的历史告诉我们一个深刻的道理:密码学的安全不仅取决于数学难题的困难性,更取决于将数学难题嵌入实际系统时每一个细节的正确性。一个理论上安全的方案,如果在实现中泄露了哪怕一比特的填充有效性信息,就可能被攻击者利用来彻底瓦解整个系统的安全保障。
密码学百科系列 · 第 21 篇
← 上一篇:混合加密与 KEM/DEM | 系列目录 | 下一篇:TLS 协议 →