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密钥对生成
密钥生成其实就三步,非常简单:
- 用真随机数发生器生成一个256位的随机数
d,范围在 [1, n-2] - 计算公钥
P = d * G(标量乘法) - 输出私钥
d(32字节)和公钥P(64字节,未压缩格式)
嗯,这里要注意——随机数质量直接决定密钥安全。我曾经在一个IoT项目里发现,某款MCU的硬件随机数发生器在刚上电时输出的前几个数有规律可循。后来我们强制要求:生成密钥前先"预热"随机数发生器,丢弃前64字节。
固件实现建议:公钥通常以04开头(未压缩格式),后面跟x坐标和y坐标各32字节。如果存储空间紧张,也可以使用压缩格式(02或03开头),但验签时需要解压缩,会多一次模平方根运算。
4.3 SM2数字签名与验签
SM2签名算法基于椭圆曲线离散对数难题。我把它拆成两个部分来讲。
签名过程(使用私钥d)
- 计算消息摘要
e = SM3(Z || M),其中Z是签名者身份标识 - 生成随机数
k,计算(x1, y1) = k * G - 计算
r = (e + x1) mod n,如果r=0或r+k=n则重新选k - 计算
s = ((1 + d)⁻¹ * (k - r * d)) mod n,如果s=0则重新选k - 输出签名 (r, s),各32字节
我曾经踩过的坑:随机数k绝对不能重复使用!如果两次签名用了同一个k,攻击者可以直接算出私钥。我在做固件升级签名服务器时,专门加了一个计数器,确保每次签名用的k都不同。另外,k也不能有高位比特泄露,否则通过侧信道也能恢复私钥。
验签过程(使用公钥P)
- 检查r和s是否在 [1, n-1] 范围内
- 计算
e = SM3(Z || M) - 计算
t = (r + s) mod n,如果t=0则验签失败 - 计算
(x1, y1) = s * G + t * P - 验证
r == (e + x1) mod n
你想想看,验签过程只用到了公钥和签名,私钥全程不参与。这就是非对称加密的魅力——公钥可以公开分发,私钥死死锁在安全存储区里。
4.4 SM2公钥加密与解密
SM2加密不是直接用公钥加密明文,而是用"密钥封装+对称加密"的混合模式。说白了就是:
- 用公钥加密一个临时生成的对称密钥
- 用这个对称密钥(SM4)加密真正的数据
具体流程如下:
加密(使用公钥P)
- 生成随机数
k,计算C1 = k * G(椭圆曲线点) - 计算
S = k * P(共享点),取x坐标作为密钥派生函数的输入 - 用KDF派生对称密钥
t,加密明文M得到C2 - 计算
C3 = SM3(x || M || y),用于完整性校验 - 输出密文
C = C1 || C3 || C2
解密(使用私钥d)
- 从C中提取C1,验证C1在曲线上
- 计算
S = d * C1,得到与加密端相同的共享点 - 用同样的KDF派生密钥,解密C2得到明文M
- 重新计算SM3校验值,与C3比对
固件中的性能考量:SM2解密只做一次标量乘法(d * C1),而加密要做两次(k * G 和 k * P)。所以在嵌入式设备上,解密通常比加密快。我建议把计算密集的加密操作放在服务器端,设备端只做解密。
4.5 SM2核心流程总览
下面这张图把SM2的密钥生成、签名验签、加密解密串在了一起。你可以把它当作固件开发时的"路线图"。
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,也能在软件层兜住。