SM2 密钥生成对比:不同 SDK 的生成接口与参数差异
密钥生成是国密应用的起点。SM2 的密钥说白了就是一对椭圆曲线密钥,但不同 Go SDK 在生成方式、返回结构、参数控制上差异不小。我刚开始切国密项目时,就因为没仔细看这些差异,导致签名验签对不上,排查了整整一下午。
这一节我们专门对比主流 Go 国密 SDK 的密钥生成接口。你会看到:同样的 SM2,调用方式完全不同,有的返回结构体,有的返回指针,还有的让你自己指定随机源。
主流 SDK 密钥生成接口概览
目前 Go 生态里能用的 SM2 SDK 大概有这几家:
- tjfoc/gmsm:国内最早一批,社区活跃,文档多
- gmssl/go-gmssl:GMSSL 的 Go 绑定,背后是开源实现
- YangZhao/sm2:轻量实现,纯 Go,适合学习
- NCC Group 的 go-sm2:国外安全团队维护,合规性不错
- 标准库 crypto/x509 的国密扩展:部分企业 fork,不通用
我们用一张图理清它们的调用差异:
接口签名与返回值差异
我把这四个 SDK 的接口签名和返回值整理成了表格,方便你对比:
| SDK | 调用方式 | 返回类型 | 是否可指定随机源 |
|---|---|---|---|
| tjfoc/gmsm | sm2.GenerateKeyPair() | *PrivateKey + error |
否(内部 crypto/rand) |
| gmssl/go-gmssl | sm2.GenerateKey(nil) |
*sm2.PrivateKey + error |
可传 nil 或 io.Reader |
| YangZhao/sm2 | sm2.GenerateKey(rand.Reader) |
*PrivateKey + error |
必须传 io.Reader |
| NCC Group go-sm2 | sm2.GenerateKey(io.Reader) |
*ecdsa.PrivateKey + error |
必须传 io.Reader |
关键发现:只有 tjfoc 把随机源藏在了内部,其他三个 SDK 都让你显式传入随机数生成器。我个人习惯用 crypto/rand.Reader,但测试环境可以用 math/rand.New(...) 做确定化密钥。
代码示例:使用不同 SDK 生成 SM2 密钥对
我们直接看代码。以下是四种 SDK 的典型生成方式:
// 1. tjfoc/gmsm
import "github.com/tjfoc/gmsm/sm2"
priv1, err := sm2.GenerateKeyPair()
if err != nil { panic(err) }
fmt.Println("私钥字节:", priv1.D.Bytes())
// 2. gmssl/go-gmssl
import "github.com/gmssl/go-gmssl/sm2"
priv2, err := sm2.GenerateKey(nil)
if err != nil { panic(err) }
fmt.Println("私钥十进制:", priv2.D)
// 3. YangZhao/sm2
import "github.com/YangZhao/sm2"
import "crypto/rand"
priv3, err := sm2.GenerateKey(rand.Reader)
if err != nil { panic(err) }
fmt.Println("公钥点:", priv3.PublicKey.X, priv3.PublicKey.Y)
// 4. NCC Group go-sm2
import "github.com/nccgroup/go-sm2"
priv4, err := sm2.GenerateKey(rand.Reader)
if err != nil { panic(err) }
// 注意返回的是 *ecdsa.PrivateKey,需要类型断言
sm2Priv := (*sm2.PrivateKey)(priv4)
fmt.Println("SM2私钥:", sm2Priv.D)
踩坑记录:我曾经用 NCC 的 SDK,以为返回的就是 *sm2.PrivateKey,直接调用 Sign 报错。仔细一看返回的是 *ecdsa.PrivateKey,需要手动转换。后续章节我们会专门讲密钥格式转换。
参数差异:随机源与曲线
除了接口签名,还有几个参数细节值得注意:
- 随机源:只有 tjfoc 内部强依赖
crypto/rand,无法修改;其他三个都可以传入测试用随机源。我做单元测试时经常用math/rand.New(math/rand.NewSource(42))确保每次生成同一对密钥。 - 曲线对象:所有 SDK 内部都使用
sm2.P256(),但有的导出曲线,有的不导出。YangZhao 的版本允许你手动传曲线,但我不建议乱改,国密标准就是这一条曲线。 - 是否检查密钥强度:gmssl 和 NCC 会检查生成的私钥是否为零或大于曲线阶,而 tjfoc 早期版本跳过这个检查,后来修了。你用的版本如果比较老,建议手动验证一下
priv.D.Sign() != 0。
密钥序列化方式差异
生成密钥之后,下一步通常是序列化存起来或传给对方。不同 SDK 的序列化方法也不一样:
| SDK | 私钥编码 | 公钥编码 | 备注 |
|---|---|---|---|
| tjfoc/gmsm | D.Bytes() (32字节) |
GetRawPublicKey() (65字节未压缩) |
还支持 PEM 封装 |
| gmssl/go-gmssl | MarshalPKCS8PrivateKey() |
MarshalPKCS8PublicKey() |
遵循 GM/T 标准 |
| YangZhao/sm2 | D.Bytes() 需手动拼 |
Marshal(pub) 返回 65字节 |
无内置 PEM |
| NCC Group go-sm2 | 通过 x509.MarshalECPrivateKey() |
x509.MarshalPKIXPublicKey() |
复用标准库函数 |
经验之谈:序列化这块最容易出问题。我记得有次对接银行接口,对方要求私钥必须是 PKCS#8 格式,而我用的 tjfoc 默认输出的是 32 字节裸私钥。最后不得不在中间加了一层转换器。如果你涉及跨系统互调,建议统一走 PKCS#8/PEM 格式。
小结
SM2 密钥生成看似简单,实则 SDK 之间隐藏了不少差异。总结三个关键点:
- 接口签名:注意随机源参数是否存在,返回类型是原生 SM2 私钥还是 ecdsa 通用类型。
- 密钥属性:有的 SDK 公开
D字段,有的需要调用 Getter 方法,有的用K命名。 - 序列化格式:裸字节、PKCS#8、PEM 三者不能混用,必要时写适配层。
掌握了这些差异,你在切换 SDK 或对接外部系统时就能提前预判问题。下个知识点我们会深入密钥格式的互转,这也是我踩坑最多的地方。慢慢来,理解根本原因比背代码重要。