SM2签名验签机制:数字签名生成流程、验签流程、SM2签名数据结构(DER编码)

签名验签,说白了就是给固件贴上一张「防伪标签」。你想想看,固件在产线烧录、空中升级的时候,怎么保证它没被人动过手脚?SM2签名就是干这个的。

我刚开始接触SM2时,总觉得签名就是「私钥加密、公钥解密」那么简单。后来踩了坑才发现——SM2的签名机制跟RSA完全不是一回事。它的核心是椭圆曲线上的零知识证明,而不是加密的逆过程。

数字签名生成流程

SM2签名的生成,我习惯把它拆成三步:预处理、核心计算、DER编码。每一步都有坑,咱们一个一个说。

第一步:预处理

签名不是直接对原始固件哈希动手。SM2标准要求先做一次「用户身份+曲线参数+公钥」的杂凑,生成一个叫 ZA 的值。然后把这个 ZA 拼到消息哈希前面,再做一次SM3。

为什么要多此一举?

我在项目中遇到过有人跳过这一步,直接把固件哈希拿去签名。结果呢?攻击者可以伪造一个不同身份但相同哈希的消息,签名照样能通过。嗯,这里要注意——ZA 相当于把签名者的身份「钉死」在签名里,谁也赖不掉。

预处理公式:

M' = ZA || M(M为待签名消息)

e = SM3(M')(e为签名输入)

第二步:核心签名计算

这一步是椭圆曲线运算的重头戏。我直接给代码,你跟着走一遍就明白了。

// SM2签名核心算法(伪代码)
输入:私钥 dA,消息摘要 e
输出:签名值 (r, s)

1. 生成随机数 k ∈ [1, n-1]    // n为椭圆曲线阶
2. 计算椭圆曲线点 (x1, y1) = [k]G   // G为基点
3. 计算 r = (e + x1) mod n
4. 如果 r == 0 或 r + k == n,回到步骤1
5. 计算 s = ((1 + dA)^(-1) * (k - r * dA)) mod n
6. 如果 s == 0,回到步骤1
7. 输出 (r, s)

这里有个关键点——随机数 k 绝对不能重复使用。我曾经在调试一个IoT设备时,发现签名每次都一样。查了半天,原来是随机数生成器被初始化成固定种子了。攻击者拿到两个相同 k 的签名,分分钟就能把私钥算出来。这个坑,我劝你别踩。

⚠️ 随机数 k 的生死线:

  • 每次签名必须生成新的 k
  • k 必须真随机,不能伪随机
  • k 不能泄露,哪怕泄露一次,私钥就没了
  • k 不能为0,不能为 n-1

验签流程

验签是签名的逆过程,但注意——它不是解密。验签的本质是验证「签名者确实拥有私钥,且消息没有被篡改」。

我习惯把验签流程叫做「三查一算」:查范围、查曲线、查身份、算结果。

验签步骤

  1. 查范围:检查 r 和 s 是否在 [1, n-1] 区间内。不在?直接拒掉。
  2. 查曲线:计算 t = (r + s) mod n。t == 0?拒掉。
  3. 查身份:用签名者的公钥和身份信息重新计算 ZA,再算 e = SM3(ZA || M)。
  4. 算结果:计算椭圆曲线点 (x1', y1') = [s]G + [t]PA(PA为公钥)。如果 x1' == r,验签通过。

💡 我的验签小技巧:

实际项目中,我建议在验签前先做一次「格式校验」。很多攻击者会故意传畸形签名,试图触发缓冲区溢出。先检查 DER 编码长度是否在合理范围内(SM2签名固定为 70-72 字节),能挡掉一大半低级攻击。

SM2签名数据结构(DER编码)

签名值 (r, s) 不能直接扔给固件。我们需要一种标准化的打包方式——DER编码。DER是ASN.1的一种编码规则,说白了就是「TLV」:Tag + Length + Value。

SM2签名的DER结构长这样:

SEQUENCE {
  INTEGER r,  // 签名值r,32字节大端整数
  INTEGER s   // 签名值s,32字节大端整数
}

我画了一张图,帮你把DER编码的每一层拆开看:

SM2签名DER编码结构 SEQUENCE (Tag 0x30) Length (0x44~0x46) INTEGER r (Tag 0x02) Length Value (32字节大端整数) 注意:最高位为1时,前面补0x00 INTEGER s (Tag 0x02) Length Value (32字节大端整数) 注意:最高位为1时,前面补0x00 DER编码注意事项 1. 总长度 = 2(SEQUENCE头) + 2(r头) + r长度 + 2(s头) + s长度 2. 标准SM2签名DER编码总长度为70~72字节

DER编码的坑

我整理了一个表格,把DER编码中常见的坑和解决方案列出来:

问题 现象 解决方案
前导零问题 r或s的最高字节≥0x80时,DER要求前面补0x00 编码时检查最高位,必要时插入0x00
长度歧义 32字节整数可能编码为33字节(含前导零) 固定r和s为32字节,前导零只补在最高位
签名长度不固定 DER编码长度在70~72字节之间波动 解析时不要硬编码长度,按TLV结构解析
大端小端混淆 ARM Cortex-M默认小端,SM2要求大端 编码前做字节序转换

⚠️ 我曾经踩过的坑:

有一次在STM32上做SM2验签,签名总是失败。查了两天,发现是DER解析器把r和s的长度字段当成了固定值。实际上,当r的最高字节小于0x80时,长度是32;大于等于0x80时,长度是33。这个「1字节」的差异,让我差点把开发板砸了。

实战建议

说了这么多,我总结几条实战中的铁律:

  • 签名端:随机数k必须用硬件TRNG生成,别用软件伪随机。我在产线上见过用rand()签名的,结果两个设备签名一模一样——私钥当场暴露。
  • 验签端:先做DER格式校验,再做密码学验签。很多攻击者会利用畸形DER编码绕过验签逻辑。
  • 存储端:签名和固件要分开存储。我见过有人把签名嵌在固件头部,结果升级时签名跟着固件一起被篡改——成了「自己验自己」的笑话。
  • 调试端:开发阶段建议打印r和s的十六进制值,方便对照标准向量。SM2的测试向量可以从GMT 0003.5-2012标准里找。

SM2签名验签,说白了就是「私钥签名、公钥验签、DER打包」。但每个环节都有细节,稍不注意就会出问题。我建议你在实现时,先把DER编码和解码的单元测试写到位,再去做签名验签的逻辑。这样出了问题,能快速定位是编码问题还是密码学问题。