RSA-PSS 盐长度工程实战:MAX_LENGTH 的真实长度与最小安全值

密码学 · 2026-11-01 · 11 阅读

前言

RSA-PSS(RSASSA-PSS)是 RSA 签名中唯一具有紧致安全证明的方案,而 salt_length 是它最容易踩坑的参数。很多工程师以为"盐越长越安全、MAX_LENGTH 就是最大安全值",甚至把 MAX_LENGTH 记成 hash_len - 2(SHA-256 下 30 字节)。

这个理解是错的。本文基于 RFC 8017、FIPS 186-5 原文和 cryptography 48.0 的实测,把三件事讲清楚:

  • MAX_LENGTH 到底是多少字节,它随什么变化;
  • 盐长度和安全强度的真实关系(没有"越大越安全"这回事);
  • 各语言库的默认盐长度为何不一致,跨语言对接怎么不出事。

一、MAX_LENGTH 不是 30 字节,是哨兵对象

1.1 它解析成多少

padding.PSS.MAX_LENGTH 不是一个数字,而是 cryptography 库的哨兵对象(_MaxLength)。它的具体取值在签名/验签时才解析,等于 RFC 8017 允许的上限 emLen - hLen - 2:

  • emLen = RSA 模数长度(字节)。2048 位密钥 = 256 字节,3072 位 = 384 字节
  • hLen = 哈希输出长度。SHA-256 = 32 字节
所以同一把 2048 位密钥,用 SHA-256 时 MAX_LENGTH = 222;密钥换成 3072 位,它就变成 350。

实测验证(cryptography 48.0,2048 位密钥):

运行输出:222。MAX_LENGTH 签出的签名只有 salt=222 能通过验证;salt=30、salt=32 全部报 InvalidSignature。
>
常见错误:有人把 MAX_LENGTH 当成 30 字节(hLen-2)去写互操作协议,结果对端按 222 验证直接失败。30 = hLen-2,而 MAX_LENGTH = emLen-hLen-2,两者相差 emLen-2×hLen(2048 位 + SHA-256 下 192 字节)。

1.2 为什么 30 字节是错的,48 字节反而是合法的

hash_len - 2 = 30 这个值来自对 RFC 编码步骤的误读。真实的约束是:

  • RFC 8017(9.1.1 第 3 步):emLen < hLen + sLen + 2 时报 encoding error。即 sLen ≤ emLen - hLen - 2,2048 位 + SHA-256 下上限是 222 字节
  • FIPS 186-5(5.4(g)):0 ≤ sLen ≤ hLen,即 FIPS 场景下上限收紧为 32 字节(SHA-256)

注意:sLen=0 是 RFC 8017 明确允许的。但 FIPS 186-5 要求 0 ≤ sLen ≤ hLen,也就是说 0 合法;真正"违反 FIPS"的是 sLen > hLen(如 48、222 字节)。很多文档说"短盐违反 FIPS 最低要求",方向正好反了——是长盐违反 FIPS。

二、三种取值模式与安全性的真实关系

2.1 三种模式对照

模式SHA-256 / 2048 位下取值依据
MAX_LENGTH(哨兵)222(= emLen-hLen-2)RFC 8017 上限
DIGEST_LENGTH(哨兵)32(= hLen)FIPS 186-5 常用值
自定义(如 0/16/32)0 ≤ sLen ≤ 32(FIPS)或 ≤ 222(纯 RFC)场景需求

2.2 盐长度 ≠ 安全强度

这是本文最重要的一个澄清。PSS 的安全性证明(Bellare-Rogaway,紧致归约)建立在 RSA 问题(inverting RSAVP1)的困难性上,归约本身不要求盐达到某个"最大"值。

RFC 8017 第 8.1 节原文说得很直接:

"Typical salt lengths in octets are hLen ... and 0. In both cases, the security of RSASSA-PSS can be closely related to the hardness of inverting RSAVP1."
Coron 在 Eurocrypt 2002(*Optimal Security Proofs for PSS and Other Signature Schemes*)进一步给出紧致归约的量化结论:

"For an application in which at most one billion signatures will be generated, k0 = 30 bits of random salt are actually sufficient to guarantee the same level of security as RSA, and taking a larger salt does not increase the security level."
翻译成工程语言:

  • 签名安全强度主要取决于 RSA 密钥长度 和 哈希函数强度,而不是盐的长度
  • 盐的作用是随机化——同一消息每次签名产生不同签名值,使攻击无法把多次签名关联起来。盐的随机性质量(必须来自 CSPRNG)比它的长度重要得多
  • "盐越长攻击难度呈指数级增长"是错误表述。超过 30 bit 之后,增加盐长不增加安全强度
  • sLen=0(确定性)仍有可证明的安全性(归约到 FDH),只是失去了随机性带来的实现层面好处(如对故障注入的容忍)
所以选盐长度时的正确问法不是"要多长才够安全",而是:

  • 合规:目标标准允许什么范围(FIPS 186-5:0 ≤ sLen ≤ hLen)
  • 互操作:对接方默认用多长的盐
  • 随机性:盐值是否每次由 CSPRNG 生成

2.3 盐长度不影响签名大小

PSS 签名长度恒等于 RSA 模数字节数(2048 位 = 256 字节),与盐长无关。盐长只改变内部编码中 PS(零填充段)的长度。这点常被误读为"盐越长签名越大"——对,错的。

三、跨语言默认值差异(实测与文档核对)

各库默认值不同,是生产环境验签失败的第一大原因。注意:这里列的是"默认"行为,不同版本有差异,对接前务必按目标版本实测。

