4. 使用Go生成SM2密钥对

先说句实在的。密钥对是整个密码体系的根基,根没扎牢,后面签名验签全是泡沫。我最早做国密项目时,图省事直接调了个过时的第三方库,结果密钥格式不标准,跟华为的网关死活对不上。从那以后,我再也不敢在密钥生成这一步将就。

本章我们就用两个主流Go国密库——gmssl/go和tjfoc/gmsm——把手里的SM2密钥对生出来,再稳稳当当地存到文件里。

4.1 两个库怎么选?

对比维度 gmssl/go tjfoc/gmsm
维护活跃度 GmSSL官方维护,更新快 社区维护,最近更新较慢
API风格 偏底层,接近OpenSSL 封装更友好,开箱即用
PEM支持 内置PEM读写 需手动编码/解码
CGO依赖 需要CGO(依赖libcrypto) 纯Go,无CGO
推荐场景 生产环境、合规要求高 快速原型、离线环境
我的习惯:如果项目最终要对接现有CA或硬件加密机,我用gmssl/go;如果是内部小工具或容器化部署(不想折腾CGO),我选tjfoc/gmsm。两个库生成的密钥本质上是一样的椭圆曲线,能互认互验。

4.2 安装与导入

先说gmssl/go的安装——注意这货需要CGO。你得先装好OpenSSL 1.1.1+的开发头文件。

# Ubuntu/Debian
sudo apt-get install libssl-dev

# macOS (Homebrew)
brew install openssl

# 安装Go包
go get -u github.com/gmssl/gmsm/x509
go get -u github.com/gmssl/gmsm/sm2

tjfoc/gmsm就简单多了,纯Go,一行搞定:

go get -u github.com/tjfoc/gmsm/sm2
踩过的坑:我曾经在CI/CD环境里忘了安装libssl-dev,gmssl/go编译报错CGO找不到头文件,排查了半小时。后来我习惯在Dockerfile里显式加一行apt-get install libssl-dev。

4.3 生成密钥对的两种姿势

4.3.1 用gmssl/go生成

package main

import (
    "crypto/rand"
    "fmt"
    "github.com/gmssl/gmsm/sm2"
    "github.com/gmssl/gmsm/x509"
)

func main() {
    // 生成密钥对
    privateKey, err := sm2.GenerateKey(rand.Reader)
    if err != nil {
        panic("生成SM2私钥失败:" + err.Error())
    }

    // 提取公钥
    publicKey := &privateKey.PublicKey

    // 序列化为DER格式
    privDER, _ := x509.MarshalECPrivateKey(privateKey)
    pubDER, _ := x509.MarshalPKIXPublicKey(publicKey)

    fmt.Printf("私钥(DER): %x\n", privDER)
    fmt.Printf("公钥(DER): %x\n", pubDER)
}

说白了这段代码就三步:调用sm2.GenerateKey、拿公钥、转成DER。注意rand.Reader是系统的密码学安全随机数源,千万别自己写rand.Seed。

4.3.2 用tjfoc/gmsm生成

package main

import (
    "fmt"
    "github.com/tjfoc/gmsm/sm2"
)

func main() {
    // 生成密钥对
    privateKey, err := sm2.GenerateKey()
    if err != nil {
        panic("生成SM2私钥失败:" + err.Error())
    }

    // 获取公钥
    publicKey := &privateKey.PublicKey

    // 序列化为字节切片(ASN.1编码)
    privBytes, _ := sm2.MarshalSm2PrivateKey(privateKey, nil)
    pubBytes, _ := sm2.MarshalSm2PublicKey(publicKey)

    fmt.Printf("私钥(Hex): %x\n", privBytes)
    fmt.Printf("公钥(Hex): %x\n", pubBytes)
}
关键差异:gmssl/go要求传rand.Reader,tjfoc/gmsm内部自己处理了随机源。另外,tjfoc的Marshal函数第二个参数为nil表示不加密,如果你需要加密私钥,可以传入一个密码短语([]byte)。

4.4 保存密钥:PEM才是持久化的王道

