SM2 非对称加密原理:从椭圆曲线到实战
说实话,很多工程师一听到「椭圆曲线密码学」就觉得头大。我当年刚接触 SM2 时也一样,觉得这东西太数学了。但后来在项目中踩过几次坑,才真正理解——其实核心思想没那么复杂。
SM2 是我国自主研发的非对称密码算法,基于椭圆曲线。它跟 RSA 最大的区别在于:同样的安全强度下,SM2 的密钥更短、计算更快。嗯,这也是为什么国密标准选了它。
椭圆曲线密码学基础
先别被名字吓到。椭圆曲线在密码学里,说白了就是一条方程:y² = x³ + ax + b。再加上一个无穷远点,构成一个群。
我习惯把椭圆曲线上的点加法想象成「在曲线上画切线找交点」。两个点相加,结果还是曲线上的点。这个性质,就是 SM2 安全性的根基。
核心概念:椭圆曲线上的点构成一个循环群。私钥是一个随机整数,公钥是私钥乘以基点 G 得到的另一个点。从公钥反推私钥,就是「椭圆曲线离散对数问题」——目前没有多项式时间解法。
SM2 使用的椭圆曲线参数是国家标准规定的,包括:
- p:素数域的大小(256 位)
- a, b:曲线方程系数
- G:基点(一个坐标点)
- n:基点 G 的阶(素数)
这些参数是公开的,你不需要自己生成。直接用就行。
我的经验:千万别自己改曲线参数!我曾经见过一个项目,为了「增强安全性」自己改了 a 和 b,结果曲线退化成了奇异曲线,私钥几分钟就被算出来了。国标参数是经过严格验证的,直接用就好。
SM2 密钥对生成
生成 SM2 密钥对其实就三步:
- 选一个随机数
d,范围在 [1, n-2] 之间 - 计算
P = d * G(椭圆曲线上的标量乘法) - 私钥就是
d,公钥就是P
代码实现大概长这样:
// 伪代码示例
function generateSM2KeyPair(): KeyPair {
// 1. 生成随机私钥
let d = randomBigInt(1, n - 2);
// 2. 计算公钥
let P = scalarMultiply(d, G);
return {
privateKey: d,
publicKey: P
};
}
这里有个关键点:随机数生成器必须足够安全。我见过有人用 Math.random() 生成私钥,结果被暴力破解了。一定要用密码学安全的随机数生成器。
避坑指南:我曾经接手过一个项目,SM2 私钥生成后直接存成了明文 JSON 文件。这等于把钥匙挂在门上。私钥必须加密存储,比如用硬件安全模块(HSM)或者至少用密码加密后存数据库。
SM2 加密与解密流程
SM2 加密不是直接用公钥加密数据。它用的是「混合加密」思路:
- 生成一个临时密钥对
- 用临时私钥和接收方公钥计算共享密钥
- 用共享密钥派生出的对称密钥加密数据
- 把临时公钥和密文一起发给接收方
解密时,接收方用自己的私钥和临时公钥算出同样的共享密钥,然后解密。
流程图如下:
你可能会问:为什么不直接用公钥加密?因为椭圆曲线上的点加法不支持直接加密大数据块。混合加密既安全又高效。
我的习惯:在实际项目中,我会把 SM2 加密封装成一个函数,内部自动处理临时密钥生成和 KDF 派生。调用方只需要传入公钥和明文,拿到密文就行。这样不容易出错。
SM2 签名与验签
签名和加密不同。签名是用私钥证明「我是我」,验签是用公钥验证「确实是你」。
SM2 签名算法基于 ECDSA,但有一些改进。核心流程:
| 步骤 | 签名方(持有私钥 d) | 验签方(持有公钥 P) |
|---|---|---|
| 1 | 计算消息哈希 e = H(M) | 计算消息哈希 e = H(M) |
| 2 | 生成随机数 k,计算点 (x1, y1) = k * G | 从签名中提取 (r, s) |
| 3 | 计算 r = (e + x1) mod n | 计算 t = (r + s) mod n |
| 4 | 计算 s = ((1 + d)⁻¹ * (k - r * d)) mod n | 计算点 (x1, y1) = s * G + t * P |
| 5 | 输出签名 (r, s) | 验证 r == (e + x1) mod n |
代码实现示例:
// SM2 签名伪代码
function sign(message: string, privateKey: bigint): Signature {
let e = hash(message);
let k = randomBigInt(1, n - 2);
let point = scalarMultiply(k, G);
let r = (e + point.x) % n;
let s = (modInverse(1 + privateKey, n) * (k - r * privateKey)) % n;
return { r, s };
}
// SM2 验签伪代码
function verify(message: string, signature: Signature, publicKey: Point): boolean {
let e = hash(message);
let t = (signature.r + signature.s) % n;
let point = addPoints(
scalarMultiply(signature.s, G),
scalarMultiply(t, publicKey)
);
let expectedR = (e + point.x) % n;
return expectedR === signature.r;
}
避坑指南:我曾经在项目中遇到一个 bug——签名时随机数 k 重复了。结果两个不同消息的签名暴露了私钥。攻击者只需要解一个二元一次方程就能算出私钥。所以,随机数 k 必须每次重新生成,绝对不能复用!
SM2 与 RSA 的对比
很多老系统还在用 RSA。我整理了一个对比表,方便你理解为什么 SM2 更适合现代场景:
| 特性 | SM2(椭圆曲线) | RSA |
|---|---|---|
| 安全强度 | 256 位 ≈ RSA 3072 位 | 3072 位 |
| 密钥长度 | 私钥 32 字节,公钥 64 字节 | 私钥 > 512 字节 |
| 签名速度 | 快(约 1ms) | 慢(约 10ms) |
| 加密速度 | 中等(混合加密) | 慢(直接加密) |
| 国家标准 | GB/T 32918 | 无(国际标准) |
说白了,SM2 在同等安全强度下,密钥更短、计算更快。这也是为什么国密标准选择了它。
核心总结:SM2 的安全性建立在椭圆曲线离散对数问题上。密钥对生成简单,但随机数必须安全。加密用混合模式,签名用改进的 ECDSA。记住:随机数不能重复,私钥不能泄露。这两条红线踩了,再强的算法也没用。