4. SM2椭圆曲线公钥密码算法

SM2算法,说白了就是国密体系里的"非对称加密担当"。我最早接触它是在一个电力行业的加密传输项目里,当时甲方明确要求必须用SM2做签名和密钥协商。说实话,刚开始看国密标准文档时,椭圆曲线那堆数学概念确实有点绕。但真正上手做固件移植后,你会发现——只要把几个核心概念吃透,代码实现反而比RSA更清爽。

4.1 椭圆曲线群的数学基础

SM2使用的椭圆曲线方程是:y² = x³ + ax + b,定义在素数域 Fp 上。这里的 p 是一个256位的大素数。为什么选256位?因为SM2的安全强度对标的是RSA-3072,但密钥长度只有32字节,这在嵌入式固件里非常友好。

我个人习惯把椭圆曲线群理解成一个"加法游戏":

  • 群元素:曲线上的所有点,加上一个无穷远点O(相当于零元)
  • 群运算:点加法。两个点P和Q相加,得到曲线上另一个点R
  • 标量乘法:k * P 表示P点自加k次。这是SM2所有操作的核心

关键参数一览(SM2推荐曲线)

参数含义值(十六进制前缀)
p素数域模数FFFFFFFE FFFFFFFF ... 00000001
a曲线系数FFFFFFFE FFFFFFFF ... 00000001
b曲线系数28E9FA9E 9D9F5E34 ... 9C9E9C9E
G基点(生成元)固定坐标 (Gx, Gy)
n基点G的阶FFFFFFFE FFFFFFFF ... 7203DF6B

这里要特别提一下阶(order)的概念。基点的阶n,就是最小的正整数,使得 n * G = O(无穷远点)。我在做固件签名验证时踩过一个坑——如果签名值r或s大于等于n,必须拒绝。有些芯片的硬件加速器不会自动做这个检查,你得自己加。

4.2 SM2密钥对生成

密钥生成其实就三步,非常简单:

  1. 用真随机数发生器生成一个256位的随机数 d,范围在 [1, n-2]
  2. 计算公钥 P = d * G(标量乘法)
  3. 输出私钥 d(32字节)和公钥 P(64字节,未压缩格式)

嗯,这里要注意——随机数质量直接决定密钥安全。我曾经在一个IoT项目里发现,某款MCU的硬件随机数发生器在刚上电时输出的前几个数有规律可循。后来我们强制要求:生成密钥前先"预热"随机数发生器,丢弃前64字节。

固件实现建议:公钥通常以04开头(未压缩格式),后面跟x坐标和y坐标各32字节。如果存储空间紧张,也可以使用压缩格式(02或03开头),但验签时需要解压缩,会多一次模平方根运算。

4.3 SM2数字签名与验签

SM2签名算法基于椭圆曲线离散对数难题。我把它拆成两个部分来讲。

签名过程(使用私钥d)

  1. 计算消息摘要 e = SM3(Z || M),其中Z是签名者身份标识
  2. 生成随机数 k,计算 (x1, y1) = k * G
  3. 计算 r = (e + x1) mod n,如果r=0或r+k=n则重新选k
  4. 计算 s = ((1 + d)⁻¹ * (k - r * d)) mod n,如果s=0则重新选k
  5. 输出签名 (r, s),各32字节

我曾经踩过的坑:随机数k绝对不能重复使用!如果两次签名用了同一个k,攻击者可以直接算出私钥。我在做固件升级签名服务器时,专门加了一个计数器,确保每次签名用的k都不同。另外,k也不能有高位比特泄露,否则通过侧信道也能恢复私钥。

验签过程(使用公钥P)

  1. 检查r和s是否在 [1, n-1] 范围内
  2. 计算 e = SM3(Z || M)
  3. 计算 t = (r + s) mod n,如果t=0则验签失败
  4. 计算 (x1, y1) = s * G + t * P
  5. 验证 r == (e + x1) mod n

你想想看,验签过程只用到了公钥和签名,私钥全程不参与。这就是非对称加密的魅力——公钥可以公开分发,私钥死死锁在安全存储区里。

4.4 SM2公钥加密与解密

SM2加密不是直接用公钥加密明文,而是用"密钥封装+对称加密"的混合模式。说白了就是:

  • 用公钥加密一个临时生成的对称密钥
  • 用这个对称密钥(SM4)加密真正的数据

具体流程如下:

加密(使用公钥P)

  1. 生成随机数 k,计算 C1 = k * G(椭圆曲线点)
  2. 计算 S = k * P(共享点),取x坐标作为密钥派生函数的输入
  3. 用KDF派生对称密钥 t,加密明文M得到 C2
  4. 计算 C3 = SM3(x || M || y),用于完整性校验
  5. 输出密文 C = C1 || C3 || C2

解密(使用私钥d)

  1. 从C中提取C1,验证C1在曲线上
  2. 计算 S = d * C1,得到与加密端相同的共享点
  3. 用同样的KDF派生密钥,解密C2得到明文M
  4. 重新计算SM3校验值,与C3比对

固件中的性能考量:SM2解密只做一次标量乘法(d * C1),而加密要做两次(k * G 和 k * P)。所以在嵌入式设备上,解密通常比加密快。我建议把计算密集的加密操作放在服务器端,设备端只做解密。

4.5 SM2核心流程总览

下面这张图把SM2的密钥生成、签名验签、加密解密串在了一起。你可以把它当作固件开发时的"路线图"。

SM2 核心流程框架图 密钥生成 随机数 d ∈ [1, n-2] P = d * G 输出: 私钥d, 公钥P 数字签名 e = SM3(Z || M) (x1,y1) = k * G r = (e + x1) mod n s = (1+d)⁻¹(k - r·d) 输出: 签名 (r, s) 公钥加密 C1 = k * G S = k * P KDF派生密钥 t C2 = SM4_enc(M, t) C3 = SM3(x||M||y) 输出: C1 || C3 || C2 说明: • 密钥生成是基础,签名和加密都依赖私钥/公钥对 • 签名验签使用SM3摘要,加密解密使用SM4对称算法

4.6 固件开发中的避坑指南

最后,我把自己在SM2固件移植中遇到的几个典型问题列出来,希望能帮你少走弯路:

问题场景错误表现解决方案
随机数生成器未初始化签名结果可预测,私钥泄露上电后先调用RNG自检,丢弃前64字节
标量乘法未做时间恒定通过计时攻击可恢复私钥使用蒙哥马利阶梯或固定时间实现
验签时未检查r,s范围可能接受无效签名严格检查 1 ≤ r,s ≤ n-1
公钥未验证是否在曲线上解密时可能触发除零异常使用前验证 y² ≡ x³ + ax + b (mod p)
KDF输出长度与密钥不匹配SM4解密失败KDF输出长度必须等于SM4密钥长度(16字节)

我的个人习惯:在固件里把SM2的密钥对生成、签名、验签、加密、解密封装成五个独立的API函数。每个函数入口都做参数合法性检查,出口都做结果验证。这样即使底层硬件加速器有bug,也能在软件层兜住。