第4章 SM2密钥对生成:私钥的随机生成、公钥的计算与密钥存储
密钥对生成,是所有密码学操作的起点。你想想看,如果私钥生成得不够随机,或者公钥计算出了偏差,那后面所有的签名、加密都是空中楼阁。我在做国密合规项目时,第一步就是检查密钥生成模块的实现,这里出问题,后面全白搭。
SM2的密钥对包含两部分:一个随机生成的私钥,以及通过椭圆曲线标量乘法计算出的公钥。说白了,私钥就是一个大整数,公钥就是椭圆曲线上的一个点。今天我们就把它彻底讲透。
4.1 私钥的随机生成
私钥本质上是一个满足 1 ≤ d ≤ n-2 的随机整数,其中 n 是SM2椭圆曲线的阶(一个约 2^256 的大素数)。
核心要求:私钥必须由高质量的随机数生成器产生,不能使用伪随机或可预测的种子。
我个人习惯使用操作系统提供的 CSPRNG(密码学安全伪随机数生成器)。在 Linux 上读 /dev/urandom,在 Windows 上用 CryptGenRandom。千万别用 rand() 或 Math.random(),那是给自己挖坑。
生成步骤其实很简单:
- 从随机源获取 32 字节(256 位)的随机字节序列
- 将其转换为大整数
k - 检查
k是否在有效范围[1, n-2]内 - 如果不在范围内,重新生成
小技巧:SM2 的私钥范围是 [1, n-2],而不是 [1, n-1]。这是因为公钥计算中有一个特殊情况需要排除。虽然概率极低(约 2^-256),但标准就是这么规定的。
我曾经遇到过一个案例:某厂商直接用 SHA256("password") 作为私钥。嗯,这相当于把家门钥匙挂在门口。私钥必须真随机,任何确定性推导都是不安全的。
4.2 公钥的计算:标量乘法
公钥的计算公式很简单:
P = d × G
其中 d 是私钥,G 是 SM2 椭圆曲线的基点,× 是椭圆曲线上的标量乘法。说白了,就是把基点 G 自己加自己 d 次。
但你不能真的加 d 次——d 是个 256 位的数,循环 2^256 次,宇宙毁灭了都算不完。实际用的是「双倍-加」算法(Double-and-Add),复杂度只有 O(log d)。
4.2.1 双倍-加算法原理
这个算法的思路很简单:把私钥 d 写成二进制,然后从左到右扫描每一位。
- 遇到 0:只做点倍(Double)
- 遇到 1:先做点倍,再做点加(Add)
举个例子,假设 d = 13(二进制 1101):
| 步骤 | 位 | 操作 | 中间结果 |
|---|---|---|---|
| 1 | 1(最高位) | 初始化 Q = G | Q = G |
| 2 | 1 | Q = 2Q + G | Q = 3G |
| 3 | 0 | Q = 2Q | Q = 6G |
| 4 | 1 | Q = 2Q + G | Q = 13G ✓ |
你看,只用了 4 步就完成了。对于 256 位的私钥,最多只需要 256 次点倍和 256 次点加,现代 CPU 上毫秒级完成。
4.2.2 代码实现示例
下面是一个简化的 Python 实现,展示核心逻辑:
def sm2_pubkey_generate(private_key: int, Gx: int, Gy: int, p: int, a: int) -> tuple:
"""
使用双倍-加算法计算公钥 P = d * G
返回公钥坐标 (Px, Py)
"""
def point_add(P, Q):
"""椭圆曲线点加法"""
if P is None:
return Q
if Q is None:
return P
x1, y1 = P
x2, y2 = Q
if x1 == x2 and y1 != y2:
return None # 无穷远点
if x1 == x2:
# 点倍
lam = (3 * x1 * x1 + a) * pow(2 * y1, -1, p) % p
else:
# 点加
lam = (y2 - y1) * pow(x2 - x1, -1, p) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return (x3, y3)
# 双倍-加算法
Q = None
G = (Gx, Gy)
d = private_key
while d > 0:
if d & 1:
Q = point_add(Q, G)
G = point_add(G, G)
d >>= 1
return Q
注意:上面的代码是教学演示,生产环境请使用经过验证的密码学库(如 GmSSL、OpenSSL 1.1.1+)。自己实现椭圆曲线运算,很容易踩到侧信道攻击的坑。
4.3 密钥的格式与存储
密钥生成出来,总得有个地方放。SM2 密钥的格式,我建议直接遵循国家标准 GM/T 0009-2012。
4.3.1 私钥格式
SM2 私钥就是一个 32 字节的大整数。存储时通常有两种格式:
- 原始格式:32 字节的大端序整数,直接存二进制文件
- DER 编码:使用 ASN.1 结构封装,包含算法标识和密钥数据
我个人更推荐 DER 编码,因为它自描述,不会搞混算法类型。PKCS#8 格式就是典型代表:
PrivateKeyInfo ::= SEQUENCE {
version INTEGER,
algorithm AlgorithmIdentifier,
privateKey OCTET STRING
}
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER, -- SM2 的 OID: 1.2.156.10197.1.301
parameters ANY DEFINED BY algorithm OPTIONAL
}
4.3.2 公钥格式
公钥是椭圆曲线上的一个点,包含 x 和 y 坐标,各 32 字节。常见格式:
| 格式 | 长度 | 说明 |
|---|---|---|
| 未压缩 | 65 字节 | 0x04 || x || y(最常用) |
| 压缩 | 33 字节 | 0x02/0x03 || x(通过 x 恢复 y) |
| 混合 | 65 字节 | 0x06/0x07 || x || y(带压缩标识) |
实际项目中,我几乎只用未压缩格式。虽然多 32 字节,但省去了恢复 y 坐标的计算,而且兼容性最好。
4.3.3 安全存储建议
避坑指南:我曾经见过一个项目,把私钥明文存在数据库的 "config" 表里,字段名叫 "sm2_private_key",连个加密都没有。这相当于把保险柜密码贴在保险柜门上。
安全存储的几个原则:
- 私钥必须加密存储:使用对称加密(如 SM4)加密后再持久化
- 密钥与数据分离:加密密钥和密文不要放在同一个地方
- 使用 HSM 或 TEE:高安全场景,私钥永远不离开硬件安全模块
- 内存中及时擦除:用完后主动清零,防止内存转储泄露
4.4 本章知识体系
下面这张图,把密钥对生成的整个流程串起来了:
从随机数生成开始,经过范围检查、标量乘法,最后编码存储。每一步都有坑,每一步都要小心。
核心总结:SM2 密钥对生成,本质上就是「随机数 + 标量乘法」。随机数决定安全性,标量乘法决定正确性,格式存储决定可用性。三者缺一不可。