4、SM2密钥对生成:椭圆曲线参数与公私钥生成

这一章我们聊聊 SM2 密钥对怎么来的。说白了,密钥对就是一把私钥和一把公钥。私钥是你自己藏好的秘密,公钥可以大大方方给出去。但 SM2 用的不是普通的 RSA 那一套,它背后是一根椭圆曲线。

4.1 椭圆曲线参数:sm2p256v1

SM2 国家推荐的标准曲线叫 sm2p256v1。我最早接触国密项目时,看到这一堆参数头都大了,后来发现其实它就是个固定模板。你想想看,椭圆曲线方程是 y² = x³ + ax + b,但同样的方程配上不同的参数,对应的曲线和安全强度就完全不同。SM2 把参数定死了,大家都用同一根曲线,才能互相通信。

sm2p256v1 的核心参数如下(都是十六进制):

参数含义值(十六进制,部分截取)
p素数模FFFFFFFE FFFFFFFFFFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF
a曲线系数 aFFFFFFFE FFFFFFFFFFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC
b曲线系数 b28E9FA9E 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
我个人习惯:在代码里直接粘贴这些参数,因为我记不住。但注意千万不能写错一个字节,否则生成的密钥对无法与标准 SM2 互通。我之前有一次把 b 参数末尾抄漏了两位,结果验签一直报错,折腾了一下午。

这组参数对应一个 256 位素数域上的椭圆曲线,安全性等价于 3072 位的 RSA。SM2 选它,就是在性能和安全性之间找了个平衡点。

4.2 私钥与公钥生成原理

原理其实很简单:

  • 私钥:就是一个随机整数 d,取值范围 [1, n-2],n 是基点阶。
  • 公钥:P = d * G,也就是私钥乘以基点 G,得到椭圆曲线上的一个点。

注意这个乘法是椭圆曲线上的标量乘法,不是普通的整数乘法。它涉及到点加和倍点运算。但作为开发者,我们不需要自己实现这些数学,直接用现成的库就行。

注意: 私钥必须是真随机数。我曾经在一个项目里用了 Python 的 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 图展示了从随机数到公私钥的完整链路,这张图直接放在代码旁边帮助理解:

SM2 密钥对生成流程 安全随机数源 os.urandom / secrets 生成私钥 d d ∈ [1, n-2] 基点 G (sm2p256v1) 固定参数 标量乘: P = d * G 椭圆曲线点乘运算 公钥 P (x, y) 未压缩点 64字节 注意 私钥必须真随机 公钥推导不可逆 输出格式 私钥: 32字节 (64 hex) 公钥: 64字节 (128 hex)

4.6 小结

密钥对生成没什么神秘的,就是四个步骤:取随机数、生成私钥、拿基点、做标量乘。但每一步都有坑:随机数质量不够、曲线参数抄错、没有校验私钥范围……这些我都踩过。现在我写代码时一定会加上一句 assert 1 < private_key < n-1,再在单元测试里验证公钥是否在曲线上(用 y² ≡ x³ + ax + b (mod p) 检查)。