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,不通用

我们用一张图理清它们的调用差异:

SM2 密钥生成接口对比 tjfoc/gmsm gmssl/go-gmssl YangZhao/sm2 NCC Group go-sm2 GenerateKeyPair() 返回 *PrivateKey + error sm2.GenerateKey(nil) 返回 *sm2.PrivateKey, error sm2.GenerateKey(rand.Reader) 返回 *PrivateKey, error GenerateKey(io.Reader) 返回 *ecdsa.PrivateKey, error 差异点 • 返回结构体 vs 指针 • 是否接受 io.Reader • 密钥格式: []byte 序列化方式 注意事项 • 只有 YangZhao 要求外部随机源 • NCC 返回的是 ecdsa 类型 • gmssl 需初始化上下文

接口签名与返回值差异

我把这四个 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 之间隐藏了不少差异。总结三个关键点:

  1. 接口签名:注意随机源参数是否存在,返回类型是原生 SM2 私钥还是 ecdsa 通用类型。
  2. 密钥属性:有的 SDK 公开 D 字段,有的需要调用 Getter 方法,有的用 K 命名。
  3. 序列化格式:裸字节、PKCS#8、PEM 三者不能混用,必要时写适配层。

掌握了这些差异,你在切换 SDK 或对接外部系统时就能提前预判问题。下个知识点我们会深入密钥格式的互转,这也是我踩坑最多的地方。慢慢来,理解根本原因比背代码重要。