4. SM2 签名验签机制:数字签名生成流程、验签流程详解、签名数据格式标准

数字签名,说白了就是给一段固件打个「防伪标记」。你想想看,固件从开发环境烧录到芯片上,中间要经过多少人的手?谁敢保证没人动过手脚?SM2 签名验签机制,就是用来解决这个信任问题的。

我最早接触 SM2 签名是在一个电力行业的项目里。当时客户要求所有终端固件升级必须带数字签名,否则不予执行。嗯,从那以后我才真正把签名流程吃透。今天咱们就把这块掰开揉碎了讲清楚。

4.1 数字签名生成流程

SM2 签名算法基于椭圆曲线密码学。它的核心思路是:用私钥对消息的哈希值做运算,生成一对大整数 (r, s)。这对整数就是签名。

生成签名的步骤其实不复杂,但每一步都有坑。我习惯把它拆成 5 个阶段:

  1. 预处理:把待签名的原始消息 M 和用户身份标识 ZA 拼接,计算哈希值 e = SM3(ZA || M)。
  2. 生成随机数:产生一个随机数 k,范围在 [1, n-1] 之间。n 是椭圆曲线的阶。
  3. 计算椭圆曲线点:计算 k * G = (x1, y1),G 是基点。
  4. 计算 r:r = (e + x1) mod n。如果 r = 0 或 r + k = n,回到第 2 步重新生成 k。
  5. 计算 s:s = ((1 + dA)-1 * (k - r * dA)) mod n。如果 s = 0,回到第 2 步。

最终签名就是 (r, s)。

关键点:随机数 k 绝对不能重复使用,也不能被预测。我在项目中见过一次事故——某厂商的随机数生成器种子固定,导致两个不同固件版本产生了相同的 k。攻击者直接算出了私钥。嗯,那场面……

下面这张图把签名流程串起来了:

SM2 数字签名生成流程 原始消息 M 用户标识 ZA SM3 哈希运算 哈希值 e 生成随机数 k k * G = (x₁, y₁) r = (e + x₁) mod n s = (1+dA)⁻¹(k - r·dA) 签名 (r, s) 条件检查 r = 0 或 r + k = n? s = 0? 任一成立 → 重新生成 k

4.2 验签流程详解

验签是签名的逆过程。验证方持有公钥 PA,收到消息 M 和签名 (r, s) 后,按以下步骤检查:

  1. 格式检查:确认 r 和 s 都在 [1, n-1] 范围内。不在范围内直接拒绝。
  2. 计算哈希:同样计算 e = SM3(ZA || M)。
  3. 计算 t:t = (r + s) mod n。如果 t = 0,验签失败。
  4. 计算椭圆曲线点:计算 s * G + t * PA = (x1, y1)。
  5. 验证 R:计算 R = (e + x1) mod n。如果 R = r,验签通过;否则失败。

我的经验:验签时最容易忽略的是「格式检查」这一步。我曾经接手过一个项目,验签代码里没判断 r 和 s 的范围,结果攻击者传了个 r=0 的签名,代码直接崩溃了。嗯,从那以后我写验签代码,第一行永远是范围检查。

验签流程的数学原理其实很巧妙。你想想看:

s * G + t * PA = s * G + (r + s) * dA * G = (s + r * dA + s * dA) * G

把 s 的表达式代入,最终会得到 k * G。所以只要签名没被篡改,算出来的 x1 一定和签名生成时的一致。

4.3 签名数据格式标准

SM2 签名的数据格式,国家标准里写得明明白白。我直接给你看最常用的 DER 编码格式:

SEQUENCE {
  INTEGER r,  -- 签名值 r,大端无符号整数
  INTEGER s   -- 签名值 s,大端无符号整数
}

具体到字节层面,DER 编码规则如下:

字段 长度(字节) 说明
SEQUENCE 标签 1 0x30
SEQUENCE 长度 1 或 2 后续数据总长度
INTEGER 标签 (r) 1 0x02
r 的长度 1 通常为 32 或 33(含前导 0x00)
r 的值 32 或 33 大端表示,最高位为 1 时加前导 0x00
INTEGER 标签 (s) 1 0x02
s 的长度 1 通常为 32 或 33
s 的值 32 或 33 大端表示,最高位为 1 时加前导 0x00

注意:DER 编码中,如果 INTEGER 的最高位字节 ≥ 0x80,必须在前面前导一个 0x00 字节。否则解码器会把它当成负数。我见过好几个团队因为这个细节导致签名验签不通过,排查了一整天。

除了 DER 编码,实际项目中还常见两种格式:

  • 原始格式:直接拼接 r || s,各 32 字节,共 64 字节。简单粗暴,但缺少自描述能力。
  • ASN.1 封装:在 DER 基础上再加一层 SEQUENCE,包含算法标识和签名值。多用于证书场景。

我个人习惯在固件保护方案里用原始格式。为什么?因为嵌入式环境资源有限,DER 解析需要额外代码量。但如果你要跟标准接口对接,比如 PKCS#11 或 GM/T 0016,那就必须用 DER。

最后说一句:签名格式的选择,直接影响你的固件升级工具的兼容性。选之前,先想清楚你的上下游都支持什么格式。