第4节:SM2椭圆曲线公钥密码算法(签名、加密、密钥交换)

SM2 算法,说白了就是国密体系里的公钥密码核心。椭圆曲线这一套,国际上有 ECDSA、ECIES,我们国家有自己的曲线参数和签名/加密/密钥交换标准。做 HSM 硬件实现,这块逃不掉,而且坑不少。

我个人习惯把 SM2 拆成三层来理解:底层是有限域运算(模乘、模逆、点加、点倍),中间层是椭圆曲线上的标量乘法,上层才是签名、加密、密钥交换协议。硬件 HSM 里,最吃资源的是点乘,特别是固定基点乘和随机点乘的并行优化。

4.1 SM2 数字签名算法

签名算法,我遇到过不少实现细节问题。SM2 签名流程跟 ECDSA 很像,但有个关键差别——它引入了用户 ID 和杂凑值 Z 的计算。这个 Z 值把曲线参数、公钥、用户 ID 全部绑在一起,防止中间人替换公钥。

签名生成步骤(简化版):

  1. 计算 Z = SM3(ENTL || ID || a || b || xG || yG || xA || yA)
  2. 计算 M' = Z || M,然后 e = SM3(M')
  3. 产生随机数 k ∈ [1, n-1]
  4. 计算点 (x1, y1) = k * G
  5. 计算 r = (e + x1) mod n,若 r=0 或 r+k=n 则重选 k
  6. 计算 s = ((1 + dA)⁻¹ * (k - r * dA)) mod n,若 s=0 则重选 k
  7. 输出签名 (r, s)
经验提示: 我曾在一次项目中遇到随机数 k 的生成器偏软,导致签名 r 值频繁冲突。千万注意:k 必须是真随机,不能用伪随机种子重复生成,否则私钥会被推导出来。HSM 硬件里我建议用片上 TRNG + 确定性子随机数混合方案。

验证流程更直接:先同样算 Z 和 e,然后判断 r 和 s 是否在 [1, n-1] 内,再算 t = (r + s) mod n,若 t=0 则拒绝;计算点 (x1, y1) = s * G + t * PA,最后检查 (e + x1) mod n 是否等于 r。嗯,这里要注意:验证方不需要私钥,点乘结果正确性全靠公钥。硬件上我推荐用 Montgomery 点乘流水线,一次验证只要两次点乘。

4.2 SM2 公钥加密算法

加密算法有点像 ECIES 的变种,但用了 SM3 和 SM4(可选)做 KDF 和对称加密。流程是:

  • 加密方: 选随机数 k,算点 C1 = k * G;再算点 S = h * PB(h 是 cofactor,SM2 曲线 h=1,其实 S=P_B),然后算点 k * PB = (x2, y2)
  • 通过 KDF 从 x2||y2 导出密钥 t 比特,若 t 全零则重来
  • 用 SM3 算 C3 = SM3(x2||M||y2)
  • 对称加密(比如 SM4)得到 C2 = Enc(M)
  • 输出密文 C1 || C3 || C2
硬件实现警告: KDF 的输出长度必须严格等于对称加密的密钥长度加消息长度。我曾经见过一个实现里 KDF 只输出了 128 位,但实际需要 256 位,导致 C2 截断。另外,C3 的哈希输入顺序 x2||M||y2 不能搞反,不然验证通不过。

解密方拿到 C1 后,先验证 C1 是否在曲线上(抵抗无效曲线攻击),然后用私钥 d 计算点 d * C1 = (x2, y2),再做 KDF 导出密钥,解密 C2,重新算哈希对比 C3。整个过程硬件上最耗时的是点验证和点乘,建议用专用点验证状态机,不要用通用点乘单元串行做。

4.3 密钥交换协议

SM2 密钥交换有点像 ECDH 的加强版,加入了临时密钥和身份认证。双方各有一个长期密钥对 (dA, PA) 和 (dB, PB),另选临时密钥对 (rA, RA) 和 (rB, RB)。

