3. SM2 密钥对生成:椭圆曲线参数选择、私钥生成与安全存储、公钥导出与编码
好,咱们进入第三章。密钥对生成,这是 PKI 体系的根基。你想想看,如果根烂了,上面长出来的证书再漂亮也没用。SM2 的密钥对生成,说白了就是三个步骤:选好曲线参数、把私钥藏严实、把公钥编对码。每一步都有坑,我一个个说。
3.1 椭圆曲线参数选择:不是随便拿条曲线就能用
SM2 用的是国家密码管理局推荐的参数。这套参数是固定的,写在 GM/T 0003 标准里。我个人习惯,从来不自己造曲线参数,直接用标准推荐的。为什么?因为曲线参数如果选得不好,会有安全漏洞。我在项目中见过有人从网上随便拷了一组 SM2 参数,结果 p 和 n 的值都写错了,签名验签死活过不去。
SM2 标准曲线参数包括以下几个核心要素:
| 参数 | 含义 | 说明 |
|---|---|---|
| p | 有限域的模数 | 256 位素数,定义了椭圆曲线的有限域 GF(p) |
| a, b | 曲线方程系数 | 方程 y² = x³ + ax + b 中的系数 |
| G | 基点(生成元) | 椭圆曲线上的一个点,用于生成公钥 |
| n | 基点的阶 | 素数,表示 G 的循环子群的阶 |
| h | 余因子 | SM2 中 h = 1,表示曲线阶与 n 相等 |
EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_sm2) 即可。千万别手写参数,我见过有人把 p 和 n 写反了,调试了三天。
3.2 私钥生成:随机数质量决定安全底线
私钥本质上就是一个随机大整数,范围在 [1, n-2] 之间。嗯,这里要注意,私钥不能为 0,也不能等于 n-1,更不能等于 n。标准里写得很清楚,私钥 d 要满足 1 ≤ d ≤ n-2。
生成私钥的核心是随机数发生器。我曾经踩过一个坑:某款硬件加密机,随机数发生器在刚上电时输出的前 32 个字节有规律可循。那次测试发现连续生成的 100 个私钥中,有 3 个居然是一样的。后来我们加上了自检逻辑,每次生成前先取 64 字节随机数做熵源检测。
私钥生成的伪代码大致如下:
// 1. 获取 SM2 曲线参数
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_sm2);
BIGNUM *order = BN_new();
EC_GROUP_get_order(group, order, NULL);
// 2. 生成私钥(随机数)
BIGNUM *priv_key = BN_new();
do {
BN_rand_range(priv_key, order);
} while (BN_is_zero(priv_key) || BN_is_one(priv_key) ||
BN_cmp(priv_key, order) >= 0);
// 3. 验证私钥范围
// 确保 1 < priv_key < n-1
BIGNUM *max = BN_dup(order);
BN_sub_word(max, 2);
if (BN_cmp(priv_key, max) > 0) {
// 重新生成
}
3.3 私钥安全存储:加密、隔离、防泄露
私钥生成之后,怎么存?我见过最离谱的做法:把私钥明文写在配置文件里,还提交到了 Git 仓库。嗯,这种操作在安全审计里直接判死刑。
私钥存储有几种主流方式:
- 加密存储: 使用对称加密算法(如 SM4)对私钥进行加密,密钥由用户口令或硬件保护。PKCS#8 格式就是干这个的。
- 硬件安全模块(HSM): 私钥永远不离开硬件,只通过 API 调用签名/解密操作。CA 的根密钥必须用 HSM。
- 密钥分割: 将私钥拆成多份,分别存储在不同位置。比如 Shamir 秘密共享,5 份中凑齐 3 份才能恢复。
我个人习惯,在开发测试环境用加密的 PEM 文件,生产环境必须上 HSM。有一次客户要求把 CA 根密钥放在软件里,我直接拒绝了——这不是技术问题,是原则问题。
SM2 私钥的 PEM 格式示例(加密后):
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHeMEkGCSqGSIb3DQEHBqEwHjANBgkqhkiG9w0BAQEFAASBgL8wggJ9AgEAAoIB
// 此处省略具体 Base64 编码内容
-----END ENCRYPTED PRIVATE KEY-----
3.4 公钥导出与编码:从椭圆曲线点到字节串
公钥的计算很简单:P = d * G。d 是私钥,G 是基点,P 就是公钥点。但怎么把这个点编码成字节串,就有讲究了。
SM2 公钥编码有三种常见格式:
| 格式 | 长度 | 说明 |
|---|---|---|
| 未压缩格式 | 65 字节 | 0x04 || x || y,x 和 y 各 32 字节 |
| 压缩格式 | 33 字节 | 0x02/0x03 || x,通过 x 恢复 y |
| 混合格式 | 65 字节 | 0x06/0x07 || x || y,带压缩标识 |
实际项目中,SM2 证书里公钥通常使用未压缩格式。为什么?因为压缩格式虽然省空间,但接收方需要做一次椭圆曲线运算来恢复 y 坐标,增加了计算量。CA 签发证书时,我建议直接用未压缩格式,省心。
公钥导出的代码示例:
// 1. 计算公钥点
EC_POINT *pub_key = EC_POINT_new(group);
EC_POINT_mul(group, pub_key, priv_key, NULL, NULL, NULL);
// 2. 转换为未压缩字节串(65 字节)
unsigned char pub_buf[65];
EC_Point2oct(group, pub_key, POINT_CONVERSION_UNCOMPRESSED,
pub_buf, 65, NULL);
// 3. 编码为 SubjectPublicKeyInfo 结构
// 包含算法标识 + 公钥字节串
// 最终输出 DER 编码
3.5 密钥对生成全流程:一张图说清楚
下面这张图把整个流程串起来了。从曲线参数开始,到私钥生成、安全存储,再到公钥导出和编码,每一步都有对应的输出和注意事项。
3.6 常见问题与避坑指南
做 SM2 密钥对生成,有几个坑我反复踩过,今天一并说出来:
- 私钥范围检查不能省: 我曾经遇到过生成的私钥恰好等于 n-1,结果公钥计算出来是无穷远点,整个签名流程直接崩溃。所以生成后一定要做范围校验。
- 随机数种子要更新: 有些嵌入式设备,每次重启后随机数种子一样,导致生成的私钥也相同。解决办法是每次生成前混入硬件唯一标识和时间戳。
- 公钥点合法性校验: 从外部导入公钥时,一定要验证该点是否在曲线上。我见过有人传了一个不在曲线上的点,验签时居然通过了——因为实现有 bug。SM2 标准要求用
EC_POINT_is_on_curve()做校验。 - 编码格式要统一: 证书里的公钥编码格式必须和解析方约定一致。有一次我们和某 CA 对接,他们用压缩格式,我们用未压缩格式,结果证书链验证失败。后来统一成未压缩格式才解决。