3、非对称密钥生成:RSA、ECC 与 SM2 实战
非对称密钥生成,说白了就是给一把锁配两把钥匙——一把公开,一把私藏。我做了这么多年安全,见过太多因为密钥生成不规范导致整个系统崩盘的案例。今天咱们就把 RSA、ECC 和 SM2 这三种主流算法掰开揉碎,看看它们到底怎么生成,又该注意什么。
核心原则:密钥生成的质量,直接决定整个密码系统的安全基线。随机数熵源不足、参数选择不当,都会让后续所有加密形同虚设。
3.1 RSA 密钥对生成
RSA 的数学基础是大整数分解难题。生成过程其实不复杂,但每一步都有坑。
3.1.1 标准生成流程
- 选择两个大素数 p 和 q:长度至少 1024 位,推荐 2048 位或更高。p 和 q 的差值不能太小,否则容易被费马分解法攻破。
- 计算 n = p × q:n 就是公钥的一部分,长度决定了 RSA 的安全强度。
- 计算欧拉函数 φ(n) = (p-1)(q-1):这个值必须保密。
- 选择公钥指数 e:常用 65537(0x10001),既保证效率又兼顾安全。别用 3 或 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 生成流程
- 选择曲线参数:包括有限域、曲线方程系数 a 和 b、基点 G、阶 n 等。推荐使用 NIST P-256、secp256k1 或国密 SM2 曲线。
- 生成随机数 d:作为私钥,取值范围 [1, n-2]。
- 计算公钥 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 知识体系总览
下面这张图把三种算法的核心流程和关系串起来了,方便你对照理解。
3.6 实战建议与避坑总结
- 随机数生成器:永远用操作系统提供的 CSPRNG(如 /dev/urandom),别自己写随机数算法。我见过有人用 rand() 生成 RSA 素数,结果每次重启服务器密钥都一样——那还加密个啥?
- 密钥长度:RSA 至少 2048 位,ECC/SM2 至少 256 位。别为了性能用 1024 位 RSA 或 160 位 ECC,那跟没加密差不多。
- 导出格式:统一用 PEM 或 DER 格式,私钥用 PKCS#8 封装,公钥用 SubjectPublicKeyInfo。不同库之间互操作时,格式不一致是最常见的坑。
- 密钥备份:生成后立即备份私钥,用密码加密存储。我习惯用 HSM 或密钥管理服务(KMS)来托管,别把私钥明文放代码仓库里——GitHub 上扫一圈,能找出几万个泄露的私钥。
一句话总结:密钥生成是安全的第一道门。门没装好,后面再强的加密算法也白搭。选对算法、用对参数、管好随机数,这三件事做到位,你的密钥生成就及格了。