1. 国密SM2算法概述
1.1 SM2的来龙去脉
很多人第一次接触SM2时都会问:它和ECDSA到底有啥区别?SM2是国家密码管理局在2010年发布的椭圆曲线公钥密码算法标准,正式代号GM/T 0003-2012。说白了,它就是我们中国自己的“ECDSA变种”,但在曲线参数、签名流程和哈希使用上做了一系列调整。
我记得早年做政务系统对接时,对方要求必须用国密算法。那时候SM2的Go实现还很少,我只能从OpenSSL的C代码翻译过来,一debug就是两周。后来才发现是签名的“小r”和“大R”转换没处理好。嗯,这些坑后面会细讲。
1.2 国家标准与行业地位
SM2目前是GB/T 32918-2016系列标准(等效于GM/T 0003)。在金融、政务、能源等敏感领域,SM2已经逐步取代RSA/ECDSA成为必选算法。为什么?因为国家要求等保三级以上系统必须支持国密算法,银行网银U盾里的证书很多已经切到SM2了。
| 对比项 | SM2 | ECDSA (P-256) | RSA-2048 |
|---|---|---|---|
| 曲线基础 | SM2专用曲线 | secp256r1 | 大整数分解 |
| 密钥长度 | 256位 | 256位 | 2048位 |
| 签名长度 | 64字节 | 64字节 | 256字节 |
| 性能(签名) | 快 | 快 | 慢 |
| 合规性 | 国内强制 | 国际通用 | 逐步淘汰 |
我在做某省电子政务项目时,审计直接要求提供SM2签名验签代码的源代码级合规证明。如果你做的Go服务要过等保三级,迟早得把这套东西摸透。
1.3 椭圆曲线密码学基础
ECC的核心一句话就能说清:在椭圆曲线上找一个点G,通过私钥d算出公钥P = d * G,但从P逆向求d几乎不可能。这里用到的曲线方程是 y² = x³ + ax + b,在有限域GF(p)上运算,p是一个大素数。
为什么要用椭圆曲线?因为同等安全强度下,ECC的密钥长度远小于RSA。256位ECC ≈ 3072位RSA。你想想看,移动端App里的证书包如果塞一个2048位的RSA公钥,握手时有多大开销?
1.3.1 有限域与基点
SM2选用的曲线参数是固定的,你不需要自己生成曲线:
- 素数p = 0xFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF(256位梅森素数)
- 系数a = 0xFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC
- 系数b = 0x28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93
- 基点G = 标准点,x和y都是固定十六进制值
- 阶n = 0xFFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123(素数)
- 余因子h = 1
用Go写一下这些参数的常量定义:
const (
SM2P, _ = new(big.Int).SetString("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16)
SM2A, _ = new(big.Int).SetString("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16)
SM2B, _ = new(big.Int).SetString("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16)
// ... Gx, Gy, n 类似
)
我个人习惯把这些参数放在一个包里,后面所有签名验签都引用同一个定义,避免多次解析的误差。
1.4 SM2密钥对与参数体系
密钥生成步骤只有三步:
- 用密码学安全的随机数生成私钥d,范围 [1, n-2]
- 计算公钥P = d * G(椭圆曲线标量乘法)
- 返回 (d, P) 作为密钥对
你可能会问:为什么不直接取全部范围?因为d=0或d=n-1会导致非法点,每个规范的ECC库都会检查。我在自己的实现中加过一个断言——如果私钥生成后d*G是无穷远点,立即重试。
1.5 签名验签流程概览
签名很简单:用私钥d对消息M的摘要(SM3哈希)做一次椭圆曲线运算,输出两个整数(r, s)。验签则是用公钥P和签名(r, s)恢复出中间值R’,验证R’的x坐标是否等于r。整体流程我画成了下面的框架图——你可以把它当作战术板,后面每章代码都和这个图对应。
你看,整个流程并不复杂。但实际开发中,签名失败往往因为:消息未经SM3哈希直接传入、r或s用了错误的字节序、公钥格式不匹配。我曾经在对接一个Java写的签名服务器时,因为Go的big.Int是可变字节序而Java是固定大端,整整调了三天。后面我会专门讲如何用Go正确处理大端字节序。