3. SM2算法原理与密钥对生成

SM2不是单一算法,而是一套椭圆曲线公钥密码体系。它包括加密、签名、密钥交换三个子协议。今天我们先啃它的数学基础,然后亲手生成密钥对。你看完了就能明白:为什么SM2比RSA更省带宽?为什么它的密钥对生成比想象中简单?

核心结论:SM2基于椭圆曲线上离散对数难题,256位密钥就达到RSA 2048位的安全强度。说人话就是:更短的钥匙,更结实的锁。

3.1 椭圆曲线长什么样?

数学上SM2用的曲线方程是:y² = x³ + ax + b (mod p)。这里p是一个256位的大素数。a、b是固定系数,国密标准里写死了——你不需要自己推导。

我刚开始学的时候很抗拒这些公式。直到有个项目要支持SM2密钥协商,我死活算不对共享密钥。后来才发现是曲线上点加法实现有bug……嗯,那次debug了整整两天。

其实你不用手写椭圆曲线运算。Java国密SDK都封装好了。但你得理解几个概念:

  • 基点G:曲线上一个公开的固定点。所有密钥对都从它派生。
  • 阶n:基点的个数。所有运算都在模n下完成。
  • 私钥d:一个随机大整数,范围 [1, n-2]。
  • 公钥P:P = d * G(椭圆曲线上的标量乘法)。
参数SM2推荐值(16进制)含义
pFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF素数模数
aFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC曲线系数
b28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93曲线系数
Gx32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7基点横坐标
GyBC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0基点纵坐标
nFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123基点阶

我的习惯:在配置文件里直接引用标准曲线参数,不要自己复制粘贴。我曾经从某博客复制G点坐标,结果少了一位,整整浪费半天排查签名验签不通过的问题。

3.2 密钥对生成的数学原理

生成SM2密钥对,本质上就是三步:

  1. 用安全的随机数发生器生成一个256位的随机数d(私钥)。
  2. 计算公钥P = d * G(椭圆曲线点乘)。
  3. 返回公私钥对 (d, P)。

你可能会问:点乘怎么算? 其实类似普通的乘法,但每一步都是椭圆曲线上的点加法。

举个例子:d=5时,P = 5*G = G+G+G+G+G。但实际用的是「double-and-add」算法——先叠加倍点,再混合加点,速度飞快。

我手写过一个点乘的调试版本,打印每一步坐标。当时为了排查一个跟ASN.1编码有关的bug,打印了整整三百行坐标……现在想想,直接调SDK多好。

3.3 SM2密钥对生成的Java实现

用Bouncy Castle(以下简称BC)生成SM2密钥对,最简单的方法是这样:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.KeyPair;

Security.addProvider(new BouncyCastleProvider());

KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
// 指定SM2曲线:SM2P256V1(国标名)
kpg.initialize(new ECGenParameterSpec("SM2P256V1"), new SecureRandom());
KeyPair kp = kpg.generateKeyPair();

byte[] privateKeyBytes = kp.getPrivate().getEncoded();
byte[] publicKeyBytes = kp.getPublic().getEncoded();

注意:BC默认用 SM2P256V1 作为曲线名。有的旧版本库叫 sm2p256v1,大小写敏感。我曾在生产环境踩过这个坑——开发机用大写,测试机用小写,结果密钥格式不一致。

生成的 privateKeyBytes 是PKCS8格式, publicKeyBytes 是X.509格式。里面包含曲线信息和密钥数据。你可以直接用 KeyFactory 从字节码恢复密钥。

如果不用BC,用腾讯/华为等国密SDK,API大同小异:

// 假设你引入的是某商业国密SDK
SM2KeyPair pair = SM2KeyPairGenerator.generateKeyPair();
byte[] pri = pair.getPrivateKey().getEncoded();
byte[] pub = pair.getPublicKey().getEncoded();

嗯,就这样。没有黑魔法。

3.4 密钥格式验证实战

生成完密钥,一定要验证。

我推荐的验证方法:用公钥对一段固定消息做SM2签名,再用私钥验签。签名成功,说明密钥对正确,格式没问题。

// 验证示例(伪代码)
Signer signer = Signer.getInstance("SM3withSM2", "BC");
signer.initSign(privateKey);
signer.update("hello".getBytes());
byte[] sig = signer.sign();

Signer verifier = Signer.getInstance("SM3withSM2", "BC");
verifier.initVerify(publicKey);
verifier.update("hello".getBytes());
boolean ok = verifier.verify(sig);  // true

如果这一步通了,后面加解密、签名、密钥协商基本稳了。

我曾经在生成密钥对后直接拿去加密,结果解密失败。原因是私钥编码时用了PBKDF2保护的格式,解密时忘传密码了。所以:生成完先验签名,省得后面花几小时定位。

3.5 知识体系图

下面这张SVG把SM2密钥对生成的核心流程串起来了。看一眼就明白:输入随机数,经过椭圆曲线点乘,输出公私钥对。底色就是你的学习地图。

SM2密钥对生成核心流程 安全随机数发生器 私钥 d (256位随机数) P = d · G (标量乘法) double-and-add 算法 输出:公私钥对 (d, P) 关键说明 • 私钥长度256位 • 公钥是曲线上的一个点 • 不可从公钥反推私钥 • 曲线参数固定且公开 • 首次生成请验证签名 • BC 曲线名:SM2P256V1

3.6 避坑清单(我踩过的)

  • 随机数强度不够:我曾经用 new Random() 生成私钥,结果每次重启服务密钥对一模一样……必须用 SecureRandom,最好用硬件随机数或安全提供者。
  • 曲线参数不一致:不同SDK的SM2曲线略有变种(比如椭圆曲线坐标编码方式不同)。BC的SM2公钥是04 || x || y未压缩形式,有些国产SDK默认压缩。对接前先确认格式。
  • 密钥格式带保护属性:用JKS或PKCS12存储私钥时,BC可能自动加上加密密码。如果后面用 KeyFactory 直接解析字节码会报错。要么统一用无密码的PKCS8,要么记录好密码。
  • 签名验签失败:不是密钥对不对,而是SM2签名结果包含两个大整数(r, s)的DER编码。有的系统只吐32字节+32字节的raw格式,需要做转换。

个人建议:在项目初期写一个工具类,提供 generateSM2KeyPair() 和 validateKeyPair() 两个方法。validate 里就是签名+验签。每次生成新密钥对,自动跑一遍验证。这样后续对接怎么都不会出基础问题。

好,密钥对生成本身不复杂。理解椭圆曲线点乘的原理,记住BC的曲线名,写对随机数。接下来你就能用这对钥匙去签名、加密、协商密钥了。