第3章 SM2算法原理:椭圆曲线密码、密钥生成、签名验签、加密解密

3.1 椭圆曲线密码基础

SM2算法的核心,说白了就是椭圆曲线上的离散对数问题。你想想看,一条椭圆曲线上的点,咱们定义一种加法运算,然后从一点出发不断自加,得到一系列点——这个“不断自加”的过程很容易算,但反过来,给你一个点想知道加了多少次,那就难了。SM2用的就是这个单向特性。

SM2推荐了一条固定的椭圆曲线,参数是国密标准里定好的。我记得几年前做第三方对接时,有家厂商的SM2参数缓存在芯片里,结果烧录时不小心改了b值,导致签名全废——从那时候起我每次都会先校验曲线参数再做运算。

SM2椭圆曲线域参数(简写)

  • p:素数,256位
  • a, b:方程 y² = x³ + ax + b 中的系数
  • G:基点,一个有限域上的点
  • n:基点G的阶,素数
  • h:余因子,通常为1

加法运算的几何意义是:过两点P、Q画直线,交曲线于另一点-R,再关于x轴对称得到R = P + Q。倍点运算就是P加自己,需要切线。这些运算都是在有限域GF(p)上做的,不是实数,所以别拿日常几何去理解——我曾经被刚入行的同事追问为什么两条平行线不能交,嗯,好在做一次demo他就明白了。

3.2 密钥生成

SM2密钥对生成特别简单:

  1. 随机选取一个256位的大整数d,作为私钥。
  2. 计算公钥P = d * G(点乘,即d次自加)。

私钥就是个随机数,公钥就是个点。我在项目里习惯用符合国密要求的硬件随机数发生器,千万别用系统自带的伪随机数——尤其Linux的/dev/urandom在某些嵌入式环境里可能熵不够。有一次客户现场频繁出现重复私钥,查了半天发现是随机种子没处理好。

小提示:私钥长度虽说是256位,但实际有效位受曲线阶n限制,一般小于n即可。公钥有两种存储格式:压缩格式(只存x坐标加标识位)和非压缩格式(存x、y两个坐标)。与国外标准互操作时注意格式协商。

3.3 签名与验签

SM2签名算法借鉴了Schnorr签名思想,但做了改进——加入了一个可选的用户标识符,避免重签名攻击。为什么要有标识符?我记得有一次做政务系统对接,对方要求所有签名都携带机构ID,SM2默认的带标识符签名刚好满足。

签名过程

  1. 用杂凑算法(SM3)计算消息M的杂凑值e。
  2. 生成随机数k(1<k<n),计算椭圆曲线点(x1, y1) = k*G。
  3. 计算r = (e + x1) mod n。若r=0或r+k=n,回到第2步。
  4. 计算s = ((1 + d)⁻¹ * (k − r * d)) mod n。若s=0,回到第2步。
  5. 签名值为(r, s),一般拼接为64字节。

特别注意:随机数k必须每次重新生成且不可重复。我曾见过某厂商实现把k写死在程序里,导致私钥直接能被算出来——这是最典型的SM2安全漏洞之一。建议用RFC 6979那样的确定性随机数方案,或者直接调用硬件安全模块的随机数接口。

验签过程

  1. 计算e(与签名方相同方式)。
  2. 校验r, s分别在[1, n-1]范围内。
  3. 计算t = (r + s) mod n,若t=0则验签失败。
  4. 计算椭圆曲线点(x1', y1') = s*G + t*P。
  5. 计算R = (e + x1') mod n,若R == r则通过,否则失败。

这里有个常见的坑:验签时点加计算一定要做无穷远点检查。我做过一次性能优化,省略了这个检查,结果遇到特定输入导致S点P点正好互为逆元,返回了无穷远点,验签直接崩了。务必把点加结果判空处理。

3.4 加密与解密

SM2加密采用了混合加密结构:用随机临时密钥协商出会话密钥,再用对称密码(SM4)加密数据。设计理念是把椭圆曲线计算负担放在密钥协商阶段,大数据量加密则靠对称部分。

加密流程

  1. 生成随机数k,计算椭圆曲线点C1 = k*G。
  2. 计算临时密钥点k*P(P为接收方公钥),取其x坐标作为部分密钥材料。
  3. 通过KDF导出会话密钥t。
  4. 用SM4 CBC模式加密明文消息M,得到密文C2。
  5. 计算杂凑值C3 = SM3(x2 || M || y2),用于完整性校验。
  6. 输出密文 C1 || C2 || C3(顺序固定)。

密文格式:

字段 长度 说明
C1 64字节(非压缩) 临时公钥点
C2 与明文等长 SM4密文
C3 32字节 SM3杂凑值

解密流程

  1. 从密文中取出C1,校验C1是否在曲线上。
  2. 用私钥d计算临时点d*C1,得到与加密方相同的密钥材料。
  3. 用同样KDF导出会话密钥t。
  4. 用SM4解密C2得到M'。
  5. 计算杂凑值C3' = SM3(x2 || M' || y2),与C3比较。
  6. 若一致,输出M';否则报错。

解密时最容易出错的是点校验。C1有可能不是曲线上点——攻击者可以故意给个错误点让你解密出错误数据,或者触发异常。我在对接银行接口时遇到过对方发的C1坐标有奇数还是偶数的编码问题,折腾了两个小时才发现是点压缩格式不匹配。所以编码标准一定要在协议里写死。

3.5 知识框架总览

下面这张图把SM2算法涉及的三个主要运算场景串在一起,方便你理解整体结构:密钥对是基础,签名验签和加密解密是上层应用,共享一个椭圆曲线运算层。

SM2 算法知识框架 椭圆曲线参数 (p, a, b, G, n, h) 密钥生成 签名验签 加密解密 随机数 d → 私钥 点乘 d*G → 公钥 P SM3 杂凑计算 e 点乘 + 模运算 → (r,s) 临时密钥 k → C1 SM4对称加解密 + SM3校验 所有运算共享椭圆曲线点加、倍点、点乘底层函数 模块间通过密钥对 (d, P) 连接

3.6 工程实现注意事项

把上面这些原理写进代码时,有几点我反复提醒团队:

  • 输入校验不能省:公钥做点乘前要检查它在不在曲线上,签名值r,s要检查范围,密文C1要检查是否是无穷远点。
  • 随机数质量决定安全:永远不要用固定k,不要用线性同余生成器。我见过一个项目用rand()生成私钥,安全评审直接被要求返工。
  • 内存安全:私钥和会话密钥用完后要显式清零。带安全属性的平台最好用memset_s或volatile防止编译器优化掉。
  • 对照标准:SM2算法细节请以GM/T 0003.1-2012为准。不同厂家实现的字节序、点编码格式可能有差异,对接前先测试互操作性。

实现SM2时,建议把椭圆曲线运算单独封装成库,上层调用者只管传私钥、公钥、明文/签名。这样后续换曲线(比如换到SM9的扭曲曲线)只用改底层,接口不变。