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 函数才解决。
签名生成全景图
下面这张图把流程画清楚了。我建议你把这张图打印出来贴在工位上,写代码时对照着来,每步检查一次,保证百分之百命中。
现场避坑指南
最后我把自己踩过的坑列了个清单,你写代码时照着查一下:
- 随机数 k 必须一次性生成且不泄漏。我曾经用一个静态种子生成随机数,结果每次签名 k 都一样——这等价于私钥直接泄露,安全团队差点把我开掉。
- 模逆运算的异常处理。如果输入是 0 或 n 的倍数,要抛异常重试。别用 try-except 静默吞掉,那样会输出 (0,0) 签名,所有验签都会失败。
- 坐标 x1 必须规约到 [0, n-1]。有些点乘实现返回的 x1 可能大于 n,如果用未规约的值算 r,r 就可能不在有效范围内。
- 签名输出格式。SM2 标准规定 (r, s) 各占 32 字节,大端序拼接输出。很多联调问题都是字节序没对齐导致的。
小技巧:我在单元测试时故意给一个坏随机数(比如 k=1),然后手动算一遍 r、s 跟程序比对。只要这个用例能过,大部分实现问题都能暴露出来。