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 相等
我的经验: 实际开发中,直接用标准库提供的参数对象就行。比如 OpenSSL 1.1.1+ 已经内置了 SM2 参数,调用 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) {
    // 重新生成
}
安全警告: 绝对不要使用伪随机数生成器(如 C 语言的 rand())来生成私钥。必须使用操作系统提供的 CSPRNG(如 /dev/urandom、BCryptGenRandom),或者通过国密认证的硬件随机数模块。私钥泄露,整个 PKI 体系就完了。

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 编码
核心要点: 公钥编码时,算法标识(OID)必须正确。SM2 的 OID 是 1.2.156.10197.1.301。如果 OID 写错了,对方解析时会当成普通 ECC 公钥,导致后续签名验签失败。我在做互操作性测试时,因为这个 OID 问题跟三家厂商联调了整整一周。

3.5 密钥对生成全流程:一张图说清楚

下面这张图把整个流程串起来了。从曲线参数开始,到私钥生成、安全存储,再到公钥导出和编码,每一步都有对应的输出和注意事项。

SM2 密钥对生成全流程 1. 选择曲线参数 SM2 标准参数(p, a, b, G, n, h) 2. 生成私钥 d CSPRNG 生成 [1, n-2] 随机数 3. 私钥安全存储 加密 / HSM / 密钥分割 4. 计算公钥 P P = d * G(椭圆曲线点乘) 5. 公钥编码 未压缩 / 压缩 / 混合格式 6. SPKI 封装 算法 OID + 公钥字节串 输出产物:加密私钥(PEM/DER)+ 公钥(SPKI 格式) ⚠️ 注意事项 • 私钥必须使用 CSPRNG 生成,禁止用 rand() • 生产环境私钥必须存储在 HSM 或加密容器中 • 公钥编码时 OID 必须正确(1.2.156.10197.1.301) • 证书中公钥推荐使用未压缩格式(65 字节)

3.6 常见问题与避坑指南

做 SM2 密钥对生成,有几个坑我反复踩过,今天一并说出来:

  • 私钥范围检查不能省: 我曾经遇到过生成的私钥恰好等于 n-1,结果公钥计算出来是无穷远点,整个签名流程直接崩溃。所以生成后一定要做范围校验。
  • 随机数种子要更新: 有些嵌入式设备,每次重启后随机数种子一样,导致生成的私钥也相同。解决办法是每次生成前混入硬件唯一标识和时间戳。
  • 公钥点合法性校验: 从外部导入公钥时,一定要验证该点是否在曲线上。我见过有人传了一个不在曲线上的点,验签时居然通过了——因为实现有 bug。SM2 标准要求用 EC_POINT_is_on_curve() 做校验。
  • 编码格式要统一: 证书里的公钥编码格式必须和解析方约定一致。有一次我们和某 CA 对接,他们用压缩格式,我们用未压缩格式,结果证书链验证失败。后来统一成未压缩格式才解决。
我的建议: 如果你刚开始做 SM2 开发,直接用 OpenSSL 1.1.1+ 的 EVP 接口。它封装好了参数选择、私钥生成、公钥导出的全部流程。别自己实现椭圆曲线点乘——数学没错,但侧信道攻击你防不住。专业的事交给专业的库。