3. SM2 密钥对生成:私钥与公钥的数学关系、生成步骤与随机数要求

密钥对生成,是 SM2 算法的起点。说白了,就是给你一把「私钥」和一把「公钥」。私钥你自己藏好,公钥可以公开给全世界。这两把钥匙之间,有一条数学上的单向通道——从私钥算出公钥很容易,反过来?几乎不可能。

我刚开始接触 SM2 时,总觉得这步很简单,不就是随机选个数嘛。后来在项目中踩过坑才明白——随机数质量不过关,整个密码体系就形同虚设。嗯,咱们一步步拆开看。

3.1 私钥与公钥的数学关系

SM2 基于椭圆曲线密码学。它用一条特定的椭圆曲线,定义在素数域 Fp 上。国标里这条曲线参数是固定的,叫 sm2p256v1。

核心关系就一句话:

公钥 = 私钥 × 基点 G

这里的「×」不是普通乘法,是椭圆曲线上的标量乘法(点乘)。私钥是一个大整数,基点 G 是曲线上一个公开的固定点。私钥反复自加 G,加私钥那么多次,得到的就是公钥。

为什么说它是单向的?因为椭圆曲线上的点乘,正向计算很快(用倍点算法),但反过来——给你公钥求私钥——就是椭圆曲线离散对数问题(ECDLP),目前没有多项式时间解法。这就是安全性的根基。

我记得有一次跟团队讨论,有人问:「那量子计算机来了怎么办?」嗯,ECDLP 确实扛不住 Shor 算法,但那是另一个话题了。在经典计算时代,SM2 的 256 位密钥强度,相当于 RSA 3072 位,够用。

3.2 密钥对生成步骤

生成步骤不复杂,但每一步都有坑。我按国标 GM/T 0003-2012 的流程,给你拆成 5 步:

  1. 生成随机数 d:在 [1, n-2] 范围内选一个随机整数。n 是基点的阶,一个固定的大素数。d 就是你的私钥。
  2. 计算公钥点 P:P = d × G。用椭圆曲线标量乘法算出结果。
  3. 验证 P 不是无穷远点:如果 P 是无穷远点(O),说明 d 选得不对,得重来。实际中概率极低,但规范要求检查。
  4. 输出密钥对:私钥 d(整数),公钥 P(椭圆曲线上的点,包含 x, y 坐标)。
  5. (可选)公钥压缩:为了节省存储,可以只存 x 坐标加一个符号位,接收方能还原出 y。

你看,逻辑上就这么几行。但实际编码时,我建议你注意两点:

  • 私钥 d 必须严格在 [1, n-2] 内,不能等于 0,也不能等于 n-1。国标里写得很清楚。
  • 公钥点 P 的坐标 x, y 都要在 [0, p-1] 范围内,且满足曲线方程 y² = x³ + ax + b (mod p)。

我的习惯:生成密钥对后,立刻做一次签名-验签自检。用刚生成的密钥对签一条测试消息,再验签。如果验签通过,说明密钥对没问题。这个习惯帮我抓出过好几次底层库的 bug。

3.3 随机数要求——最容易翻车的地方

讲真,密钥对生成里最关键的环节,不是椭圆曲线计算,而是随机数生成。私钥 d 必须真随机,否则整个系统就完了。

我曾经参与过一个安全审计项目,发现某厂商的 SM2 实现用了 C 语言的 rand() 函数生成私钥。结果呢?私钥只有 15 位有效随机性,几分钟就被暴力破解了。嗯,这就是典型的「随机数质量不达标」事故。

国标对随机数有明确要求,我整理成了一张表:

要求项 具体说明 违反后果
熵源质量 必须使用硬件随机数发生器或经认证的 DRBG 私钥可预测,被暴力破解
随机数范围 d ∈ [1, n-2],n 为基点阶 d=0 或 d=n-1 时,公钥可能为无穷远点
不可预测性 每次生成独立,不能复用或可推导 私钥泄露,签名可伪造
统计均匀性 通过 NIST SP 800-22 随机性测试 存在偏差,攻击者可利用侧信道

