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 签名验证其实就六步。我当初为了排查随机失败率,把每步都拆开做日志,才发现问题出在第三步——点加运算溢出。咱们一步步看:

  1. 检查 r 和 s 范围
    r ∈ [1, n-1] 且 s ∈ [1, n-1]。n 是基点 G 的阶。超过范围直接返回 False。这里没啥技巧,但必须做。
  2. 计算消息摘要 e
    把消息 M 与用户标识、曲线参数拼起来,做一次 SM3 哈希。很多人直接对 M 做哈希,那是错的!标准要求先算 Z 值再拼 M。
  3. 计算 t = (r + s) mod n
    注意,如果 t == 0,直接返回 False。为什么?因为后面要用到 t 的逆元,0 没有逆。我曾经被这个坑过——一次线上事故,某个签名恰好让 r+s 等于 n,结果判断为无效。排查了一整天。
  4. 计算椭圆曲线点 (x1, y1) = s * G + t * P
    这是最核心的一步。两个标量乘加运算,注意要用到的加法公式是完整的仿射坐标或雅可比坐标。我在项目中用蒙哥马利梯子实现标量乘,效率还行。
  5. 计算 R = (e + x1) mod n
    这里 x1 是曲线点横坐标,取整后加上 e 再模 n。
  6. 判断 R == r
    相等则验证通过,否则失败。
我的调试小技巧: 在每个步骤后把中间变量打印出来,跟标准测试向量的值对比。我常用国密局给的例子,一步对一步。

4.3 核心公式的理解

你可能会问:为什么要搞这么复杂的计算?说白了,验证的本质是恢复签名过程中的临时公钥。签名时用了随机数 k,验证时通过 (r,s) 和公钥就能把 k*G 算回来,再比较横坐标。

公式 s * G + t * P 中:

  • s * G 是签名者用的临时公钥的一部分
  • t * P 是用公钥 P 做了一个线性组合
  • 两者相加得到完整的临时点

如果签名未被篡改,这个点就是签名时用的那个点。嗯,这里要注意模运算的顺序——先乘法再加法,不能调换。

一句话总结验证逻辑: 验证者不需要知道私钥 d,只需要公钥 P 和签名 (r,s),就能通过椭圆曲线性质确认签名是由持有对应私钥的人生成的。

4.4 常见验证失败原因

我这两年处理过至少几十次签名验证失败案例,把常见原因列成表格,你写代码时直接对着排查:

失败表现 大概率原因 我的亲身经历
r 或 s 超出范围 签名时用的随机数 k 不符合要求,或传输过程中字节序错乱 有一次接收方解析签名时用了大端,发送方用小端,结果一半签名被判越界
t == 0 签名本身计算错误,或者公钥不对导致 r 与 s 关系异常 我记得那次是公钥坐标系搞反了,换成未压缩点就好了
中间点 x1 与 e 相加后不为 r 公钥不对、消息被篡改、或者摘要计算时 Z 值拼写错误 排查了一周,最后发现是用户标识 ID 的字节顺序反了
椭圆曲线点运算抛出异常 输入点不在曲线上,或者用错了坐标变换公式 我建议验证公钥时先做一次 点是否在曲线上 的检查
验证失败率突增(非一致性的) 随机数生成器有熵源问题,导致部分签名 r 值重复 那个项目最后发现是硬件随机数芯片坏了,换后恢复
避坑指南: 我曾经遇到过公钥字节数组和坐标转换时,未按 SM2 标准先压缩后解压。如果公钥是压缩格式,必须用标准解压算法得到完整点,不能跳过。否则验证几乎百分之百失败。

写代码时,建议把验证流程封装成一个独立函数,内部每个步骤都加断言或日志。对于高性能场景,可以用查表法加速标量乘,但初期调试阶段先跑通标准向量再说。

4.5 验证流程图

为了让你直观感受整个验证路径,我画了张 SVG 流程图。箭头方向就是数据流动方向。红色节点是检查点,绿色节点是运算节点。你调试时对着这张图,一步步核实中间值,基本不会漏。

SM2 签名验证流程 签名 (r,s) + 公钥 P + 消息 M 检查 r,s 是否在 [1, n-1] 计算摘要 e = SM3(Z || M) 计算 t = (r + s) mod n 若 t=0, 直接返回 False 计算 (x1, y1) = s*G + t*P 计算 R = (e + x1) mod n 判断 R == r ? 是 → 验证通过 否 → 验证失败 注意:每一步的中间值 都应记录日志用于调试

你看,整个流程就是一条直线,中间没有任何分支(除了 t=0 的快速返回)。所以验证失败往往是在某一步算错了。我个人的习惯是把这个流程图贴在调试白板上,脑内逐行走一遍代码。

4.6 验证与签名的关系

很容易忽略一点:验证算法和签名算法使用的是完全相同的椭圆曲线运算。你想想看,签名时需要计算 k*G,验证时需要计算 s*G + t*P,两者本质都是标量乘加。所以如果签名实现有问题,验证大概率也会出同样的 bug。

我建议你单元测试时成对测试:先签名,再验证同一个消息,确保自洽。然后再测试篡改消息后验证失败。只有两种情形都覆盖,才算验证函数写对了。

快速自查清单:
  • 公钥是否在曲线上?
  • 签名中的 r, s 是否在合理范围内?
  • 摘要 e 是否包含了 Z 值和用户标识?
  • t 是否不等于 0?
  • 点加运算是否使用了正确的坐标系统?
  • 模运算是否使用了 n 而不是 p?