4. SM2数字签名算法:签名生成与验证

数字签名,说白了就是给电子文档盖个章。这个章只有你能盖,但全世界都能验。SM2的签名算法基于椭圆曲线离散对数难题,比RSA签名更紧凑,安全性也更高。

我个人习惯把SM2签名拆成三块来看:密钥生成、签名生成、签名验证。前一块是基础,后两块是核心。咱们今天重点啃后两块。

4.1 签名生成流程

签名生成,就是签名者用自己的私钥对消息产生一个签名值。这个签名值由两个大整数组成,记作(r, s)。

先看流程图,再讲细节。

SM2 签名生成流程 输入:消息 M、私钥 dA、公钥 PA ① 计算 e = Hv(ZA || M) ② 生成随机数 k ∈ [1, n-1] ③ 计算椭圆曲线点 (x1, y1) = kG ④ 计算 r = (e + x1) mod n ⑤ 计算 s = (1+dA)⁻¹·(k - r·dA) mod n 输出:签名值 (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 来验。

SM2 签名验证流程 输入:消息 M、签名 (r, s)、公钥 PA ① 检查 r, s ∈ [1, n-1] ② 计算 e = Hv(ZA || M) ③ 计算 t = (r + s) mod n ④ 计算 (x1, y1) = sG + tPA ⑤ 验证 R = (e + x1) mod n 是否等于 r

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 的验证效率更高,这在服务端高频验签场景下优势明显。你想想看,一个网关每秒要验几万次签名,少一次模逆运算,性能差距就出来了。