4. SM2数字签名算法:签名生成与验证
数字签名,说白了就是给电子文档盖个章。这个章只有你能盖,但全世界都能验。SM2的签名算法基于椭圆曲线离散对数难题,比RSA签名更紧凑,安全性也更高。
我个人习惯把SM2签名拆成三块来看:密钥生成、签名生成、签名验证。前一块是基础,后两块是核心。咱们今天重点啃后两块。
4.1 签名生成流程
签名生成,就是签名者用自己的私钥对消息产生一个签名值。这个签名值由两个大整数组成,记作(r, s)。
先看流程图,再讲细节。
4.2 关键步骤详解
第一步:计算消息摘要 e
这里有个坑,我刚开始学的时候踩过。e 不是直接对消息 M 做哈希,而是先拼接 ZA 再哈希。ZA 包含了签名者的身份标识和公钥信息。为什么要这么干?说白了就是为了防止签名被挪用到其他身份上。
ZA 的计算方式:
ZA = Hv(ENTLA || IDA || a || b || xG || yG || xA || yA)
其中 ENTLA 是身份标识 IDA 的长度编码,a、b 是椭圆曲线参数,xG、yG 是基点坐标,xA、yA 是公钥坐标。
第二步:生成随机数 k
随机数 k 的质量直接决定签名安全性。我曾经在一个项目中看到有人用伪随机数生成器,结果 k 值可预测,攻击者直接恢复了私钥。嗯,这里必须用密码学安全的随机数生成器,比如 /dev/urandom 或者硬件随机数。
第三步:计算 kG
这是椭圆曲线上的标量乘法,也是整个签名中最耗时的操作。kG 的结果是一个椭圆曲线点 (x1, y1),我们只取 x1 参与后续计算。
第四步:计算 r
r = (e + x1) mod n。如果 r = 0 或者 r + k = n,需要重新生成 k。为什么?因为 r=0 时签名退化为只依赖私钥,而 r+k=n 会泄露私钥信息。这些边界情况,标准里写得明明白白,但实现时很容易漏掉。
第五步:计算 s
s = (1 + dA)⁻¹ · (k - r · dA) mod n。这个公式和 ECDSA 不一样,ECDSA 用的是 k⁻¹,SM2 用的是 (1+dA)⁻¹。设计上的差异导致 SM2 的签名验证不需要求逆运算,效率更高。
⚠ 签名失败重试条件:
- 如果 r = 0 或 r + k = n,重新生成 k
- 如果 s = 0,重新生成 k
- 重试次数没有上限,但理论上概率极低(约 2⁻²⁵⁶)
4.3 签名验证流程
验证流程比生成简单,因为不需要随机数。验证者拿到消息 M 和签名 (r, s),用签名者的公钥 PA 来验。
4.4 验证原理剖析
为什么验证公式能成立?我们来推一下。
签名时:s = (1+dA)⁻¹ · (k - r·dA) mod n
两边乘以 (1+dA):s · (1+dA) = k - r·dA mod n
整理:k = s + s·dA + r·dA = s + (r+s)·dA mod n
令 t = (r+s) mod n,则 k = s + t·dA mod n
两边乘以基点 G:kG = sG + t·dA·G = sG + t·PA
左边是签名时的点 (x1, y1),右边是验证时计算的点。所以验证就是检查 (e + x1) mod n 是否等于 r。
💡 性能优化小技巧:
验证步骤④中 sG + tPA 是两个标量乘法的和。可以用 Strauss 算法或 Shamir 技巧一次算完,比分别算 sG 和 tPA 再相加快 30% 左右。我在做国密网关时用这个优化,签名验证吞吐量提升了近一倍。
4.5 签名与验证的对比
| 对比项 | 签名生成 | 签名验证 |
|---|---|---|
| 输入 | 私钥 dA、消息 M | 公钥 PA、消息 M、签名 (r, s) |
| 随机数 | 需要,每次签名不同 | 不需要 |
| 核心运算 | 一次标量乘法 kG | 两次标量乘法 sG + tPA |
| 模逆运算 | 需要 (1+dA)⁻¹ | 不需要 |
| 重试机制 | r=0 或 s=0 时重试 | 无重试 |
| 输出 | 签名 (r, s) | 通过/不通过 |
4.6 常见坑与避坑指南
坑一:随机数重用
我曾经在一个开源项目中看到,有人为了性能缓存了随机数 k。结果两个不同消息用了同一个 k,攻击者直接算出了私钥。这个坑在 ECDSA 里也有,但 SM2 同样不能幸免。每次签名必须生成全新的随机数。
坑二:哈希计算不一致
签名和验证时 ZA 的计算必须完全一致。我见过一个案例,签名时用了 SM3,验证时用了 SHA-256,结果验签永远失败。还有 ZA 中身份标识的长度编码,大端小端搞反也会出问题。
坑三:边界条件遗漏
r=0、s=0、r+k=n 这些边界条件,标准里写了,但很多实现只检查了 r=0 和 s=0。漏掉 r+k=n 的检查,虽然概率极低,但一旦触发就是安全漏洞。
✅ 安全实现检查清单:
- 随机数 k 必须使用密码学安全随机数生成器
- 每次签名生成新的 k,禁止缓存或复用
- 签名前检查 r、s 是否在有效范围内
- 验证前检查 r、s 是否在 [1, n-1] 内
- ZA 的计算在签名和验证两端必须完全一致
- 使用恒定时间比较验证最终结果,防止时序攻击
SM2 签名算法整体设计得很精巧。生成时用一次标量乘,验证时用两次标量乘,但验证不需要模逆。相比 ECDSA,SM2 的验证效率更高,这在服务端高频验签场景下优势明显。你想想看,一个网关每秒要验几万次签名,少一次模逆运算,性能差距就出来了。