4. 数字签名原理:签名与验签流程、RSA与ECDSA对比
数字签名,说白了就是给数据盖个不可伪造的“电子印章”。你想想看,在 REST API 的显式加密改造里,我们不仅要防数据被偷看,还要防别人篡改,更要证明这条数据确实是你发的。签名机制就是干这三件事的——完整性、身份认证、不可否认性。
我在好几个项目里都遇到过“只加密不签名”的坑。你以为加密了就安全了?攻击者虽然解不开密文,但可以截获后把密文原封不动转发,接收方解密出来发现是合法的,还以为是你发的,这就是典型的重放攻击变种。所以,加密 + 签名必须同时上。
4.1 签名与验签流程
数字签名用非对称密码学的逆操作。私钥签名,公钥验签。流程可以拆成三步:
发送方(签名)
- 计算消息摘要:对消息做哈希运算,比如 SHA-256,得到固定长度的哈希值。
- 用私钥加密哈希:把哈希值拿自己的私钥做“签名运算”(RSA 就是加密,ECDSA 是一套椭圆曲线算法生成两个数)。这一步输出的就是签名值。
- 发送消息 + 签名:把原始消息和签名一起发给接收方。注意,消息本身可以是明文,也可以再加密。
接收方(验签)
- 分离消息和签名:从报文中取出原始消息和附带的签名。
- 计算消息摘要:对收到的消息做同样的哈希运算。
- 用公钥验证签名:用发送方的公钥对签名做验签运算,得到一个哈希值(或恢复出的摘要),与第二步计算的哈希值比对。一致则通过,否则拒绝。
X-Signature 或 Digest 字段放签名和哈希。这样消息体可以保持 JSON 原始格式,签名不影响解析。
4.2 RSA 与 ECDSA 对比
现在主流签名算法俩:RSA 和 ECDSA。很多刚入行的朋友问我选哪个,我的回答一直是——看场景,看性能要求,看密钥长度限制。别盲目跟风。
| 对比维度 | RSA (PKCS#1 v1.5/PSS) | ECDSA (如 P-256, secp256r1) |
|---|---|---|
| 安全性基础 | 大整数分解困难 | 椭圆曲线离散对数困难 |
| 相同安全强度下密钥长度 | 2048 位(128 位安全) | 256 位(128 位安全) |
| 签名速度 | 较慢(模指数运算) | 较快(点乘运算) |
| 验签速度 | 快(指数小) | 较慢(需要求逆等) |
| 签名长度 | 与密钥等长(如 256 字节) | 固定 64 字节(P-256) |
| 兼容性 | 极广(几乎所有库支持) | 广,但部分老旧系统不支持 |
| 抗量子计算 | 高脆弱性(Shor 算法直接破解) | 同样脆弱,但密钥短影响小? |
光看表格还不够。我给你说个实际例子:前几年有个 IoT 项目,设备端性能极弱,CPU 只有几十兆。用 RSA 2048 签名一次要 100 多毫秒,用户等得要骂娘。换成 ECDSA P-256,签名直接降到 5 毫秒以内。但验签客户端跑在服务端,算力强,反而 ECDSA 验签比 RSA 慢一点,不过总体还能接受。
我曾经接手过一个遗留系统,对方用了 RSA 1024 位签名。那时候我还觉得还好,结果一查,NIST 早就不推荐 1024 位了。后来所有终端必须升级到 2048 位,折腾了两个月。所以现在的底线:RSA 至少 2048 位,ECDSA 至少 P-256 (secp256r1)。千万别为了省带宽用 160 位曲线。
签名算法选型建议
- 高并发验签场景(如网关):优先用 RSA。验签快,签名次数少(服务端签名,客户端验签)。
- 资源受限嵌入式 / IoT:优先用 ECDSA。签名小、速度快,省密钥存储空间。
- 跨语言互操作:RSA 更稳妥,ECDSA 的编码格式(DER / 原始 R||S)在不同库之间偶尔会翻车。我踩过一次坑:Go 的 ecdsa 签名输出是 ASN.1 DER,Java 默认是纯 R||S,两边没对齐,折腾了半天。
4.3 核心流程一览
下面这张图把签名与验签的总体流程串起来了。我建议你仔细看看箭头和数据流向,理解每一个环节为什么不可或缺。
从图里可以看到,发送方和接收方各做一次哈希,验签的本质就是把两个哈希值比对。私钥只在签名方持有,公钥公开出去。只要私钥不泄露,任何人无法伪造你对消息的签名。
4.4 RSA 与 ECDSA 代码片段示意
下面给两个简单例子,展示在 Node.js 里怎么签名和验签。实际项目别直接用这个裸代码,要配上密钥安全存储和异常处理。
// RSA 签名 (Node.js crypto)
const { createSign, createVerify } = require('crypto');
function rsaSign(message, privateKeyPem) {
const sign = createSign('RSA-SHA256');
sign.update(message);
return sign.sign(privateKeyPem, 'base64');
}
function rsaVerify(message, signature, publicKeyPem) {
const verify = createVerify('RSA-SHA256');
verify.update(message);
return verify.verify(publicKeyPem, signature, 'base64');
}
// ECDSA 签名 (同样用 crypto)
function ecdsaSign(message, privateKeyPem) {
const sign = createSign('SHA256');
sign.update(message);
return sign.sign(privateKeyPem, 'base64');
}
function ecdsaVerify(message, signature, publicKeyPem) {
const verify = createVerify('SHA256');
verify.update(message);
return verify.verify(publicKeyPem, signature, 'base64');
}
这两个函数结构几乎一样,区别就在第三个参数——RSA 指定填充模式,ECDSA 只指定哈希。实际使用中要小心:RSA-SHA256 默认走 PKCS#1 v1.5 填充,有历史漏洞,我建议换 PSS(RSA-PSS-SHA256)。而 ECDSA 签名结果在不同库里编码格式可能不同,最好统一为 IEEE P1363 的 R||S 格式,避免互操作问题。
签名过程不能把私钥直接硬编码在代码里,更不能传到客户端。我曾经在代码审查时发现同事把私钥写在配置文件中,还 push 到了 GitHub 公开仓库。幸好及时发现,否则整个 API 系统等于白送。私钥一定要放在密钥管理服务(KMS)或硬件安全模块(HSM)里,运行时通过 API 调用签名操作,不碰原始私钥材料。
4.5 小结
数字签名不是什么黑魔法。记住三句话:私钥签,公钥验,哈希做中介。RSA 和 ECDSA 各有千秋,别死磕一种。在我的经验里,新系统我倾向于 ECDSA,密钥短、速度快、握手开销小;但如果是给银行或政府对接,他们可能要求 RSA 2048+,那就得跟着标准走。总之,签名是实现 API 显式加密不可或缺的一环,后面几章我们会把它和加密方案结合起来,形成完整的防护链路。