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 我在项目中的做法

我曾经在一个嵌入式项目里踩过坑。设备没有硬件随机数发生器,只能用软件模拟。结果生成的私钥出现了重复——因为种子来自系统启动时间,而多台设备在同一批次启动时时间戳相同。

后来我改用了混合方案:

  1. 从多个熵源(ADC 噪声、网络延迟、传感器抖动)收集原始熵。
  2. 通过 SHA-256 进行熵提取和扩展。
  3. 最终输入到操作系统 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 密钥生成流程应该是这样的:

  1. 获取高质量随机数:从 CSPRNG 读取 32 字节。
  2. 校验私钥范围:确保 \( d \in [1, n-2] \),如果不在范围内则重新生成。
  3. 计算公钥:执行椭圆曲线标量乘法 \( P = [d]G \)。
  4. 编码输出:按需求输出 DER 或 PEM 格式的私钥和公钥。

下面这张图把整个流程串起来了:

SM2 密钥生成流程 熵源(硬件/OS) CSPRNG 生成 32 字节 校验 d ∈ [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),但严谨的代码必须校验。

我的习惯:生产环境中,我会在密钥生成后立即做一次「签名-验签」自检。用刚生成的私钥签一条固定消息,再用公钥验签。如果验签失败,说明密钥生成环节有问题,立即丢弃并重新生成。这个自检成本极低,但能拦截绝大多数低级错误。