3. 签名验签流程:标准流程与常见误区
签名验签这事儿,说起来就两步——签名和验签。但我在项目里见过太多人在这两步上栽跟头了。说白了,流程本身不复杂,复杂的是那些容易被忽略的细节。
今天我就把标准流程掰开揉碎了讲,顺便聊聊我踩过的那些坑。
3.1 标准签名生成流程
签名生成,本质上就是用私钥对数据的“摘要”做加密运算。嗯,这里要注意,我们签的不是原始数据本身,而是它的哈希值。
标准流程分四步:
- 计算摘要:对原始消息做哈希运算,得到固定长度的摘要值。
- 填充摘要:把摘要值按照签名算法的要求进行填充(比如 PKCS#1 v1.5 或 PSS)。
- 私钥加密:用签名者的私钥对填充后的摘要做加密运算。
- 输出签名:把加密结果作为签名值输出,通常和原始消息一起发送。
核心要点:签名 ≠ 加密。签名用的是私钥,验签用的是公钥。千万别搞反了。
我举个例子,用 RSA 签名时,流程大概是这样的:
// 伪代码示例:RSA 签名生成
Message m = "转账 1000 元给 Alice";
byte[] digest = SHA256.hash(m); // 步骤1:计算摘要
byte[] padded = RSA_PKCS1_PADDING(digest); // 步骤2:填充
byte[] signature = RSA_private_encrypt(padded, privateKey); // 步骤3:私钥加密
// 输出 (m, signature)
我个人习惯在步骤1之前先检查一下原始数据的编码。UTF-8 还是 GBK?这个不统一,后面验签必挂。我在项目中遇到过两次这种问题,都是因为发送方和接收方用的编码不一致。
3.2 标准验签流程
验签是签名的逆过程,但逻辑上不完全对称。验签的目的是确认两件事:
- 数据没有被篡改(完整性)
- 数据确实来自声称的签名者(真实性)
标准验签流程也是四步:
- 分离签名与消息:从接收到的数据中提取出原始消息和签名值。
- 计算消息摘要:对原始消息做同样的哈希运算,得到摘要值。
- 公钥解密签名:用签名者的公钥对签名值做解密运算,得到填充后的摘要。
- 比对摘要:从解密结果中提取出原始摘要,与步骤2计算的摘要进行比对。一致则验签通过,否则失败。
注意:步骤4的比对必须是“常量时间比较”,不能用普通的字符串 equals。否则容易遭受时序攻击。我曾经在一个金融项目中看到有人用 String.equals 做比对,被安全审计直接打回。
验签的代码逻辑:
// 伪代码示例:RSA 验签
Message m = receivedMessage;
byte[] signature = receivedSignature;
byte[] digest = SHA256.hash(m); // 步骤2:计算摘要
byte[] decrypted = RSA_public_decrypt(signature, publicKey); // 步骤3:公钥解密
byte[] expectedDigest = extractDigest(decrypted); // 从填充中提取摘要
boolean valid = constantTimeEquals(digest, expectedDigest); // 步骤4:常量时间比对
3.3 常见流程误区
这部分是我最想说的。很多开发者觉得签名验签就是调个 API,没什么技术含量。但实际项目中,坑全在细节里。
误区一:签名时直接加密原始数据
有人觉得“签名 = 用私钥加密原始数据”。这是错的。签名是对摘要做运算,不是对原始数据。你想想看,如果直接加密原始数据,那签名值会随着数据长度变化,性能差不说,还容易泄露数据内容。
误区二:验签时重新计算签名再比对
我见过有人这么写:
// 错误做法
byte[] computedSignature = sign(message, privateKey);
boolean valid = computedSignature.equals(receivedSignature);
这完全不对。验签用的是公钥,不是私钥。而且这种比对方式暴露了私钥的使用场景,存在安全风险。
误区三:忽略哈希算法的选择
MD5 和 SHA-1 已经不被推荐用于签名了。我在一个遗留系统里看到过用 MD5 做签名的,当时就建议客户尽快升级。现在至少要用 SHA-256。
| 哈希算法 | 安全状态 | 推荐场景 |
|---|---|---|
| MD5 | 已破解,不推荐 | 仅用于校验,不用于签名 |
| SHA-1 | 已破解,不推荐 | 仅用于兼容旧系统 |
| SHA-256 | 安全 | 推荐用于签名 |
| SHA-3 | 安全 | 新系统可考虑 |
误区四:填充模式选错
RSA 签名常用的填充模式有 PKCS#1 v1.5 和 PSS。PSS 更安全,但兼容性稍差。我建议新系统优先用 PSS。如果你还在用 v1.5,要注意它有一些已知的攻击向量。
小技巧:如果你不确定用什么填充模式,查一下你用的加密库的默认值。很多库默认用 PKCS#1 v1.5,但这不是最安全的选择。
误区五:公钥来源不验证
验签时用的公钥,必须来自可信的渠道。如果公钥被替换了,那验签就形同虚设。我在项目中遇到过有人直接从 HTTP 请求头里拿公钥的,这跟没验签一样。
3.4 签名验签流程全景图
下面这张图把整个流程串起来了。你可以看到签名和验签是对称的,但使用的密钥不同。
3.5 避坑指南
最后,我总结几个实战中容易踩的坑:
- 密钥格式不统一:DER、PEM、PKCS#8、PKCS#12……不同系统之间密钥格式不一致,会导致验签失败。我建议统一用 PEM 格式,兼容性最好。
- 签名值编码问题:签名值是二进制数据,传输时要用 Base64 编码。有人直接传二进制,结果被文本协议截断。
- 时间戳问题:有些签名方案会带上时间戳,验签时检查时间有效性。如果系统时间不同步,签名会失效。
- 重放攻击:签名本身不防重放。如果你需要防重放,要在签名内容里加入 nonce 或时间戳。
曾经我接手过一个项目,他们的签名验签流程看起来没问题,但线上总是偶发验签失败。查了三天,最后发现是签名方和验签方用的哈希算法不一样——一个用了 SHA-256,另一个用了 SHA-256 但 truncate 成了 128 位。这种低级错误,往往是最难排查的。