SM2 椭圆曲线公钥密码算法
SM2 这套算法,说白了就是中国版的 ECC 标准。我在项目中第一次接触它时,第一反应是“跟 ECDSA 差不多嘛”,后来被坑过两次才知道——细节差异真不少。这章咱们把数学基础、密钥生成、签名验签原理,还有和 ECDSA 的关键区别捋一遍。你想想看,搞懂这些,后面迁移踩坑的几率能小一半。
数学基础
椭圆曲线密码学(ECC)的基础是有限域上的椭圆曲线。SM2 采用的曲线参数是推荐的,不是你自己随便选一条。我习惯把它想象成一个“加法群”里的离散对数问题。
- 方程:
y² = x³ + ax + b (mod p),其中a,b固定,p是大素数(256 bit)。 - 基域:
Fp,所有计算都在模 p 下进行。 - 基点 G:SM2 标准里定义了一个固定点
G = (x_G, y_G),阶为n(也是素数)。 - 曲线阶:椭圆曲线上所有点的个数,记作
h · n(h是余因子,通常为 1)。
个人经验:我建议把基域 p 和基点 G 的十六进制值记在工程配置里,别每次都从代码里挖。有一次我临时改参数测试,忘了同步 G 值,签名验签死活对不上……查了两小时。
密钥对生成
生成密钥对其实很简单,就两步:
- 随机选一个整数
d_A ∈ [1, n-2],这就是私钥。 - 计算公钥
P_A = d_A · G,椭圆曲线上的标量乘法。
公钥是一个点,包含 x 和 y 坐标,各 256 bit。我一般把私钥用硬件安全模块(HSM)保护,你至少也要用安全随机数生成器,别用 Math.random() 这种——真的有人这么干过,密钥直接废掉。
关键点:SM2 私钥和公钥都只有 256 bit,比 RSA 2048 短得多,但安全等级相当。这就是 ECC 的优势。
签名与验签原理
SM2 签名算法基于 ECDSA,但有个核心区别:它用 用户标识符 Z 对消息做预处理。别小看这步,我碰到过一个合作方没统一 Z 的计算方式,签名链全部失效。
签名过程(6 步)
- 计算
Z_A = H_256(ENTLA || IDA || a || b || x_G || y_G || x_A || y_A),其中 IDA 是用户标识(如 “”@qq.com”),ENTLA 是标识长度。 - 拼接待签名消息 M:
M' = Z_A || M。 - 计算摘要
e = H_v(M')。 - 生成随机数
k ∈ [1, n-1],计算(x_1, y_1) = k · G。 - 计算
r = (e + x_1) mod n,如果 r = 0 或 r + k = n 则重新选 k。 - 计算
s = ((1 + d_A)⁻¹ · (k - r · d_A)) mod n,如果 s = 0 则重新选 k。输出签名(r, s)。
验签过程(4 步)
- 校验
r, s ∈ [1, n-1],不通过直接拒签。 - 同样计算
Z_A和e(使用相同的用户标识 IDA 和公钥)。 - 计算
t = (r' + s') mod n,如果 t = 0 则验签失败。 - 计算点
(x_1', y_1') = s' · G + t · P_A,然后r = (e' + x_1') mod n。如果等于签名中的 r',就接受。
注意:签名中
r, s 不可逆推私钥,但这不代表随机数 k 可以重复。我在日志里见过两次用同一个 k 签不同消息的情况——私钥直接暴露,真事。
与 ECDSA 的差异点
我记得刚开始做迁移时,把 SM2 签名代码按 ECDSA 逻辑写,结果验签一直通不过。查标准才意识到几个关键差异:
| 对比项 | ECDSA(ANSI X9.62) | SM2(GB/T 32918) |
|---|---|---|
| 消息预处理 | 直接对 M 做 hash | 先拼 ZA(含用户标识),再 hash |
| 签名计算 r | r = x1 mod n | r = (e + x1) mod n |
| 签名计算 s | s = (k⁻¹ · (e + r·dA)) mod n | s = ((1+dA)⁻¹ · (k − r·dA)) mod n |
| 验签步骤 | u1 = e·s⁻¹, u2 = r·s⁻¹,计算点 u1·G + u2·PA | t = (r' + s') mod n,计算 s'·G + t·PA,判 r 相等 |
| 用户标识依赖 | 无 | 必须用 IDA 生成 ZA |
说白了,SM2 在 ECDSA 上加了 “身份绑定” 这个护城河。你在迁移时,只要把签名/验签中消息预处理和 s 的计算公式改对,其他部分可以复用已有的 ECC 库。不过,我建议不要自己手写有限域运算,用现成的 OpenSSL 或 GMSSL 封装层。
避坑指南:我曾经在做国密网关时,ECDSA 验签时直接把 SM2 的
解决方案:对签名
s 当作 ECDSA 的 s 来算 s⁻¹,结果曲线参数一样,但公式不同——验签永远失败。解决方案:对签名
(r, s) 单独写一个 sm2_verify 函数,不要跟 ECDSA 共用同一段验签代码。
核心流程结构
我把 SM2 密钥生成和签名验签的关系整理成一张图,方便你理解整体流向:
图里左边是签名流,右边是公钥生成。我习惯先画清楚数据依赖——哪个输入进哪步计算,这样写代码时不易乱。你也试试看,画图比读标准快得多。