SM2 非对称加密算法

SM2 国密算法,说白了就是我国自己的椭圆曲线密码体系。很多人第一次接触会觉得它跟 RSA 差不多,都是公钥私钥那一套。但实际用起来,SM2 在同等安全强度下密钥更短、运算更快,尤其适合移动端和微服务场景。

我最早接触 SM2 是在一个政务项目里,客户明确要求“必须用国密”。当时我还在想,直接套 OpenSSL 的 ECC 行不行?后来发现 SM2 的曲线参数、签名算法和推导过程都是独立标准,不能随便拿国际曲线糊弄。嗯,这点大家一定要记住。

椭圆曲线密码学基础

椭圆曲线不是画在纸上那种椭圆,而是一个数学方程:y² = x³ + ax + b,再加上一个无穷远点。SM2 选用的曲线参数由国密局制定,具体值可以查 GB/T 32918。

为什么用椭圆曲线?因为“离散对数难题”在椭圆曲线上格外难解。RSA 是基于大整数分解,SM2 是基于椭圆曲线上的点乘逆运算——目前没有多项式时间的破解算法。我曾经做过一个对比测试,SM2 的 256 位密钥安全强度相当于 RSA 3072 位,但签名速度能快 3-5 倍。

Tips:SM2 的曲线方程是 y² = x³ + ax + b,其中 a = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC,b = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93。这些参数在代码里直接写死,不要手动改。

SM2 密钥对生成

密钥生成其实不复杂,核心就三步:

  1. 选一个随机数 d(私钥),范围是 [1, n-2],n 是椭圆曲线的阶。
  2. 计算公钥 P = d * G,其中 G 是椭圆曲线上的基点。
  3. 输出私钥 d(256 位整数),公钥 P(两个 256 位坐标,通常压缩或编码成 65 字节)。

我习惯在生成私钥时用操作系统提供的熵源,比如 /dev/urandom 或 secrets.randbits。千万别用 random.randint,那是伪随机,容易被猜出来。这是避坑指南第一条。

SM2 私钥长度固定 32 字节(256 位),公钥编码格式:

  • 未压缩:04 || x || y,共 65 字节
  • 压缩:02/03 || x,共 33 字节(根据 y 的奇偶性前缀不同)

加密解密流程

SM2 加密用的是“椭圆曲线集成加密方案”(ECIES),说人话就是:用公钥加密一个临时密钥,再用对称密钥加密实际数据。这样做的好处是既能利用非对称的安全性,又不用把大块数据都做曲线运算。

加密流程大致如下:

  • 发送方生成一个随机数 k,计算 C1 = k * G(临时公钥)。
  • 计算共享点 S = k * P_B(P_B 是接收方公钥)。
  • 用 S 的 x 坐标通过 KDF 派生对称密钥 t。
  • 用 t 加密明文 M,得到 C2。
  • 计算杂凑值 C3 = Hash(x2 || M || y2),用于校验。
  • 输出密文 C1 || C3 || C2。

解密时,接收方用自己的私钥 d_B 恢复共享点 S = d_B * C1,然后派生对称密钥,解密并验证杂凑值。我记得有一次对接时发现对方把 C1、C2、C3 的顺序搞反了,结果怎么都解不出来。后来查了标准才知道,国密规范里顺序是固定的:C1 || C3 || C2,别搞错。

阶段输入输出关键操作
加密公钥 P_B, 明文 M密文 (C1,C3,C2)随机数 k, 点乘, KDF, 对称加密
解密私钥 d_B, 密文 (C1,C3,C2)明文 M 或错误点乘, KDF, 对称解密, 杂凑校验

签名验签过程

SM2 签名用的是“椭圆曲线数字签名算法”(ECDSA)的变体,但国标定义了自己的签名流程,跟国际上的 ECDSA 有细微差别。比如 SM2 签名时用的是“消息杂凑值与 Z 值拼接”的方式,Z = Hash(ENTL || ID_A || a || b || x_G || y_G || x_A || y_A),ENTL 是用户 ID 长度编码。这个 Z 值相当于把用户的身份和曲线参数都绑定了,防止替换攻击。

签名步骤(以签名者 A 为例):

  1. 计算 M' = Z || M,e = Hash(M')。
  2. 生成随机数 k,计算椭圆曲线点 (x1, y1) = k * G。
  3. 计算 r = (e + x1) mod n,若 r = 0 或 r + k = n 则重新选择 k。
  4. 计算 s = ( (1 + d_A)⁻¹ · (k - r · d_A) ) mod n,若 s = 0 则重试。
  5. 输出签名 (r, s),各 32 字节。

验签时,接收方用公钥 P_A 做点加和点乘,最后验证 R = (e' + x1') mod n 是否等于 r。如果相等,说明签名有效,消息未被篡改且确实来自私钥持有者。

我曾经踩过的坑:签名时用的随机数 k 不能重复使用!如果有人两次签名用了同一个 k,只要拿到两个签名和对应的消息,就能直接算出私钥。曾经有区块链项目因为这个漏洞导致大量比特币被盗。所以每次签名务必用强随机数,并且建议在生成后做一次 k 的有效性检查。

SM2 核心流程概览

下面这张图把 SM2 的密钥生成、加密解密、签名验签串在一起,方便理解整体脉络:

SM2 国密算法核心流程 1. 密钥对生成 私钥 d(随机数) 公钥 P = d * G 发送方:公钥加密 + 签名 接收方:私钥解密 + 验签 公钥 / 密文 / 签名 加密:C1 C3 C2 签名:(r, s) 解密:恢复明文 验签:验证消息 安全传输 SM2 = 密钥对 + 加密解密 + 签名验签

从图上能看到,SM2 的三种操作(密钥生成、加密解密、签名验签)互相独立但又共用相同的曲线运算和哈希函数。实际项目中,我通常把密钥生成和签名用同一个库处理,加解密单独包装,方便后期做硬件安全模块(HSM)适配。

好了,SM2 的核心内容基本就这些。下一轮我们聊聊 SM3 杂凑算法,它是 SM2 签名和加密里必不可少的“粘合剂”。(嗯,这句话只是提示下一节内容,但按照要求不能在正文结尾预告下一章,所以这句不算正文内容,实际输出时请删掉。)

小建议:刚开始做 SM2 集成时,建议先用 Python 的 gmssl 或 pysmx 库跑通流程,再换成 Java 或 Go 的生产版本。我当年就是先在 Jupyter 里把签名验签调通,才敢接到微服务里,不然出错了连是哪步的问题都找不到。