2. SM2 算法原理:椭圆曲线密码学、密钥对生成、签名验签机制

做国密适配,绕不开 SM2。说白了,它就是个基于椭圆曲线的公钥密码体系。跟 ECDSA 有点像,但细节上改了不少。我最早接触 SM2 时,第一反应是「这不就是 ECDSA 换了个曲线吗?」后来踩过坑才明白——参数不同、哈希绑定不同、签名流程也有讲究。

这一章,我带你把核心东西捋一遍。

2.1 椭圆曲线密码学基础

SM2 用的是一条定义在素数域 Fp 上的椭圆曲线。具体参数是国家规定的,你不需要自己算,直接用就行。

SM2 标准曲线参数(SM2 推荐参数)

  • 素数 p:0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
  • 系数 a:0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
  • 系数 b:0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
  • 基点 G:压缩形式 X 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
  • 阶 n:0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
  • 余因子 h:1

这里要注意,基点 G 的阶 n 是一个大素数,保证了子群具备抗离散对数攻击的能力。你想想看,如果 n 有光滑因子,那私钥就容易算出来了。

椭圆曲线的加法、倍点、标量乘法是底层运算。我个人习惯把标量乘法理解为「私钥 d 与基点 G」做 d 次倍点再加起来的运算。嗯,实际用 double-and-add 算法加速。

2.2 密钥对生成

SM2 密钥对生成非常简单,就两步:

  1. 随机选择一个整数 d,范围 [1, n-2]。这就是私钥。
  2. 计算公钥 P = d * G(椭圆曲线标量乘法)。

返回的 P 是一个点,包含 (x, y) 坐标,通常用 64 字节非压缩格式或 33 字节压缩格式存储。我在项目中遇到过有人直接把公钥截成 32 字节,导致验签失败——这坑我帮人排查过,坐标必须完整。

注意:私钥 d 必须使用安全的随机数发生器生成。曾经有厂商因为用了伪随机种子导致私钥可预测,那安全性就全没了。

为了直观,我把核心流程画成一张图。

SM2 公钥密码体系核心流程 椭圆曲线参数 p, a, b, G, n, h 私钥 d (随机数) d ∈ [1, n-2] 公钥 P = d·G 椭圆曲线点 (x, y) 数字签名 (r, s) SM3 哈希 + 用户标识 + 随机数 k 验签验证 计算 t, R, 验证 R == r 私钥 d 安全存储,公钥 P 可公开 签名需用 SM3 消息摘要和 ZA 用户标识

2.3 签名验签机制

SM2 的签名算法不是直接对消息签名,而是先算消息摘要(SM3)再加上用户标识 ZA,最后用椭圆曲线运算生成 (r, s)。为什么这么做?因为要把公钥和签名者身份绑进去,防止公钥替换攻击。

我个人的一个小技巧: 如果你在做网关接入,注意两端的用户标识 ZA 必须一致。我曾经排查过半天,最后发现是标识末尾的 '\0' 没对齐。

下面是签名和验签的核心步骤,我简化成表格方便对比:

步骤 签名 (Sign) 验签 (Verify)
1 计算 ZA = HSM3(ENTLA || IDA || a || b || xG || yG || xP || yP) 同样计算 ZA(用传入的公钥)
2 M' = ZA || M,计算 e = HSM3(M') M' = ZA || M,计算 e' = HSM3(M')
3 生成随机数 k ∈ [1, n-1],计算 (x1, y1) = k·G 计算 t = (r + s) mod n,若 t == 0 则验签失败
4 计算 r = (e + x1) mod n,若 r == 0 或 r + k == n 则回到步骤3 计算 (x2, y2) = s·G + t·P
5 计算 s = ((1 + d)−1 * (k − r·d)) mod n,若 s == 0 则回到步骤3 计算 R = (e' + x2) mod n,若 R == r 则验签通过,否则失败
输出 签名 (r, s) 通过/不通过

这里有个小坑:步骤3里随机数 k 必须每次重新生成,不能重复。我记得早些年的某个实现把 k 写死了,结果同一个私钥签出来的 r 全部一样,攻击者直接能反推出私钥。嗯,这类问题在商用密码检测中查得很严。

对比 ECDSA,SM2 的签名公式特意用了 s = (1+d)−1 * (k − r·d) 而不是 ECDSA 的 s = k−1 * (e + r·d)。这样做的好处是逆运算次数少了一个,算起来更快一点——当然主要目的是为了规避专利。

关键安全检查点(BC 检测或密评必查)

  • 私钥 d 不能为 0 或 1,且必须来自合格随机数源
  • 签名时 k 不可重用,不可预测
  • 验签时先检查 r 和 s 是否在 [1, n-1] 范围内,避免畸形签名
  • r + s 不能等于 n

最后说一句,SM2 的签名长度是固定 64 字节(两个 32 字节大整数 r 和 s),比 RSA 的签名短得多。在 IoT 设备上做适配,这个优势很明显。我做 SD-WAN 网关时,包里塞 SM2 签名完全没压力。

好了,这部分原理说完,你下一步看代码时思路会更清楚。后面章节会对接具体的 API 和改造示例。