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进制) | 含义 |
|---|---|---|
| p | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF | 素数模数 |
| a | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC | 曲线系数 |
| b | 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 | 曲线系数 |
| Gx | 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 | 基点横坐标 |
| Gy | BC3736A2 F4F6779C 59BDCEE3 6B692153 D0A9877C C62A4740 02DF32E5 2139F0A0 | 基点纵坐标 |
| n | FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 | 基点阶 |
我的习惯:在配置文件里直接引用标准曲线参数,不要自己复制粘贴。我曾经从某博客复制G点坐标,结果少了一位,整整浪费半天排查签名验签不通过的问题。
3.2 密钥对生成的数学原理
生成SM2密钥对,本质上就是三步:
- 用安全的随机数发生器生成一个256位的随机数d(私钥)。
- 计算公钥P = d * G(椭圆曲线点乘)。
- 返回公私钥对 (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密钥对生成的核心流程串起来了。看一眼就明白:输入随机数,经过椭圆曲线点乘,输出公私钥对。底色就是你的学习地图。
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的曲线名,写对随机数。接下来你就能用这对钥匙去签名、加密、协商密钥了。