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公钥,握手时有多大开销?

避坑指南: 我曾经把一个未压缩公钥(65字节)直接当64字节用,验签怎么都过不了。注意:SM2公钥是64字节的x||y坐标,没有开头的0x04标识。很多库默认加0x04,一定要看文档确认格式。

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密钥对与参数体系

密钥生成步骤只有三步:

  1. 用密码学安全的随机数生成私钥d,范围 [1, n-2]
  2. 计算公钥P = d * G(椭圆曲线标量乘法)
  3. 返回 (d, P) 作为密钥对

你可能会问:为什么不直接取全部范围?因为d=0或d=n-1会导致非法点,每个规范的ECC库都会检查。我在自己的实现中加过一个断言——如果私钥生成后d*G是无穷远点,立即重试。

注意:SM2官方标准中公钥格式是64字节的x||y(大端无符号),没有压缩形式。而ECDSA的P-256通常使用33字节的压缩公钥。如果你在做跨平台对接,一定要注意对方期望的格式。

1.5 签名验签流程概览

签名很简单:用私钥d对消息M的摘要(SM3哈希)做一次椭圆曲线运算,输出两个整数(r, s)。验签则是用公钥P和签名(r, s)恢复出中间值R’,验证R’的x坐标是否等于r。整体流程我画成了下面的框架图——你可以把它当作战术板,后面每章代码都和这个图对应。

SM2签名验签核心流程 SM2参数集 p, a, b, G, n, h 用户A密钥对 私钥 dA | 公钥 PA 用户B密钥对 私钥 dB | 公钥 PB 签名者(A) 输入: 消息M, 私钥dA 输出: 签名 (r, s) 验签者(B) 输入: 消息M, 签名(r,s), 公钥PA 输出: 通过 / 失败 注意 签名长度固定64字节 消息M需先做SM3哈希

你看,整个流程并不复杂。但实际开发中,签名失败往往因为:消息未经SM3哈希直接传入、r或s用了错误的字节序、公钥格式不匹配。我曾经在对接一个Java写的签名服务器时,因为Go的big.Int是可变字节序而Java是固定大端,整整调了三天。后面我会专门讲如何用Go正确处理大端字节序。

不要把SM2签名和ECDSA签名混为一谈!SM2签名公式里多了一个“待签名消息的哈希值作为大整数参与运算”的步骤,而且最终的s值需要做一次检查(如果s = 0或s = n - r则重新计算)。这些细节正是国测局审核时盯得最紧的地方。