4. 数字签名原理:签名与验签流程、RSA与ECDSA对比

数字签名,说白了就是给数据盖个不可伪造的“电子印章”。你想想看,在 REST API 的显式加密改造里,我们不仅要防数据被偷看,还要防别人篡改,更要证明这条数据确实是你发的。签名机制就是干这三件事的——完整性、身份认证、不可否认性。

我在好几个项目里都遇到过“只加密不签名”的坑。你以为加密了就安全了?攻击者虽然解不开密文,但可以截获后把密文原封不动转发,接收方解密出来发现是合法的,还以为是你发的,这就是典型的重放攻击变种。所以,加密 + 签名必须同时上。

4.1 签名与验签流程

数字签名用非对称密码学的逆操作。私钥签名,公钥验签。流程可以拆成三步:

发送方(签名)

  1. 计算消息摘要:对消息做哈希运算,比如 SHA-256,得到固定长度的哈希值。
  2. 用私钥加密哈希:把哈希值拿自己的私钥做“签名运算”(RSA 就是加密,ECDSA 是一套椭圆曲线算法生成两个数)。这一步输出的就是签名值。
  3. 发送消息 + 签名:把原始消息和签名一起发给接收方。注意,消息本身可以是明文,也可以再加密。

接收方(验签)

  1. 分离消息和签名:从报文中取出原始消息和附带的签名。
  2. 计算消息摘要:对收到的消息做同样的哈希运算。
  3. 用公钥验证签名:用发送方的公钥对签名做验签运算,得到一个哈希值(或恢复出的摘要),与第二步计算的哈希值比对。一致则通过,否则拒绝。
关键点:签名不是直接对消息签名,而是对消息的哈希值签名。这样做有两个原因:一是哈希可以把任意长消息变成固定长度,效率高;二是私钥签名运算很慢,对短哈希签名比对整个大消息快得多。
我个人习惯在 REST API 响应头里加 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 核心流程一览

下面这张图把签名与验签的总体流程串起来了。我建议你仔细看看箭头和数据流向,理解每一个环节为什么不可或缺。

数字签名流程概览 发送方 (私钥签名) 原始消息 M Hash 摘要 H(M) 私钥 Sk 签名运算 签名 S 传输 接收方 (公钥验签) 接收 M' 接收 S' Hash 摘要 H(M') 公钥 Pk 验签运算 比对 H(M') 与解签摘要 通过 / 拒绝

从图里可以看到,发送方和接收方各做一次哈希,验签的本质就是把两个哈希值比对。私钥只在签名方持有,公钥公开出去。只要私钥不泄露,任何人无法伪造你对消息的签名。

实用小技巧:在实际 API 网关里,我习惯在签名值中带上时间戳和随机 Nonce,防止重放。即使签名完整,攻击者拿到一条合法签名报文重复发送,也能被时间窗口卡住。你想想看,如果只验证签名,不做时效性检查,那签名就成了“万能通行证”。

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 显式加密不可或缺的一环,后面几章我们会把它和加密方案结合起来,形成完整的防护链路。