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 实战流程:从生成到落地
下面这张图把整个密钥对生成到保存的流程串起来,你一看就明白:
流程图解读:无论选哪个库,核心就是三步走——生成密钥对、提取公钥、序列化并保存成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。这样以后在任何项目里都能复用,省时又省心。