3. SM2 密钥生成:私钥与公钥的数学关系、随机数生成器的安全要求、密钥格式(PEM/DER)
密钥生成,是 SM2 落地的第一道关卡。很多朋友觉得这步简单——不就是调个库吗?
嗯,调库确实快。但我在项目中见过太多「调库一时爽,上线火葬场」的案例。密钥生成如果出了问题,后面所有环节都是白搭。今天咱们就把这步拆开揉碎,讲清楚。
3.1 私钥与公钥的数学关系
SM2 基于椭圆曲线密码学。它的核心逻辑其实很简洁:
- 私钥:一个随机的大整数 \( d \),取值范围在 \([1, n-2]\) 之间。\( n \) 是 SM2 推荐曲线的阶(一个 256 位的素数)。
- 公钥:一个椭圆曲线上的点 \( P = [d]G \)。\( G \) 是曲线的基点,\( [d]G \) 表示 \( d \) 个 \( G \) 相加(椭圆曲线上的标量乘法)。
说白了,私钥就是一个数,公钥就是把这个数乘以基点得到的坐标点。从公钥反推私钥,就是椭圆曲线上的离散对数问题——目前没有多项式时间的解法,所以安全。
关键点:公钥是二维坐标 \((x, y)\),每个分量都是 256 位。所以未压缩的公钥长度是 64 字节(不含标识头)。
我在项目中遇到过有人把公钥当字符串存,结果解析时发现坐标顺序反了。SM2 公钥的编码顺序是 \( x \parallel y \),先 x 后 y,别搞反。
3.2 随机数生成器的安全要求
私钥是随机数生成的。随机数生成器(RNG)的质量,直接决定密钥的安全性。
你想想看,如果随机数生成器有规律,攻击者就能预测你的私钥。这不是理论问题——2012 年,有人发现某些 Android 设备上的随机数生成器存在漏洞,导致大量比特币私钥被破解。SM2 也一样。
3.2.1 必须满足的条件
| 要求 | 说明 | 我的建议 |
|---|---|---|
| 真随机性 | 不能基于伪随机算法(如 rand()),必须使用硬件熵源或操作系统提供的 CSPRNG | Linux 用 /dev/urandom,Windows 用 BCryptGenRandom |
| 不可预测性 | 即使攻击者知道部分输出,也无法推断后续输出 | 避免使用线性同余生成器(LCG) |
| 无偏性 | 输出比特分布均匀,不能有统计偏差 | 生产环境建议做 NIST SP 800-22 随机性测试 |
| 抗后门 | 不能内置固定种子或弱化算法 | 开源库 + 硬件熵源混合使用 |
警告:绝对不要用 rand()、srand(time(NULL)) 或 java.util.Random 生成私钥。这些是伪随机数生成器,种子空间太小,暴力枚举即可破解。
3.2.2 我在项目中的做法
我曾经在一个嵌入式项目里踩过坑。设备没有硬件随机数发生器,只能用软件模拟。结果生成的私钥出现了重复——因为种子来自系统启动时间,而多台设备在同一批次启动时时间戳相同。
后来我改用了混合方案:
- 从多个熵源(ADC 噪声、网络延迟、传感器抖动)收集原始熵。
- 通过 SHA-256 进行熵提取和扩展。
- 最终输入到操作系统 CSPRNG 中。
这样即使某个熵源被攻击者控制,整体随机性也不会崩塌。
3.3 密钥格式:PEM 与 DER
密钥生成出来,总得存起来或者传出去。这就涉及到编码格式了。
SM2 密钥的存储格式,业界主流有两种:DER 和 PEM。它们本质上是同一份数据的不同包装方式。
3.3.1 DER 格式
DER(Distinguished Encoding Rules)是 ASN.1 的一种二进制编码。它紧凑、解析快,适合在嵌入式设备或网络传输中使用。
SM2 私钥的 DER 结构大致如下:
SEQUENCE {
INTEGER 0x01 -- 版本号
OCTET STRING (32字节) -- 私钥 d
[0] EXPLICIT {
OID sm2p256v1 -- 曲线标识
}
[1] EXPLICIT {
BIT STRING (65字节) -- 公钥(可选)
}
}
公钥的 DER 结构更简单:
SEQUENCE {
SEQUENCE {
OID sm2p256v1
}
BIT STRING (65字节) -- 未压缩公钥(0x04 || x || y)
}
提示:未压缩公钥的第一个字节固定为 0x04,表示坐标未压缩。SM2 也支持压缩格式(0x02 或 0x03),但实践中很少用。我个人习惯统一用未压缩格式,省去解压缩的麻烦。
3.3.2 PEM 格式
PEM(Privacy Enhanced Mail)就是在 DER 的基础上,加上了 Base64 编码和头尾标识。说白了,就是把二进制数据变成可读的文本。
SM2 私钥的 PEM 示例:
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBG0wawIBAQQg...
-----END PRIVATE KEY-----
公钥的 PEM 示例:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE...
-----END PUBLIC KEY-----
3.3.3 格式对比
| 特性 | DER | PEM |
|---|---|---|
| 编码方式 | 二进制 | Base64 文本 |
| 可读性 | 不可读 | 可读,可复制粘贴 |
| 文件大小 | 较小(私钥约 121 字节) | 较大(约增加 33%) |
| 解析速度 | 快 | 需要 Base64 解码 |
| 适用场景 | 嵌入式、网络传输 | 配置文件、证书交换 |
3.4 密钥生成流程总览
把上面这些串起来,一个完整的 SM2 密钥生成流程应该是这样的:
- 获取高质量随机数:从 CSPRNG 读取 32 字节。
- 校验私钥范围:确保 \( d \in [1, n-2] \),如果不在范围内则重新生成。
- 计算公钥:执行椭圆曲线标量乘法 \( P = [d]G \)。
- 编码输出:按需求输出 DER 或 PEM 格式的私钥和公钥。
下面这张图把整个流程串起来了:
3.5 避坑指南
最后,分享几个我踩过的坑:
- 私钥重复生成:我曾经在容器化部署时,多个容器共享同一个熵源,导致私钥重复。解决方案是每个容器启动时强制刷新熵池。
- 公钥编码错误:SM2 公钥的 OID 是
1.2.156.10197.1.301(sm2p256v1),不是 secp256r1 的 OID。搞混了会导致解析失败。 - PEM 换行问题:PEM 格式要求每 64 个字符换行。有些库不严格检查,但 OpenSSL 会报错。生成时务必按标准来。
- 私钥范围校验遗漏:如果私钥恰好等于 0 或 n-1,计算出的公钥会是无穷远点。虽然概率极低(2-256),但严谨的代码必须校验。
我的习惯:生产环境中,我会在密钥生成后立即做一次「签名-验签」自检。用刚生成的私钥签一条固定消息,再用公钥验签。如果验签失败,说明密钥生成环节有问题,立即丢弃并重新生成。这个自检成本极低,但能拦截绝大多数低级错误。