4. SM2 签名验签机制:数字签名生成流程、验签流程详解、签名数据格式标准
数字签名,说白了就是给一段固件打个「防伪标记」。你想想看,固件从开发环境烧录到芯片上,中间要经过多少人的手?谁敢保证没人动过手脚?SM2 签名验签机制,就是用来解决这个信任问题的。
我最早接触 SM2 签名是在一个电力行业的项目里。当时客户要求所有终端固件升级必须带数字签名,否则不予执行。嗯,从那以后我才真正把签名流程吃透。今天咱们就把这块掰开揉碎了讲清楚。
4.1 数字签名生成流程
SM2 签名算法基于椭圆曲线密码学。它的核心思路是:用私钥对消息的哈希值做运算,生成一对大整数 (r, s)。这对整数就是签名。
生成签名的步骤其实不复杂,但每一步都有坑。我习惯把它拆成 5 个阶段:
- 预处理:把待签名的原始消息 M 和用户身份标识 ZA 拼接,计算哈希值 e = SM3(ZA || M)。
- 生成随机数:产生一个随机数 k,范围在 [1, n-1] 之间。n 是椭圆曲线的阶。
- 计算椭圆曲线点:计算 k * G = (x1, y1),G 是基点。
- 计算 r:r = (e + x1) mod n。如果 r = 0 或 r + k = n,回到第 2 步重新生成 k。
- 计算 s:s = ((1 + dA)-1 * (k - r * dA)) mod n。如果 s = 0,回到第 2 步。
最终签名就是 (r, s)。
关键点:随机数 k 绝对不能重复使用,也不能被预测。我在项目中见过一次事故——某厂商的随机数生成器种子固定,导致两个不同固件版本产生了相同的 k。攻击者直接算出了私钥。嗯,那场面……
下面这张图把签名流程串起来了:
4.2 验签流程详解
验签是签名的逆过程。验证方持有公钥 PA,收到消息 M 和签名 (r, s) 后,按以下步骤检查:
- 格式检查:确认 r 和 s 都在 [1, n-1] 范围内。不在范围内直接拒绝。
- 计算哈希:同样计算 e = SM3(ZA || M)。
- 计算 t:t = (r + s) mod n。如果 t = 0,验签失败。
- 计算椭圆曲线点:计算 s * G + t * PA = (x1, y1)。
- 验证 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。
最后说一句:签名格式的选择,直接影响你的固件升级工具的兼容性。选之前,先想清楚你的上下游都支持什么格式。