3. SM2 数字签名算法:签名与验签全流程

数字签名,说白了就是给一段数据盖个「电子印章」。SM2 的签名算法基于椭圆曲线,跟 RSA 那种大数分解的思路完全不同。我个人习惯把签名流程拆成三步:预处理、核心计算、输出。这样调优的时候,哪一步慢了,一眼就能看出来。

核心要点:SM2 签名是「先哈希,再签名」的机制。消息本身不直接参与椭圆曲线运算,而是先算出一个摘要,再对这个摘要签名。这样做既安全,又高效。

3.1 签名流程详解

SM2 签名算法定义在 GM/T 0003.2-2012 标准里。我刚开始接触时,觉得公式挺绕的。后来自己手推了一遍,发现核心就几个步骤。

3.1.1 预处理阶段

签名前,需要准备好三样东西:

  • 消息 M:待签名的原始数据
  • 私钥 dA:签名者的私钥(一个大整数)
  • 椭圆曲线参数:包括基点 G、阶 n、曲线方程等

嗯,这里要注意:SM2 的签名过程会用到 ZA 这个值。ZA 是签名者身份标识、椭圆曲线参数等信息的哈希值。标准里要求把 ZA 和消息 M 拼在一起再哈希。我在项目中遇到过有人忘了拼 ZA,结果验签死活过不了——排查了半天才发现是预处理没做对。

3.1.2 签名计算流程

签名流程可以用下面的 SVG 图来理解。我习惯把流程画出来,这样调优时能快速定位瓶颈。

SM2 数字签名流程 1. 计算 ZA = H(ENTLA || IDA || a || b || xG || yG || xA || yA) 2. e = H(ZA || M) 3. 生成随机数 k 4. (x1, y1) = kG 5. r = (e + x1) mod n 6. s = ((1 + dA)-1 · (k - r · dA)) mod n 输出签名 (r, s) 条件检查 r = 0 或 r + k = n? s = 0? 任一成立 → 重新生成 k 通过 性能关键点 ① 随机数生成(步骤3) ② 点乘运算 kG(步骤4) ③ 模逆运算(步骤6)

你想想看,整个流程里最耗时的操作是什么?没错,就是 步骤4 的点乘运算 kG。这个操作占了签名总时间的 70% 以上。我在项目中做过实测,一次 SM2 签名大约需要 2-5 毫秒(取决于硬件),其中点乘就占了 3 毫秒左右。

3.1.3 签名输出

签名结果是两个大整数 (r, s),每个 256 位。标准规定 r 和 s 都不能为 0,也不能让 r + k = n。如果出现这些情况,就得重新生成随机数 k 再来一遍。

我的经验:随机数生成器是签名性能的隐形瓶颈。我曾经用过一个软件实现的随机数生成器,每次生成 k 要花 0.5 毫秒。后来换成硬件 TRNG,直接降到 10 微秒。签名总耗时从 5 毫秒降到了 3.5 毫秒。

3.2 验签流程详解

验签是签名的逆过程,但计算量其实差不多。验签不需要私钥,只需要签名者的公钥 PA。

3.2.1 验签流程

验签的核心思路是:用公钥和签名 (r, s) 重新计算出一个值,跟 r 比较。如果相等,签名就有效。

SM2 验签流程 1. 检查 r, s ∈ [1, n-1] 2. e = H(ZA || M) 3. t = (r + s) mod n 4. (x1, y1) = sG + tPA 5. R = (e + x1) mod n R == r?通过 / 失败 性能关键点 ① 双点乘运算 sG + tPA ② 比签名多一次点乘 ③ 通常比签名慢 30%-50%

验签的步骤4是 sG + tPA,这是一个双点乘运算。签名只需要一次点乘 kG,验签却要算两个点乘的和。所以验签通常比签名慢 30%-50%。

避坑指南:我曾经在验签时踩过一个坑——公钥 PA 没有做有效性检查。SM2 标准要求验签前必须检查公钥是否在椭圆曲线上,否则可能被伪造签名攻击。后来我在代码里加了一个 ec_point_is_on_curve() 检查,虽然多花了 0.1 毫秒,但安全性提升了一个档次。

3.3 签名与验签的性能瓶颈分析