我曾经踩过的坑:在嵌入式设备上,系统刚启动时熵池还没初始化好,这时候调用随机数生成器,拿到的全是 0。连续生成了 10 个密钥对,私钥全是 0……幸好是在测试环境发现的。从那以后,我每次生成密钥对之前,都会先检查随机数发生器的状态。

3.4 密钥对生成流程图

下面这张 SVG 图,把整个流程串起来了。你可以对照着看,每一步的输入输出是什么,哪里容易出问题。

SM2 密钥对生成流程图 SM2 密钥对生成流程 步骤 1:生成随机数 d 步骤 2:P = d × G 步骤 3:验证 P ≠ 无穷远点 P = O ? 是(重来) 否 步骤 4:输出密钥对 (d, P) ⚠ 关键检查点 • d ∈ [1, n-2] • 随机数熵源合格 • P 满足曲线方程 💡 建议 生成后做签名-验签自检 确认密钥对可用

3.5 代码示例:SM2 密钥对生成(Python 伪代码)

下面我用 Python 风格的伪代码,把核心逻辑写出来。实际生产环境建议用 OpenSSL 或 GmSSL 这类成熟库,别自己造轮子。

# SM2 密钥对生成(伪代码,仅演示逻辑)
# 依赖:椭圆曲线运算库(如 gmssl-python)

def generate_sm2_keypair():
    # 1. 获取曲线参数
    curve = SM2Curve()          # sm2p256v1 曲线
    G = curve.generator         # 基点
    n = curve.order             # 基点的阶
    
    # 2. 生成随机私钥
    # 注意:必须使用密码学安全的随机数生成器
    d = secure_random_int(1, n - 2)
    
    # 3. 计算公钥
    P = scalar_mult(d, G)       # 椭圆曲线标量乘法
    
    # 4. 验证公钥不是无穷远点
    if P == INFINITY_POINT:
        raise ValueError("公钥为无穷远点,请重新生成")
    
    # 5. 验证公钥在曲线上
    if not curve.contains(P):
        raise ValueError("公钥不在曲线上,数据异常")
    
    # 6. 返回密钥对
    return {
        "private_key": d,       # 整数
        "public_key": P         # (x, y) 坐标对
    }

# 使用示例
keypair = generate_sm2_keypair()
print(f"私钥: {hex(keypair['private_key'])}")
print(f"公钥 x: {hex(keypair['public_key'][0])}")
print(f"公钥 y: {hex(keypair['public_key'][1])}")

我的建议:如果你在 C/C++ 环境下开发,优先用 GmSSL 或 OpenSSL 1.1.1+ 的 SM2 支持。别自己实现椭圆曲线运算——我见过太多因为实现细节出错导致密钥无效的案例了。嗯,信任成熟的库,把精力放在业务逻辑上。

3.6 避坑指南

最后,我把这些年遇到的坑总结一下,你写代码时对照着检查:

  • 随机数种子复用:多线程环境下,随机数生成器要加锁,或者每个线程独立实例。否则两个线程可能拿到相同的私钥。
  • 私钥存储格式:私钥 d 是整数,但存储时通常用 32 字节大端序。注意字节序,我见过有人存成小端序,结果签名验签对不上。
  • 公钥未压缩 vs 压缩:未压缩公钥 65 字节(04 + x + y),压缩公钥 33 字节(02/03 + x)。通信双方要约定好格式。
  • 测试向量验证:国标 GM/T 0003-2012 附录里提供了密钥对测试向量。生成后一定要用测试向量验证,确保实现正确。

我曾经翻过的一个车:在 ARM 平台上,硬件随机数发生器返回的随机数有字节序反转的问题。私钥 d 的高 8 位全是 0,有效位数只有 248 位。虽然安全性没降太多,但不符合国标要求。后来我加了一层字节序检查和熵源质量测试,才彻底解决。

密钥对生成,是整个 SM2 体系的基石。私钥丢了,一切归零;随机数差了,形同虚设。把这一步做扎实,后面的签名、加密才能站得住脚。