3. SM2密钥对生成原理:椭圆曲线参数、私钥与公钥的数学关系

密钥对生成,是整个SM2体系的起点。你想想看,没有一对可靠的公私钥,后面的签名验签都无从谈起。我个人习惯把密钥对生成理解成三步:选曲线、定私钥、算公钥。每一步背后都有数学原理撑着,今天咱们就把这些原理掰开揉碎。

3.1 椭圆曲线参数——一条路的说明书

SM2用的是素数域上的椭圆曲线,这条曲线在Fp上的方程为:

y² = x³ + ax + b  (mod p)

看似简单,但要让它成为可用的密码学曲线,必须给出一套参数。这套参数就像盖房子的施工图纸,少了任何一项,房子就歪了。SM2的标准参数(SM2曲线)由国家密码管理局发布,业界直接复用,我们一般不用自己造。

下面这张表列出了SM2曲线的必要参数,我挑重点讲:

参数符号含义SM2标准值(示例)
素数阶p定义有限域Fp的模数,决定了域的大小0xFFFFFFFE…(256位)
系数aa曲线方程参数0xFFFFFFFE…(与p有关)
系数bb曲线方程参数0x28E9FA9E…
基点G一个在曲线上的点,所有运算从此出发(x_G, y_G),均为256位
阶n基点G的阶,即最小的正整数n满足n·G = O0xFFFFFFFE…(素数)
余因子h曲线阶除以n得到的整数,SM2中h=11

我的一点经验:当年第一次接触SM2时,我总以为这些参数随便选就好,结果测试时发现公钥点不在曲线上——后来意识到参数里任何一个字节抄错,整个密钥对就废了。所以啊,参数一定要从官方文档一字不差地复制,别手敲。

这些参数里,p和n的位数直接决定了安全强度。SM2的p是256位素数,对应的安全等级大概和RSA 3072-bit相当。而b的取值要保证曲线非奇异(判别式不为0),否则椭圆曲线的群结构就坏了。

3.2 私钥——一个随机的「门票」

私钥,说白了就是一个随机大整数。我在项目中习惯直接拿密码学安全随机数生成器(CSPRNG)造一个[1, n-1]范围内的数,记作d。

注意,这个随机数必须真随机,不能用伪随机比如math/rand。我曾经在一个早期项目里偷懒用了math/rand,结果每次重启进程私钥都一样……那场景,现在想起来还后背发凉。

私钥d的数学约束:

  • 1 ≤ d ≤ n-1
  • d 必须随机均匀分布
  • d 的长度通常等于n的比特长度(256位)

你可能会问:“私钥为什么要限制小于n?”因为当d≥n时,d·G会绕回到原点,安全性也会退化。所以这一步是硬性规定,谁不守谁吃亏。

3.3 公钥——私钥乘以基点

公钥的计算公式非常简单:

P = d * G

这里的乘法是椭圆曲线上的标量乘法(Scalar Multiplication),不是普通整数乘法。它表示把基点G自己在曲线上连加d次:

d·G = G + G + G + … + G (d个G相加)

听着是不是有点像复利?但实际上因为曲线上点的加法有几何意义(做切线与对称),这个过程不能直接暴力加d次——否则256位的d算到宇宙毁灭也出不来结果。实际用的是双倍-加算法(Double-and-Add),把d写成二进制,然后逐位执行操作。

我画了一张流程图,把生成密钥对的整体关系放进去,你看一眼就懂了:

SM2密钥对生成流程与数学关系 椭圆曲线参数 • p (素数模数) • a, b (曲线系数) • G (基点坐标) • n (基点阶) 随机数发生器 CSPRNG 真随机 私钥 d 1 ≤ d ≤ n-1 标量乘法运算 P = d * G Double-and-Add 算法,逐位迭代 输出:椭圆曲线上的点 (x_P, y_P) 公钥 P (x_P, y_P) 在曲线上 注意: • d * G 不可逆推 • 已知P求d是椭圆曲线 离散对数难题(ECDLP)

从图上你能看到:参数和随机数分别进入两个不同的处理路径,最后在标量乘法中汇合,输出公钥。而私钥到公钥的映射是单向的——因为椭圆曲线离散对数问题(ECDLP)保证:已知P和G,想求d的计算复杂度是指数级的,现代计算机连256位都破解不了。

3.4 密钥对的生成步骤(实操视角)

如果你要自己实现(不依赖库),大概是下面这几步:

  1. 加载参数:把SM2曲线的p, a, b, G, n, h拿到手,存成结构体。
  2. 校验参数:确保p是素数、曲线非奇异、G在曲线上、n是素数且n·G = 无穷远点。这一步不能跳,否则后面可能爆炸。
  3. 生成随机私钥:调用crypto/rand.Reader生成256位随机数,模n得到d,若d=0则重试。
  4. 计算公钥:用Double-and-Add算法算出P = d * G。
  5. 输出密钥对:私钥d存为32字节大端整数,公钥P通常压缩或非压缩编码。

我曾经踩过的坑:有一次我用了自己写的Double-and-Add实现,结果在边界情况(d=1或d=n-1)时出了bug,公钥算成无穷远点。后来强制在计算前做一次d mod n的归一化才解决。我的建议是:除非你有充足的理由,否则直接用标准库或经过验证的SM2实现,别自己手写标量乘法。

3.5 公钥的两种表示

公钥是曲线上的一个点,通常有两种表示方式:

  • 非压缩格式:前缀0x04 + 32字节x坐标 + 32字节y坐标,共65字节。完整保留坐标信息,但是多了1个字节。
  • 压缩格式:前缀0x02或0x03 + 32字节x坐标,共33字节。根据y的奇偶性选择前缀(0x02偶、0x03奇),接收方通过曲线方程重新算出y。省了32字节,但要多一次模平方根运算。

我个人偏好非压缩格式,简单直接,调试时一眼就能看出坐标值。但在带宽敏感的环境(比如物联网),压缩格式更实用。

3.6 小结与避坑清单

再啰嗦几句我总结的避坑经验:

场景常见坑解决方案
参数加载抄错了参数值,导致公钥不在曲线上从官方文档逐字节验证,最好写一个自动校验函数
随机数生成用了伪随机生成器,私钥可预测强制使用crypto/rand.Reader,测试用固定种子仅限单元测试
私钥范围d=0或d≥n,导致公钥为无穷远点生成后检查1 ≤ d ≤ n-1
公钥验证公钥未做二次验证,后续签名验签出invalid point验签或加密前先检查公钥点是否在曲线上且阶为n

密钥对生成虽然看起来代码不多,但每一行都藏着数学和安全的细节。理解这些原理后,后面讲签名和验签时你会觉得顺理成章——因为本质上它们都基于同一个椭圆曲线群。