好了,流程讲完了。咱们来聊聊性能瓶颈。我做了个表格,把签名和验签的各个操作拆开来看。

操作 签名耗时占比 验签耗时占比 瓶颈等级
哈希运算 (SM3) 5%-10% 5%-10% 低
随机数生成 5%-15% 0% 中
点乘运算 (kG / sG + tPA) 60%-75% 70%-85% 高
模逆运算 5%-10% 0% 中
模加/模乘运算 5%-10% 5%-10% 低
数据拷贝/内存分配 5%-10% 5%-10% 中

3.3.1 点乘运算:最大的瓶颈

点乘运算,说白了就是算 kG(一个整数乘以椭圆曲线上的一个点)。这个操作涉及大量的点加和倍点运算。256 位的 k,大约需要 256 次倍点和 128 次点加。

为什么点乘这么慢?因为每次点加都要做模逆运算(求倒数)。模逆运算在软件里很慢,一次大约 0.01 毫秒。256 次倍点 + 128 次点加,光模逆就占了 3-4 毫秒。

优化方向:点乘运算的优化是 SM2 性能调优的核心。常用的方法包括:

  • 使用雅可比坐标:避免模逆运算,把模逆推迟到最后一步
  • 预计算:提前算好 2G、4G、8G 等,减少在线计算量
  • NAF 编码:减少点加次数,从平均 128 次降到 85 次

3.3.2 随机数生成:容易被忽视的瓶颈

签名流程里,每次都要生成一个 256 位的随机数 k。如果随机数生成器质量不好,或者生成速度慢,签名性能会直接受影响。

我记得有一次,客户反馈签名速度不稳定,有时候快有时候慢。排查后发现,随机数生成器在系统熵不足时会阻塞等待。后来我们换了一个非阻塞的随机数生成器,问题就解决了。

3.3.3 模逆运算:签名特有的瓶颈

签名流程的步骤6需要计算 (1 + dA)-1,这是一个模逆运算。验签不需要模逆,所以签名在这一点上比验签多了一个瓶颈。

不过,模逆运算可以通过 费马小定理 或 扩展欧几里得算法 来优化。我个人习惯用费马小定理,因为它在硬件上更容易实现并行化。

3.3.4 数据拷贝与内存分配

这个瓶颈看起来不起眼,但在高并发场景下会放大。每次签名/验签都要分配临时缓冲区存中间结果。如果内存分配器效率低,或者频繁触发 GC(比如在 Java 里),性能会下降 20%-30%。

我的建议:在性能敏感的场景里,尽量复用缓冲区。我一般会在初始化时预分配好所有临时变量,签名/验签时直接用,不申请新内存。这个优化在 C/C++ 里效果特别明显,一次签名能省 0.2-0.5 毫秒。

3.4 签名与验签的性能对比

咱们来做个直观的对比。假设硬件环境是 ARM Cortex-A72 @ 1.5GHz,软件实现使用 OpenSSL 的 SM2 模块。

指标 签名 验签
平均耗时 3.2 ms 4.5 ms
点乘次数 1 次 (kG) 2 次 (sG + tPA)
模逆次数 1 次 0 次
随机数需求 需要 不需要
公钥检查 不需要 需要
吞吐量 (单核) ~310 次/秒 ~220 次/秒

你想想看,验签比签名慢了 40% 左右。这个差距主要来自双点乘运算。不过,验签不需要随机数,也不需要模逆,所以在某些场景下(比如随机数生成器很慢时),验签反而可能比签名快。

注意:上面的数据是软件实现的典型值。如果使用硬件加速(比如 ARM 的 SM2 指令集),点乘运算可以加速 5-10 倍。签名和验签的耗时差距也会缩小到 20% 以内。

3.5 性能瓶颈总结

说了这么多,咱们来总结一下。SM2 签名和验签的性能瓶颈,核心就三个:

  1. 点乘运算:占 60%-85% 的耗时,是调优的第一目标
  2. 随机数生成:签名特有的瓶颈,容易被忽视
  3. 模逆运算:签名特有的瓶颈,可以通过坐标变换规避

后面的章节,我会逐一讲解这些瓶颈的优化方法。嗯,咱们先从点乘运算开始——毕竟它才是性能调优的主战场。