3、RSA 签名算法:密钥对生成、签名与验签流程、填充模式
RSA 签名,说白了就是利用非对称加密的「反向操作」来实现身份认证。你私钥签的名,我公钥能验过,那这消息肯定是你发的。这个逻辑听起来简单,但实际落地时坑不少。我早年做安全 SDK 时,就因为填充模式选错,跟服务端联调了整整两天……嗯,咱们今天就把这事彻底聊透。
3.1 RSA 密钥对生成:数学基础与实操
RSA 的安全性建立在「大整数分解」这个数学难题上。两个大质数相乘很容易,但反过来分解就很难。密钥生成就是围绕这个核心展开的。
生成步骤大致如下:
- 选两个大质数 p 和 q。实际应用中,p 和 q 至少 512 位,且不能太接近。
- 计算 n = p × q。n 的位数就是密钥长度,比如 2048 位 RSA,n 就是 2048 位。
- 计算欧拉函数 φ(n) = (p-1)(q-1)。
- 选公钥指数 e。常用值 65537(0x10001),这个值在安全性和性能之间平衡得很好。
- 计算私钥 d,满足 e × d ≡ 1 (mod φ(n))。d 就是私钥的核心。
关键点:p 和 q 必须安全销毁。我见过有人把 p、q 留在日志里,结果整个密钥体系直接报废。生成后立即用安全方式擦除,别犹豫。
实际开发中,我们很少手写密钥生成。OpenSSL 一把梭:
# 生成 2048 位私钥
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
# 提取公钥
openssl pkey -in private.pem -pubout -out public.pem
我个人习惯用 3072 位起步。2048 位目前还安全,但 NIST 已经在往 3072 推了。你想想看,签名系统一用就是好几年,别刚上线就面临升级压力。
3.2 签名与验签流程:到底在算什么东西?
RSA 签名不是直接对原始消息加密。实际流程分三步:
- 哈希摘要:对消息做 Hash(比如 SHA-256),得到固定长度的摘要。
- 填充:把摘要塞进特定格式的填充结构中。
- 私钥加密:用私钥对填充后的数据做模幂运算,输出签名值。
验签就是反过来:公钥解密签名得到填充数据,拆出摘要,跟本地算的摘要比对。
避坑:签名不是「加密消息」。RSA 加密和签名虽然数学运算一样,但填充方式完全不同。千万别混用,否则安全漏洞直接拉满。
看一段 Python 示例,用 cryptography 库实现签名与验签:
from cryptography.hazmat.primitives import hashes, asymmetric
from cryptography.hazmat.primitives.asymmetric import padding
# 签名
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 验签
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("验签通过")
except InvalidSignature:
print("签名无效")
注意看,签名和验签用的填充参数必须完全一致。我曾经遇到过 salt_length 不匹配导致验签失败,排查了半天才发现是两端配置不同步。
3.3 填充模式:PKCS#1 v1.5 与 PSS
填充模式是 RSA 签名里最容易出问题的地方。说白了,就是给摘要「穿衣服」,让它符合 RSA 运算的格式要求。
| 特性 | PKCS#1 v1.5 | PSS(PKCS#1 v2.1) |
|---|---|---|
| 结构 | 固定格式:0x00 0x01 + 填充字节 + 0x00 + DigestInfo | 随机盐值 + 掩码生成函数(MGF) |
| 安全性 | 存在已知攻击向量(如 Bleichenbacher 攻击变种) | 有严格安全证明,抗攻击能力强 |
| 兼容性 | 广泛支持,老旧系统首选 | 较新,部分旧库不支持 |
| 签名长度 | 固定,与密钥长度一致 | 固定,与密钥长度一致 |
| 推荐场景 | 遗留系统兼容、硬件限制环境 | 新系统、高安全需求场景 |
警告:PKCS#1 v1.5 签名本身目前没有公开的实用攻击,但它的结构太「规整」了,容易在实现层面出问题。PSS 引入了随机盐值,同样的消息每次签名结果都不同,这天然防御了某些侧信道攻击。新项目我强烈建议直接用 PSS。
PSS 的核心在于两个东西:盐值(salt)和掩码生成函数(MGF)。盐值越长,安全性越高,但签名空间不变。MGF 本质上是一个伪随机函数,用来扩散盐值的影响。
我曾经在一个 IoT 项目里被迫用 PKCS#1 v1.5,因为设备上的加密芯片只支持这个模式。当时做了两件事:一是严格校验签名输入长度,二是加了额外的 HMAC 做双重校验。嗯,妥协方案,但总比裸奔强。
3.4 知识体系总览
下面这张图把 RSA 签名的核心脉络串起来了。我建议你把它存下来,以后设计签名方案时对照着看。
3.5 选型建议
最后给点实在的建议。你设计签名方案时,按这个优先级来:
- 首选:RSA-PSS + SHA-256 + 3072 位密钥。安全、标准、未来十年无忧。
- 次选:RSA-PKCS#1 v1.5 + SHA-256 + 2048 位。兼容老旧系统时用,但要做好升级计划。
- 不推荐:任何低于 2048 位的密钥、SHA-1、自定义填充。
个人经验:我习惯在签名数据里带上时间戳和随机数。这样即使同一份文档多次签名,结果也不同,能有效防止重放攻击。另外,私钥的存储一定要用硬件安全模块(HSM)或安全飞地(TEE),别裸写在配置文件里。
RSA 签名看着简单,但每个环节都有讲究。密钥生成要真随机,填充模式要选对,验签要严格比对。把这些基础打牢了,后面讲 ECDSA、Ed25519 时你才能触类旁通。