在 RSA-PSS 和 PKCS#1 v1.5 之间做选择,就是选择在 RSA 私钥运算之前如何编码哈希值。两者都出自 PKCS #1(RFC 8017),都获得 FIPS 186-5 批准,也都能使用同一个 RSA 密钥对。区别在于它们的安全性论证、确定性,以及签名者和验证者之间必须一致的参数。
概览
| RSASSA-PSS | RSASSA-PKCS1-v1_5 | |
|---|---|---|
| 规范 | RFC 8017 §8.1,编码方式 EMSA-PSS §9.1 | RFC 8017 §8.2,编码方式 EMSA-PKCS1-v1_5 §9.2 |
| 编码 | 加盐哈希,用 MGF1 掩码 | 0x00 01 FF…FF 00 ‖ DigestInfo |
| 随机化 | 是(除非盐长度为 0) | 否:相同的密钥和消息产生相同的签名 |
| 安全性证明 | 在随机预言模型下紧归约到 RSA 问题 | 没有可比的证明;针对正确实现的验证者,不存在实际可行的攻击 |
| 需要约定的参数 | 哈希、MGF 哈希、盐长度、尾部字段 | 只有哈希 |
| 签名大小 | 等于模数长度(RSA-2048 为 256 字节) | 相同 |
| 速度 | 相同的 RSA 运算 | 相同 |
| JOSE | PS256、PS384、PS512(盐 = 哈希长度) | RS256、RS384、RS512 |
| TLS 1.3 | 使用 RSA 的 CertificateVerify 必须采用(rsa_pss_rsae_sha256 0x0804、rsa_pss_pss_sha256 0x0809) | 只用于证书内的签名(rsa_pkcs1_sha256 0x0401) |
| X.509 | id-RSASSA-PSS 1.2.840.113549.1.1.10,带参数 | sha256WithRSAEncryption 1.2.840.113549.1.1.11 |
| FIPS 186-5 约束 | 盐长度介于 0 和哈希长度之间 | §5.4(h) 中的 DigestInfo 检查 |
安全性差异
PKCS#1 v1.5 签名填充始于1991年。它是确定性的,也从未有过像 PSS 那样简洁的安全性证明,但三十年过去,针对会检查完整编码块的验证者,仍然不存在实际可行的伪造方法。现实中的攻破都源于实现缺陷:Bleichenbacher 在2006年提出的攻击,针对会忽略 DigestInfo 之后多余字节的解析器,伪造了公钥指数为 3 时的签名;同类错误的变体后来又在 NSS(BERserk,2014年)等库中重现。RFC 8017 §8.2.2 通过重新编码预期的块并逐字节比较,避免了这一整类问题,FIPS 186-5 §5.4(h) 则把这些检查写进了规范。
RSASSA-PSS(由 Bellare 和 Rogaway 提出,2002年在 PKCS #1 v2.1 中标准化)把随机盐与消息哈希一起做哈希,再用 MGF1 对结果做掩码。它的安全性在随机预言模型下可以紧归约到 RSA 问题;而且由于签名是随机化的,单个签名中出现故障或侧信道时泄露的信息更少。盐长度为零时,PSS 变为确定性签名,但在略有不同的假设下仍保有安全性证明。
不要把两者与 PKCS #1 v1.5 的加密填充混为一谈:后者确实已被填充预言攻击攻破(Bleichenbacher 1998年,ROBOT 2017年),并已从 TLS 1.3 中移除。签名填充和加密填充是共用一个名字的两种不同方案。
互操作性:盐长度陷阱
PSS 要求签名者和验证者就三个参数达成一致:哈希、MGF1 哈希和盐长度。JOSE 把三者都固定了(PS256 表示 SHA-256、MGF1-SHA-256 和 32 字节的盐),TLS 1.3 则要求盐长度等于哈希长度。其他场合就必须自己检查:OpenSSL 1.1.1 和 3.0 默认使用最大长度的盐签名(RSA-2048 搭配 SHA-256 时为 222 字节),而 OpenSSL 3.1 及更高版本默认使用 auto-digestmax,生成与摘要等长的盐。于是,配置了固定盐长度的验证者会拒绝另一代版本生成的签名。
下面的脚本用 OpenSSL 3.6.5 运行,展示了这两种行为。
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out key.pem 2>/dev/null
openssl pkey -in key.pem -pubout -out pub.pem
printf 'hello' > msg
openssl dgst -sha256 -sign key.pem -out v15a.sig msg
openssl dgst -sha256 -sign key.pem -out v15b.sig msg
cmp -s v15a.sig v15b.sig && echo "PKCS#1 v1.5: identical signatures" || echo "PKCS#1 v1.5: different signatures"
openssl dgst -sha256 -sign key.pem -sigopt rsa_padding_mode:pss -out pssa.sig msg
openssl dgst -sha256 -sign key.pem -sigopt rsa_padding_mode:pss -out pssb.sig msg
cmp -s pssa.sig pssb.sig && echo "PSS: identical signatures" || echo "PSS: different signatures"
for saltlen in digest max; do
printf 'verify PSS with rsa_pss_saltlen:%s -> ' "$saltlen"
openssl dgst -sha256 -verify pub.pem -sigopt rsa_padding_mode:pss \
-sigopt "rsa_pss_saltlen:$saltlen" -signature pssa.sig msg 2>&1 | grep -o -E "Verified OK|salt length check failed"
done
cd / && rm -rf "$dir"
PKCS#1 v1.5: identical signatures
PSS: different signatures
verify PSS with rsa_pss_saltlen:digest -> Verified OK
verify PSS with rsa_pss_saltlen:max -> salt length check failed
接受 auto 的验证者会从签名中检测盐长度;在签名方不受你控制时,这是兼容性最好的设置。
密钥与证书
同一个 RSA 密钥可以生成这两种签名,证书中的 rsaEncryption 密钥也可用于两者(TLS 称之为 rsa_pss_rsae)。X.509 还定义了 id-RSASSA-PSS 密钥,其参数把密钥限定为只能用于 PSS(TLS 中的 rsa_pss_pss);这类密钥能防止跨方案使用,但许多工具和部分浏览器对它们支持不佳,因此用 rsaEncryption 密钥做 PSS 签名才是实际可行的选择。WebPKI 中的证书签名几乎全是 PKCS#1 v1.5 或 ECDSA,单凭这一点,验证者就必须支持 v1.5。
如何选择
- TLS 1.3 握手:PSS,强制要求;无论使用哪种 RSA 证书,库都会自动处理。
- JWT:所有使用方都支持时用
PS256,否则用RS256;更好的做法是考虑ES256(参见 ES256 与 RS256 对比)。 - 你签发的 X.509 证书和 CRL:为获得最大兼容性,使用 PKCS#1 v1.5(
sha256WithRSAEncryption),除非某个封闭生态系统统一采用了 PSS。 - 使用 RSA 密钥的新文档签名、固件签名或 API 签名:PSS,盐长度等于哈希长度。
- 与 HSM 和云 KMS 互操作:两者都受支持(例如 AWS KMS 中的
RSASSA_PSS_SHA_256和RSASSA_PKCS1_V1_5_SHA_256);与验证方的预期保持一致。 - 验证任一种签名:使用库提供的高层验证函数,切勿手动解码编码后的消息。
迁移说明
从 v1.5 迁移到 PSS 不需要新密钥,因此很容易分阶段进行。
- 先让验证者支持 PSS:在 JOSE 中,为该密钥允许
PS256(“一个密钥只用一种算法”仍是规则,因此要么用新的kid和algPS256发布同一份密钥材料,要么轮换密钥)。 - 显式固定参数:哈希 SHA-256、MGF1 SHA-256、盐长度 32 字节;不要依赖在不同版本间会变化的库默认值。
- 切换签名方,并留意提到盐长度或填充的验证失败。
- 旧签名过期后,停止为这些密钥接受 v1.5,只在证书或第三方有要求的地方保留 v1.5。
- 如果反正要轮换密钥,不妨考虑改用 ECDSA P-256 或 Ed25519,它们比任何 RSA 填充方式都更小,签名也更快。