4、国密SM2签名算法:椭圆曲线基础与启动校验实战

SM2签名算法,说白了就是国密体系里的“数字手印”。你写了一段固件,怎么证明它是你写的、没人改过?SM2就是干这个的。我最早接触它是在一个电力终端的项目里,当时甲方要求启动时必须验签,否则不给过。嗯,从那以后我就把SM2的细节摸了个透。

4.1 SM2椭圆曲线密码学基础

椭圆曲线密码学(ECC)的核心,是在一个有限域上定义一条椭圆曲线,然后利用曲线上点的“加法”构成一个循环群。SM2使用的曲线参数是国家密码管理局定义的,我直接列出来:

参数 值(十六进制) 说明
p FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF 素数模数,256位
a FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC 曲线系数
b 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 曲线系数
Gx 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 基点G的x坐标
Gy BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0 基点G的y坐标
n FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 基点G的阶

你想想看,为什么SM2要用256位的曲线?因为它的安全强度相当于RSA-3072,但密钥长度只有32字节。在嵌入式环境里,这太重要了——Flash和RAM都金贵,能省一点是一点。

我的习惯:在代码里定义SM2曲线参数时,一定要用const修饰,并且加上CRC校验。我曾经见过一个案例,Flash里曲线参数被意外改写,结果签名验证永远通不过。你排查三天都未必能想到是参数坏了。

4.2 SM2签名与验签流程

SM2的签名流程,我习惯把它拆成三步:预处理、签名生成、签名验证。咱们先看签名生成:

4.2.1 签名生成(Sign)

  1. 计算消息摘要:使用SM3对消息M计算杂凑值e = SM3(M)。
  2. 生成随机数k:在[1, n-1]范围内生成一个随机数。注意,这个k必须每次都不一样,而且绝对不能泄露。
  3. 计算椭圆曲线点:(x1, y1) = k * G,取x1。
  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。
  6. 输出签名:(r, s),每个都是32字节,共64字节。

这里有个坑,我必须要说。随机数k如果重复使用,私钥dA就能被算出来。我在一个IoT设备里见过,因为随机数生成器没种子,每次上电签名用的k都一样。攻击者拿到两个签名,直接就把私钥还原了。所以,随机数质量是SM2签名的命门。

4.2.2 签名验证(Verify)

  1. 检查r和s:必须在[1, n-1]范围内,否则直接拒绝。
  2. 计算消息摘要:e' = SM3(M)。
  3. 计算t:t = (r + s) mod n。如果t == 0,验签失败。
  4. 计算椭圆曲线点:(x1', y1') = s * G + t * PA,其中PA是公钥。
  5. 计算R:R = (e' + x1') mod n。
  6. 比较:如果R == r,验签通过;否则失败。
核心要点:验签过程只用到了公钥PA,不需要私钥参与。这就是为什么你可以把公钥烧死在BootROM里,而私钥永远留在产线或安全芯片中。

4.3 SM2在启动校验中的应用

启动校验,说白了就是让BootROM在加载固件之前,先验一下签名。流程大概是这样的:

SM2启动校验流程图 上电复位 BootROM加载公钥PA 读取固件头部签名(r, s) SM3计算固件摘要e SM2验签:R == r ? 验签失败 → 停止启动 验签通过 → 跳转固件 说明:公钥PA在BootROM中固化,固件头部包含SM2签名(r, s)和固件本体

这个流程看起来简单,但实际落地时有很多细节。我挑几个关键点说说:

4.3.1 公钥的存储与保护

公钥PA必须放在BootROM里,而且BootROM本身要写保护。我见过有些方案把公钥放在Flash的某个扇区,结果攻击者通过SPI接口把公钥替换成自己的,然后用自己的私钥签名恶意固件——整个验签就形同虚设了。

避坑指南:我曾经在一个项目中,BootROM用OTP(一次性可编程)存储公钥的哈希值,而不是公钥本身。启动时先读Flash里的公钥,再算哈希跟OTP里的比对。这样即使Flash被篡改,也能检测出来。但要注意,OTP的位一旦烧错就废了,产线测试一定要覆盖这个场景。

4.3.2 签名与固件的绑定

签名(r, s)通常放在固件头部,紧跟在固件镜像之前。我建议的固件布局是这样的:

偏移 内容 大小 说明
0x0000 签名r 32字节 SM2签名第一部分
0x0020 签名s 32字节 SM2签名第二部分
0x0040 固件长度 4字节 小端格式
0x0044 固件版本 4字节 用于版本回退检测
0x0048 固件数据 可变 SM3摘要的输入

4.3.3 性能优化

SM2验签涉及两次点乘运算(s * G 和 t * PA),在低主频MCU上可能耗时几百毫秒。我的优化建议:

  • 预计算:把基点G的倍数点提前算好存起来,减少实时计算量。
  • 使用硬件加速器:很多国产MCU内置SM2/SM3硬件模块,验签时间能压到10ms以内。
  • 并行化:如果BootROM支持双核,可以让一个核算SM3摘要,另一个核准备点乘数据。
我的经验:在STM32L4上纯软件实现SM2验签,大概需要350ms。换成带国密硬件加速的GD32F5系列,直接降到8ms。如果你的产品对启动时间有严格要求(比如汽车电子要求<100ms),一定要选带硬件加速的芯片。

4.4 常见问题与避坑

最后,我总结几个实际项目中容易踩的坑:

  • 随机数重复:前面说过,k重复等于私钥泄露。解决方案是使用硬件TRNG,或者至少用一个带熵池的DRBG。
  • 签名长度检查:验签时一定要检查r和s是否在[1, n-1]范围内。我曾经见过一个攻击,通过构造r=0的签名让验签逻辑进入死循环。
  • 公钥点校验:验签前要确认公钥PA在曲线上,且不是无穷远点。否则攻击者可以用一个无效公钥绕过验签。
  • 时序攻击:SM2的点乘运算如果时间不恒定,攻击者可以通过测量验签时间来推断私钥。建议使用蒙哥马利阶梯等恒定时间算法。

嗯,SM2签名这块内容不少,但核心就三点:曲线参数别搞错、随机数要真随机、公钥要锁死。把这三点守住,启动校验就稳了八成。