第4章 SM2密钥对生成:私钥随机生成、公钥推导、密钥格式归纳
密钥对生成是做 SM2 的第一步,也是后面所有操作的根基。很多同学一上来就调 openssl 或 gmssl 把密钥造出来,但实际项目中踩过的坑可不少。我记得有一次要对接一个国密网关,对方要求私钥必须符合 SM2 曲线阶的范围,结果我随手生成的一个私钥因为高位字节全是零被 reject 了…… 嗯,从那以后我再也不忽略私钥的随机性校验。
4.1 私钥随机生成 – 这步没你想的那么简单
SM2 私钥是一个整数 d,取值范围在 [1, n-2] 之间,其中 n 是 SM2 椭圆曲线的阶。n 的值是 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123。
random.randint(1, n-2),当 n 接近 2^256 时,随机数高位可能全是零,导致公钥计算异常。
我个人习惯的做法是:先拿安全的随机数生成器(比如 os.urandom)产生 32 字节,然后转换成整数,再模 n 并加 1。这样能保证私钥均匀分布在 [1, n-1] 区间内。伪代码长这样:
def generate_private_key():
# 生成 32 字节随机数
rand_bytes = os.urandom(32)
d = int.from_bytes(rand_bytes, 'big')
# 取模 n 并保证不为 0
d = (d % (n - 1)) + 1
return d
4.2 公钥推导 – 点乘运算的性能艺术
公钥 Q = d * G,其中 G 是 SM2 标准基点。这是一个椭圆曲线上的标量乘法。说白了就是连加 d 次 G,但你不能真的加 d 次(d 是 256 位整数),得用倍点——双倍-加法算法。
我常用的优化方式有两个:
- 使用 Montgomery ladder:抵抗侧信道攻击,运算时间恒定。
- 预计算窗口法:提前把 G 的 2^w 倍点存好,用空间换时间。
当然,如果你是调现成的库(比如 gmssl、openssl 的 EC 点乘),这些细节已经帮你封装好了。但你真要对性能极致优化,那还是得手写或改底层。
公钥可以有两种表示形式:
| 格式 | 字节长度 | 说明 |
|---|---|---|
| 非压缩 | 65 字节 | 0x04 + x(32) + y(32),完整坐标 |
| 压缩 | 33 字节 | 0x02/0x03 + x(32),根据 y 的奇偶确定前缀 |
4.3 密钥格式 – 让不同系统能愉快聊天
SM2 密钥对在不同平台、不同语言之间传递时,必须有一套统一的编码规则。最常见的三种格式:
- 原始字节:私钥 32 字节大端整数;公钥 65 或 33 字节。最简单,但缺少元信息。
- ASN.1 DER:遵循 SM2 密钥对的 OID 和结构,比如
SM2PrivateKey和SM2PublicKey的 ASN.1 定义。典型用途是数字证书中的主公钥。 - PEM:将 DER 二进制用 Base64 编码,加上头尾标记。人类可读,传输方便。
下面我给出一个完整的密钥对生成示例,并用原始字节输出:
import os
from gmssl import sm2
# 曲线参数(SM2 标准)
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
Gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
Gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
# 使用 gmssl 库(推荐)
private_key = os.urandom(32)
private_key_int = int.from_bytes(private_key, 'big') % (n - 1) + 1
sm2_crypt = sm2.CryptSM2(public_key='', private_key=str(private_key_int))
public_key = sm2_crypt._kg(private_key_int, (Gx, Gy))
print(f"私钥 (32字节 hex): {private_key.hex()}")
print(f"公钥 (65字节 hex, 0x04开头): {public_key.hex()}")
openssl ec -inform DER -outform PEM 在 DER 和 PEM 间互转。注意指定 SM2 曲线名 -param_enc nid_sm2。
4.4 密钥生成流程总览
下面这张图把整个密钥对生成的核心链条画清楚了。从熵源开始,到私钥校验,再经过点乘运算,最后输出两种格式的公钥。你想想看,每一步都可能成为性能瓶颈或安全漏洞。
4.5 性能优化实录
我在做某银行加密中间件时遇到过一个棘手问题:每次 TLS 握手都要生成一对 SM2 密钥,生成的频率是每秒数千次。`os.urandom` 调用太多,熵池见底,进程卡了好几个 ms。后来改用 getrandom() 批量生成 64 字节再切分,问题就解决了。
另外提一嘴密钥格式的选择。如果你的系统只在内部传递,用原始字节最快。但要和国密证书、PKI 体系对接,就必须走 DER/PEM 了。我建议做一个格式抽象层,对外统一接口,对内按场景切换实现。
好,这一章的内容就到这里。记住,密钥对生成看似简单,但随机数质量、点乘效率、格式兼容性往往是线上问题的根源。把这一步做扎实了,后面的加解密才能站得稳。