4. SM2 签名验证算法详细流程归纳总结
签名验证这一块,说白了就是检查一笔签名到底是不是由声称的私钥持有者生成的。我刚开始接触国密时,总觉得验证比签名简单——不就是算两个数嘛?后来踩了几次坑才发现,细节多得要命。今天我把验证算法从头到尾捋一遍,顺便把我个人习惯的检查清单也抖出来。
4.1 验证算法的输入与输出
先搞清楚手上有哪些东西。验证方拿到的数据包括:
- 消息 M:原始消息,通常是字节串
- 签名 (r, s):两个大整数
- 公钥 P:椭圆曲线上的一个点,包含 x 和 y 坐标
- 系统参数:椭圆曲线参数 a, b, p, n, G 等
输出就一个布尔值:签名有效(True)或无效(False)。我个人习惯在代码里用 0/1 而不是 bool,方便后续统计失败率。
| 输入项 | 类型 | 说明(我踩过的坑) |
|---|---|---|
| 消息 M | 字节串 | 长度不限,但注意摘要生成时需过一遍 SM3 |
| 签名 (r, s) | 大整数 (256 bit) | 我曾经犯过 r 和 s 传反的错误,导致一批签名全判无效 |
| 公钥 P | 椭圆曲线点 | 必须验证 P 在曲线上,否则后面全是白算 |
| 系统参数 | 结构体 | 建议写个全局常量,别每次都解析一遍 |
4.2 验证步骤分解
SM2 签名验证其实就六步。我当初为了排查随机失败率,把每步都拆开做日志,才发现问题出在第三步——点加运算溢出。咱们一步步看:
- 检查 r 和 s 范围
r ∈ [1, n-1]且s ∈ [1, n-1]。n 是基点 G 的阶。超过范围直接返回 False。这里没啥技巧,但必须做。 - 计算消息摘要 e
把消息 M 与用户标识、曲线参数拼起来,做一次 SM3 哈希。很多人直接对 M 做哈希,那是错的!标准要求先算 Z 值再拼 M。 - 计算 t = (r + s) mod n
注意,如果 t == 0,直接返回 False。为什么?因为后面要用到 t 的逆元,0 没有逆。我曾经被这个坑过——一次线上事故,某个签名恰好让 r+s 等于 n,结果判断为无效。排查了一整天。 - 计算椭圆曲线点 (x1, y1) = s * G + t * P
这是最核心的一步。两个标量乘加运算,注意要用到的加法公式是完整的仿射坐标或雅可比坐标。我在项目中用蒙哥马利梯子实现标量乘,效率还行。 - 计算 R = (e + x1) mod n
这里 x1 是曲线点横坐标,取整后加上 e 再模 n。 - 判断 R == r
相等则验证通过,否则失败。
4.3 核心公式的理解
你可能会问:为什么要搞这么复杂的计算?说白了,验证的本质是恢复签名过程中的临时公钥。签名时用了随机数 k,验证时通过 (r,s) 和公钥就能把 k*G 算回来,再比较横坐标。
公式 s * G + t * P 中:
s * G是签名者用的临时公钥的一部分t * P是用公钥 P 做了一个线性组合- 两者相加得到完整的临时点
如果签名未被篡改,这个点就是签名时用的那个点。嗯,这里要注意模运算的顺序——先乘法再加法,不能调换。
4.4 常见验证失败原因
我这两年处理过至少几十次签名验证失败案例,把常见原因列成表格,你写代码时直接对着排查:
| 失败表现 | 大概率原因 | 我的亲身经历 |
|---|---|---|
| r 或 s 超出范围 | 签名时用的随机数 k 不符合要求,或传输过程中字节序错乱 | 有一次接收方解析签名时用了大端,发送方用小端,结果一半签名被判越界 |
| t == 0 | 签名本身计算错误,或者公钥不对导致 r 与 s 关系异常 | 我记得那次是公钥坐标系搞反了,换成未压缩点就好了 |
| 中间点 x1 与 e 相加后不为 r | 公钥不对、消息被篡改、或者摘要计算时 Z 值拼写错误 | 排查了一周,最后发现是用户标识 ID 的字节顺序反了 |
| 椭圆曲线点运算抛出异常 | 输入点不在曲线上,或者用错了坐标变换公式 | 我建议验证公钥时先做一次 点是否在曲线上 的检查 |
| 验证失败率突增(非一致性的) | 随机数生成器有熵源问题,导致部分签名 r 值重复 | 那个项目最后发现是硬件随机数芯片坏了,换后恢复 |
写代码时,建议把验证流程封装成一个独立函数,内部每个步骤都加断言或日志。对于高性能场景,可以用查表法加速标量乘,但初期调试阶段先跑通标准向量再说。
4.5 验证流程图
为了让你直观感受整个验证路径,我画了张 SVG 流程图。箭头方向就是数据流动方向。红色节点是检查点,绿色节点是运算节点。你调试时对着这张图,一步步核实中间值,基本不会漏。
你看,整个流程就是一条直线,中间没有任何分支(除了 t=0 的快速返回)。所以验证失败往往是在某一步算错了。我个人的习惯是把这个流程图贴在调试白板上,脑内逐行走一遍代码。
4.6 验证与签名的关系
很容易忽略一点:验证算法和签名算法使用的是完全相同的椭圆曲线运算。你想想看,签名时需要计算 k*G,验证时需要计算 s*G + t*P,两者本质都是标量乘加。所以如果签名实现有问题,验证大概率也会出同样的 bug。
我建议你单元测试时成对测试:先签名,再验证同一个消息,确保自洽。然后再测试篡改消息后验证失败。只有两种情形都覆盖,才算验证函数写对了。
- 公钥是否在曲线上?
- 签名中的 r, s 是否在合理范围内?
- 摘要 e 是否包含了 Z 值和用户标识?
- t 是否不等于 0?
- 点加运算是否使用了正确的坐标系统?
- 模运算是否使用了 n 而不是 p?