SM2 签名生成算法详细流程归纳总结

SM2 签名算法,说白了就是国标里那套椭圆曲线签名方案。我当年第一次接触时,以为跟 ECDSA 差不多,结果踩了不少坑——最疼的一次是 r、s 全零导致签名失败,查了一整天才发现是随机数 k 没处理好。今天我把整个流程拆开揉碎讲清楚,你跟着走一遍就不会再踩同样的雷。

签名需要哪些东西

签名不是凭空生成,你得先准备好三样输入:

  • 签名者私钥 dA:一个 256 位的大整数,谁都不能告诉。
  • 原始消息 M:要签名的数据,长度不限。
  • 系统参数:椭圆曲线基 G、阶 n、哈希函数 SM3 等。

输出就是 signature = (r, s),两个 256 位整数。

我的习惯:私钥 dA 最好从硬件安全模块(HSM)拿,别直接写在代码里。我之前见过有人把私钥硬编码在 APK 里,结果被反编译……嗯,后果你自己想。

签名生成六步走

下面这张表把这六步说清楚了。每步我都会补一句实际写代码时的坑。

步骤 操作 公式 / 说明 常见失败原因
1 预处理消息 M ZA = H256(ENTLA || IDA || a || b || xG || yG || xA || yA)
M' = ZA || M
IDA 缺省值用错(标准建议 "1234567812345678")
2 计算哈希 e e = SM3(M') 哈希长度不足或溢出不检查
3 生成随机数 k k ∈ [1, n-1] k 为 0 或弱随机数导致 r=0
4 计算椭圆曲线点 (x1, y1) (x1, y1) = k · G 点乘实现错误导致坐标越界
5 计算 r r = (e + x1) mod n r = 0 或 r + k = n → 必须重试
6 计算 s s = ((1 + dA)-1 · (k - r · dA)) mod n s = 0 → 必须重试;模逆实现出错
关键检查:第 5、6 步做完后,必须检查 r ≠ 0 且 s ≠ 0,否则从头再来。我曾经写过一个版本忘了检查 s==0,结果线上时不时签名失败,调了好久才发现。

核心运算拆解

这六步里最有技术含量的就三个:

1. 椭圆曲线点乘 k·G

这不是普通的整数乘法,是把私标点 G 自加 k 次。我用 Montgomery 阶梯算法实现,防止侧信道攻击。你如果追求速度,可以用预计算表,但注意表要抗内存泄露。

2. SM3 哈希

国标哈希,输出 256 位。预处理阶段算 ZA 时要注意用户身份 ID 的长度编码(ENTLA),很多开源库把这块写错了。

3. 模逆运算 (1+dA)-1 mod n

我这人爱用扩展欧几里得算法。费马小定理也能做,但 n 不是素数?n 是素数,没问题,不过扩展欧几里得更快。要注意入参 (1+dA) 可能等于 0,虽然概率极低(dA = n-1),但判断语句不能省。

踩坑实录:我团队有个新人用 Python 的 pow() 算模逆,没注意 pow 的第三个参数必须是正整数。n 本身是 256 位大数,没问题,但 pow 在负数指数时会抛异常。后来统一封装了一个 safe_mod_inv 函数才解决。

签名生成全景图

下面这张图把流程画清楚了。我建议你把这张图打印出来贴在工位上,写代码时对照着来,每步检查一次,保证百分之百命中。

SM2 签名生成流程 SM2 签名生成流程图 输入: M, dA, 参数 1. 计算 ZA, M' = ZA||M 2. e = SM3(M') 3. 随机数 k ∈ [1, n-1] 4. (x1,y1) = k·G 5. r = (e + x1) mod n r=0? 重试 6. 计算 s s=0? 重试 输出 (r,s)

现场避坑指南

最后我把自己踩过的坑列了个清单,你写代码时照着查一下:

  • 随机数 k 必须一次性生成且不泄漏。我曾经用一个静态种子生成随机数,结果每次签名 k 都一样——这等价于私钥直接泄露,安全团队差点把我开掉。
  • 模逆运算的异常处理。如果输入是 0 或 n 的倍数,要抛异常重试。别用 try-except 静默吞掉,那样会输出 (0,0) 签名,所有验签都会失败。
  • 坐标 x1 必须规约到 [0, n-1]。有些点乘实现返回的 x1 可能大于 n,如果用未规约的值算 r,r 就可能不在有效范围内。
  • 签名输出格式。SM2 标准规定 (r, s) 各占 32 字节,大端序拼接输出。很多联调问题都是字节序没对齐导致的。
小技巧:我在单元测试时故意给一个坏随机数(比如 k=1),然后手动算一遍 r、s 跟程序比对。只要这个用例能过,大部分实现问题都能暴露出来。