3、非对称密钥生成:RSA、ECC 与 SM2 实战

非对称密钥生成,说白了就是给一把锁配两把钥匙——一把公开,一把私藏。我做了这么多年安全,见过太多因为密钥生成不规范导致整个系统崩盘的案例。今天咱们就把 RSA、ECC 和 SM2 这三种主流算法掰开揉碎,看看它们到底怎么生成,又该注意什么。

核心原则:密钥生成的质量,直接决定整个密码系统的安全基线。随机数熵源不足、参数选择不当,都会让后续所有加密形同虚设。

3.1 RSA 密钥对生成

RSA 的数学基础是大整数分解难题。生成过程其实不复杂,但每一步都有坑。

3.1.1 标准生成流程

  1. 选择两个大素数 p 和 q:长度至少 1024 位,推荐 2048 位或更高。p 和 q 的差值不能太小,否则容易被费马分解法攻破。
  2. 计算 n = p × q:n 就是公钥的一部分,长度决定了 RSA 的安全强度。
  3. 计算欧拉函数 φ(n) = (p-1)(q-1):这个值必须保密。
  4. 选择公钥指数 e:常用 65537(0x10001),既保证效率又兼顾安全。别用 3 或 5,太容易出问题。
  5. 计算私钥指数 d:d ≡ e⁻¹ mod φ(n),即 e 关于 φ(n) 的模逆元。

我的习惯:生成 p 和 q 时,我会额外检查它们是不是强素数——即 (p-1)/2 和 (q-1)/2 也是素数。虽然标准没强制要求,但能有效抵抗 Pollard's p-1 分解攻击。

3.1.2 代码示例(Python + cryptography)

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

# 生成 2048 位 RSA 密钥对
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)

# 提取公钥
public_key = private_key.public_key()

# 导出 PEM 格式
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

我曾经踩过的坑:有一次生成 RSA 密钥时,随机数生成器出了问题,导致 p 和 q 的差值只有几百。结果攻击者用费马分解法几分钟就把 n 分解了。从那以后,我每次生成完都会用 Miller-Rabin 素性检测多跑几轮,确保 p 和 q 真的够「随机」。

3.2 ECC 密钥对生成

ECC 基于椭圆曲线离散对数难题。同样的安全强度下,ECC 的密钥长度比 RSA 短得多——256 位 ECC 约等于 3072 位 RSA。

3.2.1 生成流程

  1. 选择曲线参数:包括有限域、曲线方程系数 a 和 b、基点 G、阶 n 等。推荐使用 NIST P-256、secp256k1 或国密 SM2 曲线。
  2. 生成随机数 d:作为私钥,取值范围 [1, n-2]。
  3. 计算公钥 Q = d × G:即私钥 d 与基点 G 的标量乘法。

你想想看,ECC 的生成过程比 RSA 简单多了,但坑一点不少。最典型的就是曲线选择——千万别自己造曲线,用标准曲线就好。

3.2.2 代码示例(Python + cryptography)

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.backends import default_backend

# 使用 NIST P-256 曲线
private_key = ec.generate_private_key(
    ec.SECP256R1(),
    backend=default_backend()
)

public_key = private_key.public_key()

# 导出 PEM
private_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

避坑指南:我曾经见过有人用 secp256r1 和 secp256k1 混用,结果签名验证死活过不去。记住,生成和验证必须用同一条曲线,参数完全一致才行。

3.3 SM2 密钥对生成

SM2 是国家密码管理局发布的椭圆曲线公钥密码算法,本质上也是 ECC 的一种,但曲线参数和签名算法都是中国自主设计的。

3.3.1 与标准 ECC 的差异

对比项 SM2 NIST P-256 secp256k1
曲线方程 y² = x³ + ax + b y² = x³ - 3x + b y² = x³ + 7
素数 p 0xFFFFFFFE...(256 位) 0xFFFFFFFF...(256 位) 0xFFFFFFFF...(256 位)
基点 G 固定值 固定值 固定值
签名算法 SM2 签名(类似 ECDSA 但不同) ECDSA ECDSA
密钥交换 SM2 密钥交换协议 ECDH ECDH

