3. SM2密钥对生成原理:椭圆曲线参数、私钥与公钥的数学关系
密钥对生成,是整个SM2体系的起点。你想想看,没有一对可靠的公私钥,后面的签名验签都无从谈起。我个人习惯把密钥对生成理解成三步:选曲线、定私钥、算公钥。每一步背后都有数学原理撑着,今天咱们就把这些原理掰开揉碎。
3.1 椭圆曲线参数——一条路的说明书
SM2用的是素数域上的椭圆曲线,这条曲线在Fp上的方程为:
y² = x³ + ax + b (mod p)
看似简单,但要让它成为可用的密码学曲线,必须给出一套参数。这套参数就像盖房子的施工图纸,少了任何一项,房子就歪了。SM2的标准参数(SM2曲线)由国家密码管理局发布,业界直接复用,我们一般不用自己造。
下面这张表列出了SM2曲线的必要参数,我挑重点讲:
| 参数 | 符号 | 含义 | SM2标准值(示例) |
|---|---|---|---|
| 素数阶 | p | 定义有限域Fp的模数,决定了域的大小 | 0xFFFFFFFE…(256位) |
| 系数a | a | 曲线方程参数 | 0xFFFFFFFE…(与p有关) |
| 系数b | b | 曲线方程参数 | 0x28E9FA9E… |
| 基点 | G | 一个在曲线上的点,所有运算从此出发 | (x_G, y_G),均为256位 |
| 阶 | n | 基点G的阶,即最小的正整数n满足n·G = O | 0xFFFFFFFE…(素数) |
| 余因子 | h | 曲线阶除以n得到的整数,SM2中h=1 | 1 |
我的一点经验:当年第一次接触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写成二进制,然后逐位执行操作。
我画了一张流程图,把生成密钥对的整体关系放进去,你看一眼就懂了:
从图上你能看到:参数和随机数分别进入两个不同的处理路径,最后在标量乘法中汇合,输出公钥。而私钥到公钥的映射是单向的——因为椭圆曲线离散对数问题(ECDLP)保证:已知P和G,想求d的计算复杂度是指数级的,现代计算机连256位都破解不了。
3.4 密钥对的生成步骤(实操视角)
如果你要自己实现(不依赖库),大概是下面这几步:
- 加载参数:把SM2曲线的p, a, b, G, n, h拿到手,存成结构体。
- 校验参数:确保p是素数、曲线非奇异、G在曲线上、n是素数且n·G = 无穷远点。这一步不能跳,否则后面可能爆炸。
- 生成随机私钥:调用
crypto/rand.Reader生成256位随机数,模n得到d,若d=0则重试。 - 计算公钥:用Double-and-Add算法算出P = d * G。
- 输出密钥对:私钥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 |
密钥对生成虽然看起来代码不多,但每一行都藏着数学和安全的细节。理解这些原理后,后面讲签名和验签时你会觉得顺理成章——因为本质上它们都基于同一个椭圆曲线群。