SM2密钥对生成:私钥随机生成、公钥推导、密钥格式(DER/PEM)

搞密码学的人都知道,密钥对是整个系统的根基。私钥一旦泄露,什么加密签名都是白搭。公钥推导错了,对方根本验不了你的签名。我记得刚入行那会儿,自己手写了一个SM2密钥生成,结果公钥算出来总是不对……查了半天才发现是曲线参数搞混了。这章咱们就踏踏实实把密钥对生成的每一环掰开揉碎,包括私钥怎么随机出来,公钥怎么算,最后怎么存成标准格式。

私钥的随机生成

私钥本质上是一个256位的随机整数,范围在1到n-1之间(n是SM2曲线的阶)。说白了,就是从天文数字里抽一个数,不能是0,不能是n。这个随机数必须由合格的随机数发生器产生——不是什么Math.random(),而是国家密码管理局认可的硬件熵源或经过认证的DRBG。

我个人习惯用RNG(Random Number Generator)直接读取/dev/random,或者在安全芯片里调用硬件TRNG。为什么呢?因为我在项目里遇到过两次:一次是测试环境用了伪随机种子,导致多个设备生成了同样的私钥,差点酿成安全事故;另一次是某款低端芯片的TRNG输出不稳定,抽出来的数居然偏小,反复出现n-2这种边角值。

警告: 别用rand()或SecureRandom.getInstance("SHA1PRNG")这种未经国密认证的随机数生成器。一定要用GM/T 0023或FIPS 140-2级别以上的RNG。

私钥的生成步骤其实很简单:

  1. 生成256位随机字节串
  2. 将其转为大整数,检查是否在[1, n-1]区间
  3. 如果越界或为0,重试
  4. 输出私钥d

你想想看,这个过程的计算量极小,关键就在于随机源的质量。我建议你在产品中始终做一次私钥熵源健康测试——比如连续生成1000个私钥,检查直方图分布和重复率。

公钥的推导计算

公钥P通过椭圆曲线上的标量乘法得到:P = d * G。G是SM2的基点,坐标固定。说白了就是把私钥这个数,当成“走几步”,从G开始跳跳跳,最后落在曲线上某个点。

这个计算有坑。最大的坑就是验证点是否在曲线上。计算完成后必须检查y² ≡ x³ + ax + b (mod p)。我曾在项目里遇到过第三方库实现bug,返回的点坐标根本不在SM2曲线上,但接口没报错,导致后续加密全部失效。避坑:不要信任任何外部库的“自动验证”,手工验一次,也就几十行代码。

经验之谈: 公钥推导速度取决于标量乘法实现。低端MCU上建议用Montgomery ladder,能抗侧信道攻击。我在电力终端项目里就吃过平铺算法的亏——示波器一抓电流波形,私钥直接暴露。

密钥格式与编码

有了私钥和公钥,怎么存?怎么传?标准答案:DER或PEM。别搞什么自定义字节流,后果是跨平台互换时你哭都来不及。

DER是二进制编码,基于ASN.1结构。SM2私钥的ASN.1定义大致如下:

SEQUENCE {
    INTEGER 0x01,          -- 版本
    OCTET STRING (32字节)  -- 私钥d
    [0] EXPLICIT {
        OID 1.2.156.10197.1.301  -- SM2椭圆曲线公钥密码算法
    }
    [1] EXPLICIT {
        BIT STRING (65字节) -- 未压缩公钥04 || x || y
    }
}

PEM其实就是DER的Base64编码,加上头尾标识行。私钥头是-----BEGIN EC PRIVATE KEY-----,公钥头是-----BEGIN PUBLIC KEY-----。注意SM2公钥的OID是1.2.156.10197.1.301,别和ECDSA的OID搞混。

特性DERPEM
存储形式二进制Base64文本
可读性差好,可贴入邮件
大小约121字节(私钥)约168字节(私钥)
标准支持所有语言原生所有语言原生
传输场景硬件安全模块文件、网络
实用建议: 内部存储优先用DER,省空间。对外交换用PEM,方便让人肉眼检查。我曾经在对接银行接口时,对方非要PEM格式,我直接拿DER转Base64加行头就行,省去了重新实现的麻烦。

公钥的DER结构略有不同:

SEQUENCE {
    SEQUENCE {
        OID 1.2.156.10197.1.301
        OID 1.2.840.10045.2.1   -- 公钥算法标识
    }
    BIT STRING (65字节)  -- 04 || x || y (未压缩)
}

这里有个小陷阱:公钥的格式里包含了算法标识的SEQUENCE嵌套,很多新手只传BIT STRING,结果对方解码报错。记住:公钥是一个完整的SubjectPublicKeyInfo结构。

密钥校验与验证

生成完之后不能直接拿去用,至少要跑一遍自校验。我习惯做三件事:

  1. 检查私钥范围:d ∈ [1, n-1],不是0也不是n。
  2. 验证公钥在曲线上:代入曲线方程检查。
  3. 签名测试:用一对密钥签名一个随机消息,再用公钥验签。通过才确认密钥对可用。

为什么一定要做签名测试?因为我在一个国产加密卡对接项目里遇到过,密钥对生成成功了,但实际签名时曲线运算器有硬件bug,算出的公钥坐标是镜像点,签名验不过。从那以后我不论多信任硬件,都加这一步。

SM2密钥对生成流程图 硬件/软件熵源 256位随机数 d∈[1,n-1]? 否,重试 私钥 d P = d * G (SM2) 标量乘法(Montgomery ladder) 公钥 P (x,y) 曲线方程验证 签名 & 验签测试 图:SM2密钥对生成流程及自校验检查点

最后,我给你一个生产环境下的最佳实践清单:

  • 私钥生成后立即用DER编码写入安全存储区,同时用PEM备份到HSM外接口(若产品合规允许)。
  • 公钥导出时默认用未压缩格式(65字节04开头),避免第三方不支持压缩格式。
  • 始终保留一份密钥指纹(SM3 hash)用于快速校验,我在证书签发系统里就是用这个发现重复入库的。