基本流程:

  1. A 发 RA 给 B,B 发 RB 给 A
  2. 双方各自计算:tA = (dA + rA * x1) mod n,tB = (dB + rB * x2) mod n(x1、x2 是对方临时点的 x 坐标)
  3. 然后算共享点 U = h * tA * (PB + x2 * RB)。注意 h=1 时可以简化
  4. 从 U 的 x2||y2 导出会话密钥
  5. 双方各自计算认证标签(用 SM3 算杂凑值)并交换验证
核心要点: 密钥交换协议的硬件实现难点在于两阶段点乘:第一阶段算 tA/tB 时要用到对方临时点的 x 坐标,第二阶段算 U 时又要同时用到对方公钥和临时点。我建议用双点乘单元并行运算,不然延迟会翻倍。另外,输入顺序和杂凑算法不能错——否则两边算出来的共享密钥不一样,那就玩脱了。

4.4 SM2 三种功能的硬件实现对比

功能 主要点乘次数 哈希调用 特殊风险
签名生成 1 次(固定基点 G) 2 次 SM3 随机数 k 泄漏 → 私钥泄露
签名验证 2 次(G 和 PA) 2 次 SM3 无效曲线攻击(需验证 PA 在曲线上)
加密 2 次(G 和 PB) KDF + 2 次 SM3 C1 不在曲线上、KDF 输出长度错误
解密 1 次(C1) KDF + 2 次 SM3 验证 C1 在曲线上
密钥交换 3 次(含临时点) 3 次 SM3 参数顺序错乱、共享密钥不一致

4.5 核心流程图:SM2 签名与验证的过程

下面这张图概括了签名生成与验证的数据流,重点标出了 Z 值的计算入口和最终比较环节。硬件实现时,这两条路径可以共享点乘单元,但数据流控制逻辑要分开。

SM2 签名与验证流程 输入:M,密钥对 dA,PA 计算 Z = Hash(参数||ID) e = Hash(Z || M) 生成随机 k,计算 k*G 计算 r=(e+x1) mod n 计算 s = ... 输出 (r,s) 输入:M,签名(r,s),公钥PA 计算 Z = Hash(参数||ID) e = Hash(Z || M) 验证 r,s 在 [1,n-1] 内 计算 t=(r+s) mod n 计算 s*G + t*PA 比较 e+x1 与 r 判断: (e+x1) ≡ r ? 是 → 通过 否 → 拒绝

你看,左侧签名和右侧验证的很多计算步骤是镜像关系(Z、e 完全一样)。硬件里可以用同一组哈希引擎并行计算签名和验证,节省面积。不过要注意,签名时需要生成随机数 k,而验证不需要,所以随机数生成器只在签名路径上激活。我之前的做法是让点乘模块支持“固定基点乘”和“随机点乘”两种模式,通过微码控制寄存器切换。

4.6 避坑指南

  • Z 值同步: 签名方和验证方的用户 ID 长度、字节序必须完全一致。我曾经吃过亏——两边 ID 传错了尾字节,导致 Z 值不同,签名死活验不过。
  • 随机数复用: HSM 硬件里不要用同一个 TRNG 输出给多个并发签名线程,否则可能碰撞。我建议每个签名通道独立 TRNG 实例。
  • 点乘旁路攻击: 固定基点乘可以用预计算表加速,但要注意抗侧信道;随机点乘用 Montgomery ladder 是最安全的。
  • 无效曲线攻击: 收到对方公钥或临时点后,必须验证其是否在指定曲线上,且不是无穷远点。这个检查在硬件里可以用一个简单的自定义指令完成。
个人经验: 做 SM2 硬件实现时,我最头疼的是密钥交换协议里的临时点坐标传递顺序。SM2 标准里规定 RA 的格式是 x||y,但有的参考实现会把 y 放前面。我建议在 HSM 固件层统一用规范格式并做字节序校验,否则两边握手必崩。