SM2密钥生成:从原理到落地
密钥对是怎么算出来的?SM2曲线与密钥生成原理
SM2密钥对的核心,说白了就是椭圆曲线上的点乘运算。你想想看,一条固定的椭圆曲线(SM2国标曲线),上面有个已知的基点 G。私钥是一个随机大整数 d,取值范围是 [1, n-2],n 是基点的阶。公钥就是 d 乘以 G,结果是个曲线上的点 P。
公式很简单:P = d × G
我在项目中遇到过一个问题:线上生成密钥对的时候,偶尔耗时特别长。后来排查发现,是因为随机数生成器在某些虚拟化环境下性能不稳定。嗯,这个坑后面会细讲。
安全随机数:别在这儿栽跟头
密钥安全全看随机数。如果随机数可预测,私钥就相当于暴露了。我曾经见过一个项目,直接用 Python 的 random.randint 生成 SM2 私钥,结果上线不到一周,密钥就被逆向出来了。因为 random 模块用的是梅森旋转算法,属于伪随机,能被预测。
SM2 要求使用密码学安全的随机数生成器(CSPRNG)。常见的三种选择:
- 操作系统源:Linux 下的
/dev/urandom,Windows 下的CryptGenRandom - Python 标准库:
os.urandom或secrets模块 - 第三方库:OpenSSL 的随机数引擎,gmssl 内部已封装
/dev/random 阻塞,导致密钥生成超时。建议统一用 /dev/urandom,或者直接调用 gmssl 的 random_hex,它内部已经处理了平台差异。
密钥存储格式:PEM vs DER
生成密钥对之后,得存起来。SM2 的密钥存储格式沿用了 X.509 和 PKCS#8 标准。最常见的两种格式是 PEM 和 DER。
| 特性 | PEM | DER |
|---|---|---|
| 编码方式 | Base64 + 头尾标记 | 二进制 ASN.1 DER |
| 可读性 | 好(纯文本) | 差(二进制) |
| 大小 | 比 DER 大 30% 左右 | 紧凑 |
| 传输方式 | 方便粘贴、拷贝 | 适合二进制协议 |
| 常见场景 | 配置文件、文本交换 | 硬件安全模块、嵌入式 |
PEM 看起来像这样:
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg...
-----END PRIVATE KEY-----
SM2 私钥在 PKCS#8 标准中,会嵌套一个 EC 参数 OID(国密是 1.2.156.10197.1.301)。公钥则使用 SubjectPublicKeyInfo 结构,同样含 OID。
实战:用 Python 生成 SM2 密钥对并导出
我平时用的最多的是 gmssl 库,国产 Python 包,原生支持 SM2。下面是一个完整的生成与导出示例:
from gmssl import sm2
# 生成密钥对
private_key, public_key = sm2.GenKeyPair()
print("私钥 (hex):", private_key)
print("公钥 (hex):", public_key)
# 导出为 PEM 格式
private_key_pem = sm2.PrivateKey(private_key).export_pem()
public_key_pem = sm2.PublicKey(public_key).export_pem()
# 导出为 DER 格式
private_key_der = sm2.PrivateKey(private_key).export_der()
public_key_der = sm2.PublicKey(public_key).export_der()
with open("sm2_private.pem", "wb") as f:
f.write(private_key_pem)
with open("sm2_public.pem", "wb") as f:
f.write(public_key_pem)
这里 GenKeyPair() 内部调用了 os.urandom 作为随机源,生成 32 字节私钥后,再做一次点乘得到公钥。导出的 PEM 文件自带头尾和换行,可以直接发给对方。
这张图概括了从随机数到密钥文件的完整链路。你觉得核心就三步:随机数 → 私钥 → 公钥,最后用 PEM/DER 固化到硬盘。但每一步都有隐藏的门道,后面几章我们会深入拆解签名验签的具体实现。