3. SM2 密钥对生成:从随机数到编码格式
这一章我们聊聊 SM2 密钥对的生成。说白了就是两件事:怎么安全地产生私钥,以及怎么从私钥算出公钥。最后还得看懂密钥的三种“长相”——16 进制、DER、PEM。我面试别人时最爱问的一个细节就是:“公钥计算用椭圆曲线乘法,你知道具体怎么算的吗?” 很多人能答出公式,但一到编码格式就懵了。
3.1 私钥的随机生成
SM2 私钥就是一个 位于 [1, n-1] 范围内的随机整数。其中 n 是 SM2 椭圆曲线的阶(一个约 2^256 的素数)。我当年第一次看标准时觉得这太简单了——不就是 random() 吗?后来吃了一次亏才明白:这里的“随机”必须用密码学安全随机数生成器(CSRNG),不能拿 Python 的 random.randint 来搞。
/dev/urandom 或 OpenSSL 的 RAND_bytes。我曾经见过有人用 rand()%n 生成私钥,结果因为模数偏差导致私钥分布不均匀,被攻击者用数学方法猜出了密钥。这在生产环境不能出现。
生成私钥的步骤很简单:
- 通过 CSRNG 产生 32 字节(256 位)的随机字节序列。
- 将字节序列转为大整数
k。 - 检查
k是否在 [1, n-1] 范围内。如果不在,重新生成。 - 如果
k==0或k>=n,丢弃重来。
这里有个效率窍门:SM2 的 n 非常接近 2^256,所以随机 32 字节落在有效区间的概率极高(约 99.9%),基本一次就过。我习惯在代码里加一个循环上限,防止随机数生成器出 Bug 死循环。
看一段 C++ 示例:
#include <openssl/obj_mac.h>
#include <openssl/ec.h>
#include <openssl/bn.h>
// 生成 SM2 私钥(OpenSSL 3.x)
EC_KEY *key = EC_KEY_new_by_curve_name(NID_sm2);
if (!key) {
// 错误处理
return nullptr;
}
if (1 != EC_KEY_generate_key(key)) {
// 生成失败
EC_KEY_free(key);
return nullptr;
}
const BIGNUM *priv = EC_KEY_get0_private_key(key);
// priv 就是私钥,可直接用 BN_bn2hex 转为 16 进制
hex 串输出到安全日志用于调试,但上线前会关掉这个输出。另外,SM2 私钥千万要显式擦除,不能直接 free 就完事——我建议用 OPENSSL_cleanse 清零后再释放内存。
3.2 公钥的计算过程
公钥 K 就是椭圆曲线上的一个点,公式为 K = k × G。k 是私钥,G 是 SM2 标准定义的基点(一个固定坐标点)。这里的乘法不是普通整数乘法,而是椭圆曲线上的标量乘法。
说白了,就是“把 G 点自己加 k 次”。但 k 是 256 位的大数,不能真加 2^256 次——计算复杂度必须用双倍-加算法(Double-and-Add)降到 O(log k)。
我举一个简单例子让你快速理解算法流程:
| 步骤 | 二进制表示 k 的每一位 | 操作 |
|---|---|---|
| 初始化 | — | 结果 Q = O(无穷远点) |
| 从最高位到最低位 | 1 | Q = 2Q(双倍); Q = Q + G(加) |
| 下一位 | 0 | Q = 2Q(只双倍,不加) |
| 下一位 | 1 | Q = 2Q; Q = Q + G |
实际实现时还要考虑椭圆曲线点加法中的模运算和逆元计算。OpenSSL 已经封装好了,你直接调 EC_POINT_mul 就行:
// 基于上面生成的 key,公钥已经在 key 对象中
const EC_POINT *pub = EC_KEY_get0_public_key(key);
// 或者自己算:EC_POINT_mul(group, pub, priv, NULL, NULL, ctx);
3.3 密钥格式:16进制、DER、PEM
生成完私钥和公钥后,我们要把它存到文件里或者传给对方。常见的三种格式我分别讲下。
3.3.1 16 进制串(Hex)
最直观的表示方式。私钥 Hex:直接把 256 位的整数转为 64 个十六进制字符。公钥 Hex:对于非压缩公钥,就是 04 + 64 字节 x 坐标 + 64 字节 y 坐标,总共 130 个十六进制字符。
私钥 Hex: c9a7b3d8f1e6...(64 字符)
公钥 Hex(非压缩): 04b1c2d3e4f5...(130 字符)
我一般在开发和调试阶段用 Hex 格式,因为人眼可读,直接粘贴方便。但不能在生产配置文件里用纯文本存私钥——除非你真的很想被安全审计师“问候”。
3.3.2 DER 编码
DER 是 ASN.1 的一种二进制编码,更紧凑、更标准。私钥 DER 包含版本号、私钥值、曲线标识、公钥值等字段。公钥 DER 通常是一个 SEQUENCE 包含算法标识和 BIT STRING。
举个例子,用 OpenSSL 导出 DER 格式的命令:
# 导出私钥 DER
openssl ec -in sm2_private.pem -outform DER -out sm2_private.der
# 导出公钥 DER
openssl ec -in sm2_private.pem -pubout -outform DER -out sm2_public.der
openssl asn1parse 看一眼结构。
3.3.3 PEM 编码
PEM 就是 DER 的 Base64 封装,加上头尾标记行。这是最通用的交换格式,兼容 OpenSSL、Java、Python 等几乎所有密码库。
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDD...
-----END EC PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EE...
-----END PUBLIC KEY-----
我自己的习惯是:开发环境用 Hex,测试环境用 DER,生产环境只用 PEM。PEM 文件可以加密码保护,而且绝大多数工具都能解析。
最后总结三种格式的对比:
| 格式 | 可读性 | 标准程度 | 文件大小 | 适用场景 |
|---|---|---|---|---|
| 16 进制 | 高 | 非标准(自己约定) | 最大(字符翻倍) | 调试、日志 |
| DER | 低(二进制) | ASN.1 国际标准 | 最小(约 120 字节) | 嵌入式、硬件模块 |
| PEM | 中(Base64) | RFC 7468 标准 | 中等(比 DER 大 33%) | 证书、密钥文件、网络传输 |
-----BEGIN PUBLIC KEY-----,SM2 也是同样的头(因为公钥算法标识在 OID 里区分),但私钥头不同:SM2 用 -----BEGIN EC PRIVATE KEY-----,RSA 用 -----BEGIN RSA PRIVATE KEY-----。手写一行命令就能验证:openssl ec -pubin -in sm2_pub.pem -text -noout。