3.3.2 生成流程

SM2 的密钥生成流程和标准 ECC 几乎一样,只是曲线参数不同。我建议直接用国密标准的实现库,比如 GmSSL 或 PicoSSL,别自己手写曲线参数——万一写错一个字节,整个密钥对就废了。

# 使用 GmSSL 生成 SM2 密钥对(伪代码示例)
from gmssl import sm2

# 创建 SM2 对象
sm2_crypt = sm2.CryptSM2()

# 生成密钥对
private_key = sm2_crypt.generate_private_key()
public_key = sm2_crypt._get_public_key(private_key)

# 导出
print("私钥:", private_key)
print("公钥:", public_key)

注意:SM2 的私钥长度是 32 字节(256 位),公钥是 65 字节(未压缩格式,包含 0x04 前缀 + x 坐标 + y 坐标)。压缩格式是 33 字节。导出时务必确认格式,否则对方解析不了。

3.4 三种算法的对比与选型

维度 RSA ECC(P-256) SM2
安全强度(等效对称密钥) 112 位(2048 位密钥) 128 位 128 位
密钥长度 2048~4096 位 256 位 256 位
生成速度 慢(需要找大素数) 快 快
签名/解密速度 慢 快 快
标准化程度 全球通用 全球通用 中国国标
适用场景 传统系统、兼容性要求高 移动端、IoT、高性能场景 国密合规、政务系统

我的建议:新项目优先选 ECC 或 SM2。RSA 虽然成熟,但密钥太长、生成太慢。除非你要兼容老系统,否则别再用 RSA 2048 以下了——那玩意儿现在用普通服务器都能几个月内破解。

3.5 知识体系总览

下面这张图把三种算法的核心流程和关系串起来了,方便你对照理解。

非对称密钥生成知识体系 非对称密钥生成知识体系 RSA 选择 p, q(大素数) 计算 n = p × q 选择 e(常用 65537) 计算 d ≡ e⁻¹ mod φ(n) 公钥: (n, e) | 私钥: (d) ECC 选择曲线参数 生成随机数 d(私钥) 计算 Q = d × G 公钥: Q | 私钥: d 推荐曲线: P-256, secp256k1 SM2 使用国密标准曲线 生成随机数 d(私钥) 计算 Q = d × G 公钥: Q | 私钥: d 兼容国密合规要求 核心安全要素 随机数熵源充足 | 参数标准化 | 密钥长度合规 | 导出格式统一 密钥生成 密钥存储 密钥使用 密钥生命周期管理:生成 → 存储 → 使用 → 轮换 → 销毁

3.6 实战建议与避坑总结

  • 随机数生成器:永远用操作系统提供的 CSPRNG(如 /dev/urandom),别自己写随机数算法。我见过有人用 rand() 生成 RSA 素数,结果每次重启服务器密钥都一样——那还加密个啥?
  • 密钥长度:RSA 至少 2048 位,ECC/SM2 至少 256 位。别为了性能用 1024 位 RSA 或 160 位 ECC,那跟没加密差不多。
  • 导出格式:统一用 PEM 或 DER 格式,私钥用 PKCS#8 封装,公钥用 SubjectPublicKeyInfo。不同库之间互操作时,格式不一致是最常见的坑。
  • 密钥备份:生成后立即备份私钥,用密码加密存储。我习惯用 HSM 或密钥管理服务(KMS)来托管,别把私钥明文放代码仓库里——GitHub 上扫一圈,能找出几万个泄露的私钥。

一句话总结:密钥生成是安全的第一道门。门没装好,后面再强的加密算法也白搭。选对算法、用对参数、管好随机数,这三件事做到位,你的密钥生成就及格了。