库默认盐长度(SHA-256 / 2048 位)说明
Python cryptographyMAX_LENGTH → 222哨兵在签名时解析为 emLen-hLen-2
Go crypto/rsa(默认 opts)PSSSaltLengthAuto → 222签名取最大,验证自动探测
Rust ringhLen → 32固定取哈希输出长度
Java PSSParameterSpec.DEFAULT20旧默认(SHA-1 时代,JDK 19 起已废弃);现代 named algorithm(SHA256withRSA/PSS)→ 32(= hLen)
OpenSSL 3.x CLI(-rsassa-pss)签名时 clamp 到 hLen → 323.0 起为符合 FIPS 186-4/5 的 sLen ≤ hLen 收紧了默认值
踩坑场景:Python 端用默认 MAX_LENGTH(222 字节)签名,Go 端用默认 PSSSaltLengthAuto 验证——能成功(验证侧自动探测盐长)。但如果 Go 端显式指定 PSSSaltLengthEqualsHash(32 字节)验证,必然失败。反过来,Rust ring 签出的 32 字节盐签名,Python 端用默认 MAX_LENGTH 验证也失败。

GO
// 假设 digest := sha256.Sum256(msg)
// Go 的 VerifyPSS 第二个参数是 crypto.Hash 常量(sha256.SHA256),不是 hash 实例
err1 := rsa.VerifyPSS(pub, sha256.SHA256, digest, sig, &rsa.PSSOptions{
    SaltLength: rsa.PSSSaltLengthAuto,        // 成功:验证侧自动探测盐长
})
err2 := rsa.VerifyPSS(pub, sha256.SHA256, digest, sig, &rsa.PSSOptions{
    SaltLength: rsa.PSSSaltLengthEqualsHash,  // 失败:显式 32 字节与 222 不匹配
})

自动探测盐长的实用函数(排查"签名看着对但验不过"时首选):

不要把"多盐长逐个尝试"当作生产验签逻辑。多盐试探只用于互操作排查;生产环境应把盐长作为协议参数显式约定并固定,验签时只用约定的那一个值。逐个试探等于放弃"盐长不匹配即拒绝"的检查,且可能被用来放大侧信道/验证开销。

四、生产环境最佳实践

4.1 选型决策

场景推荐盐长理由
FIPS 186-5 合规DIGEST_LENGTH(32)标准明确 0 ≤ sLen ≤ hLen,32 字节是常用值
仅遵循 RFC 8017、追求最大兼容性MAX_LENGTH(222)验证侧自动探测时对任意盐长都接受
跨语言对接显式写死一个值(建议 32)所有主流库都支持 32;协议文档中注明
确定性场景(可重放、便于离线复现)0合法但失去随机性,需评估合规要求

4.2 推荐配置示例

4.3 遗留系统兼容

对接老系统(salt 未知)时,用第 3 节的 probe_salt_length 做一次性排查,确定对端盐长后写死进协议。不要长期保留多盐试探。

4.4 性能

盐长对性能影响可忽略:主要成本在 RSA 模幂运算(O(密钥长度)),MGF1 生成和随机数生成都是 O(hLen) 级别。在 2048 位密钥下,sLen 从 0 到 222 的签名耗时差异远小于一次 RSA 运算本身的抖动。真正的性能考量是密钥长度和是否走 CRT,与盐长无关。

五、常见错误清单

  • 把 MAX_LENGTH 当成 30 字节 —— 它是哨兵,解析为 emLen-hLen-2(2048+SHA-256 下 222 字节)
  • 认为盐越长越安全 —— 超过 ~30 bit 后不再增加安全强度(Coron 2002);安全强度由密钥长度和哈希决定
  • 认为短盐违反 FIPS —— 方向反了;FIPS 186-5 是上限 sLen ≤ hLen,0 字节合法,长于 hLen 才违规
  • 跨语言对接不约定盐长 —— Python/Go 默认 222,Rust/Java 常用 32,不写死必然翻车
  • 把多盐试探当生产验签 —— 只用于排查;生产应固定盐长并严格匹配
  • 用固定/可预测的盐值 —— 盐必须每次由 CSPRNG 生成;用固定盐会失去随机化优势(sLen=0 是合法例外,但属有意为之的确定性方案)

六、国密对照

RSA-PSS 这套参数体系(emLen、hLen、sLen、trailer 0xBC)是 PKCS#1 独有的。SM2 数字签名没有"盐长"参数:SM2 签名的随机性来自每次独立生成的随机数 k(GM/T 0003.2-2012 第 5.2 节签名算法),没有可配置的 salt_length,也不存在"默认值"一说。

所以:

  • 做国密合规改造时,RSA-PSS 签名点直接替换为 SM2 签名(GM/T 0009-2012 使用规范),不存在"选多长 SM2 盐"的问题
  • 双算法并行期(RSA + SM2 混合验签),两侧的验签逻辑完全独立,不要复用 PSS 盐长探测代码
  • SM2 侧真正要防的坑是随机数 k 的生成质量(每次独立、CSPRNG)——等价于 RSA-PSS 里"盐的随机性",但机制不同
国密场景下,凡是原来用 RSA-PSS 的签名点(证书签名、API 报文签名、国密 TLS 握手中的证书/消息签名),替换为 SM2 后不再有"盐长"这个参数;对接时只要确认对端按 GM/T 0003.2-2012 生成、按 GM/T 0009-2012 规范验签即可,不要照搬 PSS 的盐长约定。

参考来源

相关实践