4、SM2密钥对生成:椭圆曲线参数与公私钥生成
这一章我们聊聊 SM2 密钥对怎么来的。说白了,密钥对就是一把私钥和一把公钥。私钥是你自己藏好的秘密,公钥可以大大方方给出去。但 SM2 用的不是普通的 RSA 那一套,它背后是一根椭圆曲线。
4.1 椭圆曲线参数:sm2p256v1
SM2 国家推荐的标准曲线叫 sm2p256v1。我最早接触国密项目时,看到这一堆参数头都大了,后来发现其实它就是个固定模板。你想想看,椭圆曲线方程是 y² = x³ + ax + b,但同样的方程配上不同的参数,对应的曲线和安全强度就完全不同。SM2 把参数定死了,大家都用同一根曲线,才能互相通信。
sm2p256v1 的核心参数如下(都是十六进制):
| 参数 | 含义 | 值(十六进制,部分截取) |
|---|---|---|
| p | 素数模 | FFFFFFFE FFFFFFFFFFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF |
| a | 曲线系数 a | FFFFFFFE FFFFFFFFFFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC |
| b | 曲线系数 b | 28E9FA9E 9D9F5E34 4D5A9E4B CF6509A7 F39789F5 15AB8F92 DDBCBD41 4D940E93 |
| Gx | 基点 G 的 x 坐标 | 32C4AE2C 1F198119 5F990446 6A39C994 8FE30BBF F2660BE1 715A4589 334C74C7 |
| Gy | 基点 G 的 y 坐标 | BC3736A2 0F4F677C 7A6675A6 9B3EBBDA 2CF826C0 5B8C3E07 A4F6F5DC A1A4E6EF |
| n | 基点阶(素数) | FFFFFFFE FFFFFFFFFFFFFFFF FFFFFFFF 7203DF6B 21C6052B 53BBF409 39D54123 |
| h | 余因子 | 1 |
b 参数末尾抄漏了两位,结果验签一直报错,折腾了一下午。
这组参数对应一个 256 位素数域上的椭圆曲线,安全性等价于 3072 位的 RSA。SM2 选它,就是在性能和安全性之间找了个平衡点。
4.2 私钥与公钥生成原理
原理其实很简单:
- 私钥:就是一个随机整数
d,取值范围[1, n-2],n是基点阶。 - 公钥:
P = d * G,也就是私钥乘以基点 G,得到椭圆曲线上的一个点。
注意这个乘法是椭圆曲线上的标量乘法,不是普通的整数乘法。它涉及到点加和倍点运算。但作为开发者,我们不需要自己实现这些数学,直接用现成的库就行。
random.randint 来生成,结果测试环境跑出来 5 次有 2 次私钥一模一样的(因为种子问题)。后来全部改用 os.urandom + 密码学安全的随机数生成器,才稳定下来。所以这里一定用 secrets.randbelow 或 Cryptodome.Random。
4.3 Python 代码实现
我用 Python 的 gmssl 库来做密钥对生成,因为它内置了 sm2p256v1 曲线,不需要你自己配置参数。如果你在没有第三方库的环境下,也可以用 OpenSSL 的命令行,但 Python 里还是 gmssl 最顺手。
先安装:pip install gmssl
from gmssl import sm2
# 使用 sm2p256v1 标准曲线(gmssl 默认就是这根曲线)
sm2_crypt = sm2.CryptSM2(
public_key="",
private_key=""
)
# 生成密钥对
private_key = sm2_crypt.generate_private_key()
public_key = sm2_crypt.generate_public_key(private_key)
print("私钥 (十六进制):", private_key)
print("公钥 (十六进制):", public_key)
print("公钥长度:", len(public_key), "个十六进制字符")
运行后你会看到:私钥是一串 64 个十六进制字符(256 位),公钥是 128 个字符(因为包含未压缩的 x 和 y 坐标)。
不过 gmssl 的 generate_public_key 方法内部其实做了私钥校验,如果私钥为 0 或大于等于 n,它会抛异常。这个设计挺好,我遇到过自己手写椭圆曲线时没做校验,结果生成了一个无效公钥,对接方疯狂报错。
私钥 ← 安全随机数 range(1, n-1)
公钥 ← 私钥 × 基点 G (EC 标量乘法)
输出 ← 私钥 hex(64位), 公钥 hex(128位)
4.4 如果自己写生成逻辑(不依赖 gmssl)
有些同学喜欢探究底层,或者项目不允许引入太多依赖。那你得手动处理椭圆曲线点运算。下面是个精简例子(只展示关键步骤,不包含完整实现):
import os
from hashlib import sha256
# 假设你已经有了 sm2p256v1 的参数 p, a, b, Gx, Gy, n
# 以及一个实现了椭圆曲线点加、倍点、标量乘的 Point 类
def generate_keypair(curve_params):
# 生成私钥:1 到 n-2 之间的随机整数
n = curve_params['n']
private_key = int.from_bytes(os.urandom(32), 'big') % (n - 1) + 1
# 计算公钥
G = Point(curve_params['Gx'], curve_params['Gy'], curve_params['a'], curve_params['p'])
public_point = G * private_key # 标量乘
return private_key, public_point
# 实际生产不要这样写,请用标准库或经过审计的库
我自己写过一个 demo 验证过整个流程,但到了线上还是果断换成 gmssl 或 cryptography。因为椭圆曲线标量乘的侧信道攻击、时间攻击等问题,手写代码很难防御。
4.5 密钥对生成流程图
下面这张 SVG 图展示了从随机数到公私钥的完整链路,这张图直接放在代码旁边帮助理解:
4.6 小结
密钥对生成没什么神秘的,就是四个步骤:取随机数、生成私钥、拿基点、做标量乘。但每一步都有坑:随机数质量不够、曲线参数抄错、没有校验私钥范围……这些我都踩过。现在我写代码时一定会加上一句 assert 1 < private_key < n-1,再在单元测试里验证公钥是否在曲线上(用 y² ≡ x³ + ax + b (mod p) 检查)。