DER是二进制,直接写文件也行,但可读性太差。我习惯用PEM格式——说白了就是在DER外面包一层Base64和头尾标识。这样不但人能看,还能跟OpenSSL互相导入。

4.4.1 用gmssl/go保存PEM

import (
    "encoding/pem"
    "os"
)

// 保存私钥为PEM
privPEM := &pem.Block{
    Type:  "EC PRIVATE KEY",
    Bytes: privDER,   // 上面生成的DER数据
}
file, _ := os.Create("sm2_private.pem")
pem.Encode(file, privPEM)
file.Close()

// 保存公钥为PEM
pubPEM := &pem.Block{
    Type:  "PUBLIC KEY",
    Bytes: pubDER,
}
file, _ = os.Create("sm2_public.pem")
pem.Encode(file, pubPEM)
file.Close()

4.4.2 用tjfoc/gmsm保存PEM

import (
    "encoding/pem"
    "os"
)

// 私钥序列化为DER后再包PEM
privDER, _ := sm2.MarshalSm2PrivateKey(privateKey, nil)
privPEM := &pem.Block{
    Type:  "SM2 PRIVATE KEY",
    Bytes: privDER,
}
file, _ := os.Create("sm2_private.pem")
pem.Encode(file, privPEM)
file.Close()

// 公钥
pubDER, _ := sm2.MarshalSm2PublicKey(&privateKey.PublicKey)
pubPEM := &pem.Block{
    Type:  "SM2 PUBLIC KEY",
    Bytes: pubDER,
}
file, _ = os.Create("sm2_public.pem")
pem.Encode(file, pubPEM)
file.Close()
注意:PEM的Type字段里,gmssl/go用"EC PRIVATE KEY"(兼容标准),tjfoc用"SM2 PRIVATE KEY"(国标推荐)。两者都能被对方读取,但建议统一为国标格式,方便跟第三方系统对接。

4.5 实战流程:从生成到落地

下面这张图把整个密钥对生成到保存的流程串起来,你一看就明白:

SM2密钥对生成与保存流程 选择:gmssl/go 或 tjfoc/gmsm 调用 sm2.GenerateKey() 提取公钥:privateKey.PublicKey Marshal + PEM → 私钥文件 Marshal + PEM → 公钥文件 ✅ 密钥对就绪,可签名/验签 注意 CGO或纯Go 取决于库选择
流程图解读:无论选哪个库,核心就是三步走——生成密钥对、提取公钥、序列化并保存成PEM。左分支处理私钥,右分支处理公钥。私钥文件一定要保护好权限(chmod 600)。

4.6 避坑指南

  • 随机数种子问题:我遇到过一个同事在生产环境上用了math/rand生成密钥,导致多台机器冒出相同密钥,差点酿成安全事故。SM2密钥必须用crypto/rand,这个没得商量。
  • PEM类型混淆:有人说“我用OpenSSL打不开生成的PEM”。我一看,私钥PEM的Type写成了"EC PRIVATE KEY",而对方OpenSSL版本只认"PRIVATE KEY"(不带EC)。解决办法:两个库统一用"SM2 PRIVATE KEY"最安全。
  • 文件权限:私钥文件默认创建可能是0644,你想想看,同一台服务器上的其他进程都能读走你的私钥。我曾经吃过这个亏,后来在保存后手动os.Chmod(fileName, 0600),养成习惯。
  • CGO兼容性:用gmssl/go的项目如果跑在alpine镜像里,要额外安装gcc和libssl-dev。我推荐用多阶段构建,第一阶段编译,第二阶段用scratch或distroless运行,这样镜像体积小很多。

4.7 小结

嗯,到这里你已经能独立生成SM2密钥对并保存为PEM文件了。不管用gmssl/go还是tjfoc/gmsm,核心思路都一样:生成→导出→包装→保存。两个库生成的密钥可以互换使用,这点你尽管放心。

最后给你一个实用建议:把密钥生成函数封装成一个独立模块,比如pkg/crypto/keygen.go,输入一个目录路径,自动生成私钥.pem和公钥.pem。这样以后在任何项目里都能复用,省时又省心。