18 KiB
好的,这是一个非常专业且重要的问题。RSA 的 PKCS#5 填充和 PSS 填充是两种完全不同的方案,用于不同的场景,其核心区别在于安全性和设计目的。
首先,需要纠正一个常见的术语混淆:
- PKCS#5 原本是为基于口令的加密(Password-Based Encryption)设计的,主要用于对称加密算法(如 DES)。当人们谈论 RSA 的“PKCS#5 填充”时,通常指的是 PKCS#1 v1.5 填充方案。这是一个历史遗留的误称,但已经非常普遍。
因此,我们接下来的对比实际上是 PKCS#1 v1.5 与 PSS 的对比。
核心区别概览
| 特性 | PKCS#1 v1.5 (常被误称为 PKCS#5) | PSS (Probabilistic Signature Scheme) |
|---|---|---|
| 主要用途 | 加密 和 签名 | 仅用于签名 |
| 设计哲学 | 确定性 / 旧标准 | 概率性 / 新一代安全标准 |
| 安全性可证明性 | 不可证明。存在已知的理论攻击(如自适应选择密文攻击),但在实践中可通过其他方式缓解。 | 可证明安全。在随机预言机模型下,其安全性可以归约到解决 RSA 问题的困难性上。 |
| 随机性 | 确定性(对于签名)。相同的消息和密钥总是产生相同的签名。 | 概率性。每次对同一消息签名都会产生不同的签名结果,因为它引入了随机数(盐)。 |
| 结构 | 格式固定,相对简单。 | 结构更复杂,使用掩码生成函数(MGF)和随机盐。 |
| 抗攻击性 | 较弱。容易受到如 Bleichenbacher 攻击等,需要额外的防护措施。 | 更强。随机盐的引入使其能有效抵抗多种攻击,尤其是选择明文攻击。 |
| 应用场景 | 旧系统、兼容性要求高的场景、TLS 1.2 及更早版本的 RSA 密钥交换。 | 新系统的首选。TLS 1.3、现代密码学库(如 OpenSSL, Bouncy Castle)的默认推荐。 |
深入解析
1. PKCS#1 v1.5 填充(用于签名)
这是一种确定性的填充方案。
-
过程:
- 对消息
M进行哈希(如 SHA-256),得到哈希值H(M)。 - 构建一个特定的 ASN.1 数据结构,其中包含了哈希算法的标识符和哈希值
H(M)。这是为了确保签名和验证双方使用相同的哈希算法。 - 将这个数据结构填充到与 RSA 模数
n长度相同的块。填充方式是固定的:0x00 0x01 0xff 0xff ... 0xff 0x00 || ASN.1 || H(M)。 - 对这个填充后的块进行 RSA 私钥运算(即
s = pad(m)^d mod n),生成签名s。
- 对消息
-
缺点:
- 确定性:相同的消息总是产生相同的签名,这为攻击者进行暴力破解或分析提供了便利。
- 格式固定:攻击者可以提交大量精心构造的报文给签名者进行签名,如果签名服务器没有严格验证解包后的结构格式,就可能泄露私钥信息。著名的 Bleichenbacher 攻击 就是利用了这个弱点。
2. PSS (Probabilistic Signature Scheme) 填充
这是一种概率性的、更安全的填充方案。
-
过程(简化版):
- 对消息
M进行哈希,得到H(M)。 - 生成一个随机数 salt(盐)。
- 将盐和消息哈希值一起进行哈希等运算,过程涉及掩码生成函数(MGF),最终构造一个复杂的、随机的待签名块。
- 对这个随机的块进行 RSA 私钥运算,生成签名
s。由于盐是随机的,所以每次签名结果都不同。
- 对消息
-
优点:
- 安全性可证明:数学上可以证明,破解 PSS 的难度等同于破解 RSA 基础问题本身。
- 随机性:有效防止攻击者通过收集签名来分析私钥。
- 增强的冗余:复杂的结构使得任何对签名或消息的篡改都极难通过验证。
总结与建议
| 方面 | PKCS#1 v1.5 | PSS |
|---|---|---|
| 安全性 | 较低,有已知风险 | 高,是现代标准 |
| 性能 | 稍快(因为计算简单) | 稍慢(因为引入了MGF和随机数生成) |
| 兼容性 | 极高,所有系统都支持 | 较好,现代系统和标准(如 TLS 1.3)都支持 |
| 你应该选择? | 仅在需要与旧系统保持兼容时使用。 | 在任何新项目或系统中,只要允许,就应优先使用 PSS 进行签名。 |
重要提示: 以上讨论主要围绕签名。对于 RSA 加密,同样有对应的填充方案:
- RSA 加密的旧标准叫 PKCS#1 v1.5 加密填充。
- RSA 加密的新、更安全的标准叫 OAEP (Optimal Asymmetric Encryption Padding)。
- PSS 只用于签名,不能用于加密。OAEP 用于加密,不能用于签名。
所以,完整的现代最佳实践是:
- RSA 签名:使用 RSA-PSS。
- RSA 加密:使用 RSA-OAEP。
非常好的问题!这正是 RSA-PSS 设计中最精妙和关键的部分。
简单直接的答案是:Salt 值不需要单独存放或传输。它被“捆绑”并隐藏在最终的签名值中,验证方可以通过解码和计算过程将其恢复出来。
下面我们来详细拆解这个过程。
1. PSS 签名的生成过程(含 Salt)
首先,我们回顾一下签名方是如何生成 PSS 签名的。这个过程会清楚地展示 Salt 是如何被使用的。
假设我们有:
- 消息:
M - 哈希函数:
Hash(如 SHA-256) - 掩码生成函数:
MGF(通常使用相同的哈希函数) - RSA 模数长度:
k字节 (例如 2048 位 = 256 字节) - Salt 长度:
sLen(通常等于哈希输出的长度,例如 SHA-256 则为 32 字节)
签名步骤:
- 生成 Salt:随机生成一个长度为
sLen的盐值salt。 - 第一次哈希:计算消息
M的哈希值mHash = Hash(M)。 - 构造数据块 (DB):
- 创建一个由固定填充位、哈希算法标识和 Salt 组成的数据块。
- 结构为:
PS | 0x01 | saltPS是一串0x00填充字节,其长度使得整个数据块 (DB) 加上其他部分恰好是k字节。0x01是一个固定的分隔符。
- 计算 MGF 的输入:
- 构造一个哈希块
H,通常为:H = 0x00 | mHash | salt - (注:实际标准格式为
H = Hash(0x00 | mHash | salt),这里为简化理解,描述其核心思想)。
- 构造一个哈希块
- 应用掩码:
- 使用 MGF 函数,以
H为种子,生成一个与数据块 (DB) 长度相同的伪随机掩码dbMask。 - 将数据块 (DB) 与这个掩码进行异或 (XOR) 操作,得到掩码后的数据块
maskedDB。maskedDB = DB XOR dbMask - 这一步是核心,它用随机数(由
H衍生)将 Salt 和固定填充位“加密”或“随机化”了。
- 使用 MGF 函数,以
- 形成待签名的编码消息 (EM):
- 将
maskedDB、H和固定的尾部填充拼接在一起,形成一个长度为k字节的编码消息EM。 - 结构:
EM = maskedDB | H | 0xbc(其中0xbc是一个固定的尾部标记)
- 将
- RSA 运算:
- 将
EM作为一个大整数,使用 RSA 的私钥对其进行签名运算(即s = EM^d mod n),得到最终的签名值s。
- 将
关键点: Salt s 并没有被直接加密,而是被作为构造 EM 的一部分。整个 EM 被 RSA 私钥加密后,Salt 就被完美地“隐藏”在了最终的签名值 s 之中。
2. PSS 验证过程(如何恢复 Salt)
验证方持有:消息 M、签名 s、以及发送方的 RSA 公钥。
验证步骤:
-
RSA 解密:
- 使用 RSA 的公钥对签名
s进行“解密”运算(即EM' = s^e mod n)。 - 如果签名有效,恢复出的编码消息
EM'应该和签名时生成的EM完全相同。
- 使用 RSA 的公钥对签名
-
解析 EM‘:
- 将
EM‘按照约定的格式拆解,得到maskedDB‘、H’和尾部标记0xbc。
- 将
-
恢复数据块 (DB):
- 这是最巧妙的一步。验证方现在有
H‘,但它需要DB来计算 Salt。 - 它使用和签名方完全相同的 MGF 函数,以
H’为种子,生成相同的掩码dbMask。 - 将
maskedDB‘与dbMask进行异或 (XOR) 操作,即可**还原出原始的数据块DB‘**。DB’ = maskedDB‘ XOR MGF(H’, length_of_maskedDB)`
- 这是最巧妙的一步。验证方现在有
-
解析 DB’ 并提取 Salt:
- 解析
DB‘的结构。它应该由一串0x00字节、一个0x01的分隔符和最后的sLen字节的 Salt 组成。 - 验证方直接从这个还原出的
DB’中提取出最后sLen字节,这就是 Salt 值salt‘。
- 解析
-
重建与验证:
- 验证方自己计算消息
M的哈希mHash = Hash(M)。 - 它使用刚提取的
salt‘和计算出的mHash,按照签名步骤 3 和 4 的方法,重建出哈希块H’’。 - 最终检查:比较自己计算出的
H’’和从EM’中解析出来的H‘是否完全相同。 - 如果两者一致,并且所有填充格式都正确,则签名验证通过。否则,失败。
- 验证方自己计算消息
总结与图示
Salt 的流向可以概括为:
salt -> DB -> maskedDB -> EM -> s (签名值)
验证时 Salt 的恢复路径为:
s (签名值) -> EM‘ -> maskedDB‘ -> (maskedDB‘ XOR MGF(H’)) -> DB‘ -> salt‘
这种设计的巨大优势:
- 无需传输 Salt:Salt 是自包含在签名中的,不需要额外的通信开销。
- 安全性:攻击者无法从签名
s或maskedDB中直接看到或猜出 Salt,因为它被 MGF 生成的掩码保护着。只有能正确“解密”(即通过 XOR 操作)出DB的人才能得到 Salt,而这个操作需要正确的H‘,这又依赖于原始消息M的正确性。 - 确定性验证:尽管签名本身是概率性的(每次不同),但验证过程是确定性的。对于给定的
(M, s),验证结果总是确定的。
希望这个详细的解释能帮助你彻底理解 PSS 中 Salt 的精妙之处!
好的,我们来深入探讨一下 RSA-OAEP(Optimal Asymmetric Encryption Padding,最优非对称加密填充)。它是一种用于 RSA 加密的、可证明安全的填充方案,旨在取代不安全的 PKCS#1 v1.5 加密填充。
核心设计思想
OAEP 的设计灵感来自于 Feistel 网络,其核心思想是:
- 概率性加密:通过引入随机数,使得每次加密相同消息都会产生完全不同的密文。
- 增强安全性:其安全性在“随机预言机模型”下可以被证明(即破解它的难度等同于解决 RSA 难题本身)。
- 防止攻击:特别能够抵抗针对 PKCS#1 v1.5 的 Bleichenbacher 选择密文攻击。
RSA-OAEP 的结构与加密过程
OAEP 的加密过程就是构建一个特殊的、结构化的、待加密的“消息块”的过程。我们假设 RSA 的模数 n 的长度为 k 字节(例如,2048 位 = 256 字节)。
输入:
- 原始消息
M - 一个可选的标签
L(通常为空字符串,可以用于绑定特定上下文) - 一个随机数生成器
- 两个哈希函数(通常为
Hash和MGF,MGF 是掩码生成函数,通常由另一个哈希函数构成)
步骤:
-
参数与长度检查:
- 消息
M不能太长。其最大长度受限于k - 2*hLen - 2,其中hLen是哈希函数的输出长度(例如 SHA-256 则为 32 字节)。这为随机数和固定填充预留了空间。
- 消息
-
生成随机数:
- 生成一个长度为
hLen的随机字节串seed(种子)。这是概率性的来源。
- 生成一个长度为
-
构造数据块 DB:
- 数据块
DB由三部分拼接而成:DB = lHash | PS | 0x01 | M - lHash:对可选标签
L进行哈希计算得到的值Hash(L)。如果L为空,这就是Hash(""),一个固定值。它的主要作用是让接收方可以验证上下文的一致性。 - PS:一串
0x00字节,作为填充位。其长度使得整个DB的长度为k - hLen - 1字节。 - 0x01:一个固定的分隔符,用于区分填充位和真实消息。
- M:原始消息。
- 数据块
-
应用掩码(Masking):
- 这是 OAEP 最核心、最精妙的一步,它使用了掩码生成函数
MGF。 - 用 seed 掩码 DB:使用 MGF 函数,以
seed为输入,生成一个与DB等长的掩码dbMask。dbMask = MGF(seed, length_of_DB) - 将
DB与dbMask进行异或操作,得到掩码后的数据块maskedDB。maskedDB = DB XOR dbMask - 现在,
DB中的消息M已经被随机化了。
- 这是 OAEP 最核心、最精妙的一步,它使用了掩码生成函数
-
掩码 seed:
- 接下来,再用
maskedDB去掩码seed,完成一种“双向绑定”。 - 使用 MGF 函数,以
maskedDB为输入,生成一个与seed等长(hLen)的掩码seedMask。seedMask = MGF(maskedDB, hLen) - 将
seed与seedMask进行异或操作,得到掩码后的种子maskedSeed。maskedSeed = seed XOR seedMask
- 接下来,再用
-
形成编码消息 (EM):
- 将处理后的各部分拼接起来,形成一个长度为
k字节的、结构化的待加密块。EM = 0x00 | maskedSeed | maskedDB - 0x00:一个前导零,有助于确保整个
EM表示的数值小于 RSA 模数n。
- 将处理后的各部分拼接起来,形成一个长度为
-
RSA 加密:
- 将
EM视为一个大整数。 - 使用 RSA 的公钥
(e, n)对其进行加密计算,得到最终的密文c。c = EM^e mod n
- 将
整个过程可以用下图直观表示(这是一个非常重要的概念图):
flowchart TD
subgraph Inputs [输入]
direction LR
M[消息 M]
L[标签 L<br>通常为空]
RNG[[随机数生成器]]
end
RNG -- 生成 --> seed[随机种子 seed<br>长度 hLen]
L -- 哈希 --> lHash[lHash = Hash(L)<br>长度 hLen]
subgraph Construction [构造数据块 DB]
direction LR
lHash
PS[填充串 PS<br>全零字节]
Delim[分隔符 0x01]
M
Construction_Output[DB = lHash | PS | 0x01 | M]
end
seed -- MGF1 --> dbMask[dbMask = MGF(seed, len(DB))]
Construction_Output -- XOR --> maskedDB[maskedDB = DB XOR dbMask]
maskedDB -- MGF1 --> seedMask[seedMask = MGF(maskedDB, hLen)]
seed -- XOR --> maskedSeed[maskedSeed = seed XOR seedMask]
maskedSeed --> EM[编码消息 EM]
maskedDB --> EM
EM --> Final[EM = 0x00 | maskedSeed | maskedDB]
Final -- 转换为整数 --> RSA[RSA公钥加密<br>c = EM<sup>e</sup> mod n]
RSA --> Ciphertext[输出密文 c]
RSA-OAEP 的解密与验证过程
解密是加密的逆过程,其核心是恢复并验证 EM 的结构。
输入: 密文 c,私钥 (d, n),标签 L(必须与加密时相同,通常为空)。
步骤:
-
RSA 解密:
- 使用 RSA 的私钥
(d, n)对密文c进行解密,得到一个大整数。 - 将这个整数转换为长度为
k字节的字节串,即恢复出编码消息EM。 - 如果解密失败或
EM的长度不为k,则立即中止并返回错误。
- 使用 RSA 的私钥
-
解析 EM:
- 按照结构解析
EM:EM = 0x00 | maskedSeed | maskedDB - 检查第一个字节是否为
0x00,如果不是,则说明密文无效,中止。
- 按照结构解析
-
恢复 seed:
- 从
EM中取出maskedSeed(前hLen字节)和maskedDB(剩余部分)。 - 计算
seedMask:seedMask = MGF(maskedDB, hLen) - 恢复出随机种子
seed:seed = maskedSeed XOR seedMask
- 从
-
恢复数据块 DB:
- 计算
dbMask:dbMask = MGF(seed, length_of_maskedDB) - 恢复出数据块
DB:DB = maskedDB XOR dbMask
- 计算
-
解析和验证 DB:
- 这是最关键的安全检查步骤。
- 将
DB解析为:lHash' | PS | 0x01 | M' - 验证:
a. 计算
Hash(L),与解析出的lHash‘比较,必须完全相同。 b. 检查填充串PS是否全部由0x00字节组成。 c. 检查0x01这个分隔符是否存在。 - 如果以上任何一项检查失败,不应返回解密出的消息
M‘,而应返回一个通用的“解密错误”。这是为了防止攻击者通过错误信息进行侧信道攻击(就像 Bleichenbacher 攻击那样)。
-
输出明文:
- 只有所有验证都通过,才输出解析出的消息
M‘作为原始明文M。
- 只有所有验证都通过,才输出解析出的消息
总结与特点
| 特性 | 说明 |
|---|---|
| 安全性 | 可证明安全,抵抗选择密文攻击(CCA2)。是现代应用的首选。 |
| 随机性 | 概率性加密,依赖随机数 seed,相同明文每次加密产生不同密文。 |
| 结构验证 | 解密时进行严格的格式验证(lHash,PS,0x01),任何无效输入都会导致失败,且不会返回任何具体错误信息。 |
| 错误处理 | 必须以一种“无信息”的方式失败,不泄露任何关于失败原因的具体细节,这是其安全性的重要组成部分。 |
| 应用 | TLS 1.3 中 RSA 加密的唯一指定方式。现代密码学库(如 OpenSSL, .NET, Java)的默认或推荐选项。 |
简单来说,OAEP 通过引入随机数和两次掩码操作(MGF),将消息、随机数和固定哈希值“混合”成一个结构化的、随机化的整体,再交给 RSA 加密。解密方则通过反向操作和严格的格式检查来确保密文的完整性和真实性,任何篡改都会被检测并导致解密失败。这种设计使其极其安全。