SM2 非对称加密原理:从椭圆曲线到实战

说实话,很多工程师一听到「椭圆曲线密码学」就觉得头大。我当年刚接触 SM2 时也一样,觉得这东西太数学了。但后来在项目中踩过几次坑,才真正理解——其实核心思想没那么复杂。

SM2 是我国自主研发的非对称密码算法,基于椭圆曲线。它跟 RSA 最大的区别在于:同样的安全强度下,SM2 的密钥更短、计算更快。嗯,这也是为什么国密标准选了它。

椭圆曲线密码学基础

先别被名字吓到。椭圆曲线在密码学里,说白了就是一条方程:y² = x³ + ax + b。再加上一个无穷远点,构成一个群。

我习惯把椭圆曲线上的点加法想象成「在曲线上画切线找交点」。两个点相加,结果还是曲线上的点。这个性质,就是 SM2 安全性的根基。

核心概念:椭圆曲线上的点构成一个循环群。私钥是一个随机整数,公钥是私钥乘以基点 G 得到的另一个点。从公钥反推私钥,就是「椭圆曲线离散对数问题」——目前没有多项式时间解法。

SM2 使用的椭圆曲线参数是国家标准规定的,包括:

  • p:素数域的大小(256 位)
  • a, b:曲线方程系数
  • G:基点(一个坐标点)
  • n:基点 G 的阶(素数)

这些参数是公开的,你不需要自己生成。直接用就行。

我的经验:千万别自己改曲线参数!我曾经见过一个项目,为了「增强安全性」自己改了 a 和 b,结果曲线退化成了奇异曲线,私钥几分钟就被算出来了。国标参数是经过严格验证的,直接用就好。

SM2 密钥对生成

生成 SM2 密钥对其实就三步:

  1. 选一个随机数 d,范围在 [1, n-2] 之间
  2. 计算 P = d * G(椭圆曲线上的标量乘法)
  3. 私钥就是 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 加密与解密流程 发送方(加密) 生成临时密钥对 (r, R) 计算共享密钥 S = r * P_B 派生对称密钥 K = KDF(S) 加密明文 M → 密文 C 输出: (R, C) 传输 (R, C) 接收方(解密) 收到 (R, C) 计算共享密钥 S = d_B * R 派生对称密钥 K = KDF(S) 解密密文 C → 明文 M 输出明文 M 说明:SM2 加密使用 ECDH 密钥交换思想,每次加密生成临时密钥对 即使同一明文多次加密,密文也不同(具备语义安全性)

你可能会问:为什么不直接用公钥加密?因为椭圆曲线上的点加法不支持直接加密大数据块。混合加密既安全又高效。

我的习惯:在实际项目中,我会把 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。记住:随机数不能重复,私钥不能泄露。这两条红线踩了,再强的算法也没用。