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 值,签名验签死活对不上……查了两小时。

密钥对生成

生成密钥对其实很简单,就两步:

  1. 随机选一个整数 d_A ∈ [1, n-2],这就是私钥。
  2. 计算公钥 P_A = d_A · G,椭圆曲线上的标量乘法。

公钥是一个点,包含 x 和 y 坐标,各 256 bit。我一般把私钥用硬件安全模块(HSM)保护,你至少也要用安全随机数生成器,别用 Math.random() 这种——真的有人这么干过,密钥直接废掉。

关键点:SM2 私钥和公钥都只有 256 bit,比 RSA 2048 短得多,但安全等级相当。这就是 ECC 的优势。

签名与验签原理

SM2 签名算法基于 ECDSA,但有个核心区别:它用 用户标识符 Z 对消息做预处理。别小看这步,我碰到过一个合作方没统一 Z 的计算方式,签名链全部失效。

签名过程(6 步)

  1. 计算 Z_A = H_256(ENTLA || IDA || a || b || x_G || y_G || x_A || y_A),其中 IDA 是用户标识(如 “”@qq.com”),ENTLA 是标识长度。
  2. 拼接待签名消息 M:M' = Z_A || M。
  3. 计算摘要 e = H_v(M')。
  4. 生成随机数 k ∈ [1, n-1],计算 (x_1, y_1) = k · G。
  5. 计算 r = (e + x_1) mod n,如果 r = 0 或 r + k = n 则重新选 k。
  6. 计算 s = ((1 + d_A)⁻¹ · (k - r · d_A)) mod n,如果 s = 0 则重新选 k。输出签名 (r, s)。

验签过程(4 步)

  1. 校验 r, s ∈ [1, n-1],不通过直接拒签。
  2. 同样计算 Z_A 和 e(使用相同的用户标识 IDA 和公钥)。
  3. 计算 t = (r' + s') mod n,如果 t = 0 则验签失败。
  4. 计算点 (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 密钥生成和签名验签的关系整理成一张图,方便你理解整体流向:

SM2 核心流程结构 随机数 k 私钥 d_A 用户标识 IDA 消息 M P_A = d_A · G 计算 Z_A M' = Z_A || M e = H(M') (x₁,y₁) = k·G r = (e + x₁) mod n 签名 (r, s) 说明:公钥 P_A 由私钥 d_A 和基点 G 计算得到,签名过程绑定用户标识 IDA;验签时需用同一 IDA 重新计算 Z_A 和 e,否则失败。

图里左边是签名流,右边是公钥生成。我习惯先画清楚数据依赖——哪个输入进哪步计算,这样写代码时不易乱。你也试试看,画图比读标准快得多。