3. SM2 密钥对生成:从随机数到密钥文件

说到密钥对生成,第一步就是私钥。私钥说白了就是一个随机大整数,它必须足够随机。为什么?因为如果随机数生成器出了问题,那么你的私钥就能被预测出来。

我见过有些项目用系统库的 random 模块去生成私钥——嗯,这是个灾难。SM2 私钥的随机性必须达到密码学安全级别。

3.1 私钥随机生成

私钥的取值空间是整个 [1, n-2] 区间,其中 n 是 SM2 曲线的阶。这个范围有多大?n 是 256 位的素数,所以私钥就是一个 256 位的随机数。

个人习惯:我生成私钥后,一定会校验它是否落在合法区间内。虽然概率极低,但万一随机数恰好等于 0 或 n-1,那公钥计算就会出大问题。

看段代码:

import os
from gmssl import sm2

# 生成 32 字节(256 位)的随机数作为私钥
private_key_bytes = os.urandom(32)
private_key_hex = private_key_bytes.hex()

print(f"私钥(十六进制): {private_key_hex}")

# 校验区间
n = int("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16)
private_key_int = int(private_key_hex, 16)

if private_key_int < 1 or private_key_int > n - 2:
    raise ValueError("私钥不在合法区间内,重新生成!")

print("私钥合法性校验通过")

3.2 公钥点乘计算

私钥拿到了,公钥怎么算?就是 点乘运算:P = d * G。把私钥 d 当成标量,跟椭圆曲线的基点 G 做点乘,得到的结果就是公钥 P。

这个过程是不可逆的——从公钥反推私钥需要解椭圆曲线离散对数问题,目前算力做不到。

我在项目中遇到过一个问题:点乘的实现效率直接决定了密钥生成的性能。如果你是批量生成密钥对,那点乘的优化就非常关键。

from gmssl import sm2

# 使用 SM2 库计算公钥
private_key = "0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210"
sm2_crypto = sm2.CryptSM2(public_key="", private_key=private_key)

# 点乘运算:公钥 = 私钥 * 基点G
public_key = sm2_crypto._kg(int(private_key, 16), sm2_crypto.ecc_table['g'])
public_key_hex = hex(public_key[0])[2:] + hex(public_key[1])[2:]

print(f"公钥(未压缩): {public_key_hex}")

我曾经踩过的坑:公钥的字节序有讲究。SM2 标准的坐标系是大端字节序,如果你在解析过程中搞反了字节顺序,那算出来的公钥坐标就完全不匹配。

3.3 密钥格式:十六进制 vs ASN.1 DER

密钥生成完了,存成什么格式?两种主流选择:

格式特点使用场景
十六进制字符串简单直接,可读性强开发测试、快速原型
ASN.1 DER 编码标准规范,可包含元信息生产环境、跨系统交互

十六进制格式很简单:私钥就是 64 个 hex 字符(32 字节),公钥就是 128 个 hex 字符(64 字节,未压缩的 x 和 y 坐标拼接)。

ASN.1 DER 就不一样了。它用了一套 TLV(Tag-Length-Value)的结构来编码密钥。看个例子就明白了:

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec

# 先将私钥转为 cryptography 对象
private_key_int = int(private_key_hex, 16)
# ...(省略中间步骤)...

# 导出为 PKCS8 DER 格式
private_key_der = private_key.private_bytes(
    encoding=serialization.Encoding.DER,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

print(f"私钥 DER 编码的长度: {len(private_key_der)} 字节")
print(private_key_der.hex())

# 公钥 DER 编码(SubjectPublicKeyInfo 格式)
public_key_der = public_key.public_bytes(
    encoding=serialization.Encoding.DER,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print(f"公钥 DER 编码: {public_key_der.hex()}")

核心结论:ASN.1 DER 格式虽然体积大一些(包含 OID、算法标识等),但它是标准化的。如果你要和 OpenSSL 或其他国密库交互,DER 格式是通用语言。

3.4 密钥生成全流程(SVG 图)

下面我用一张图梳理整个流程:

SM2 密钥对生成流程 系统安全随机数源 私钥 d P = d * G 点乘运算 公钥 P (x, y) 十六进制编码 ASN.1 DER 编码 说明 私钥范围: [1, n-2] 点乘不可逆

3.5 完整的密钥对生成函数

最后,我把代码封装成一个完整的函数。这样调用起来特别方便:

import os
from gmssl import sm2

def generate_sm2_keypair():
    """生成 SM2 密钥对,返回 (私钥hex, 公钥hex)"""
    # 1. 生成随机私钥
    private_key_bytes = os.urandom(32)
    private_key_hex = private_key_bytes.hex()

    # 2. 校验私钥范围
    n = int("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16)
    private_key_int = int(private_key_hex, 16)
    if not (1 <= private_key_int <= n - 2):
        return generate_sm2_keypair()  # 递归重试

    # 3. 计算公钥
    sm2_crypto = sm2.CryptSM2(public_key="", private_key=private_key_hex)
    pub_x, pub_y = sm2_crypto._kg(private_key_int, sm2_crypto.ecc_table['g'])
    public_key_hex = format(pub_x, 'x') + format(pub_y, 'x')

    return private_key_hex, public_key_hex

# 测试
sk, pk = generate_sm2_keypair()
print(f"私钥 (hex): {sk}")
print(f"公钥 (hex): {pk}")

总结一下:密钥对生成的核心就三件事——安全随机数、点乘运算、格式编码。私钥就是随机数,公钥就是点乘结果,至于存成 hex 还是 DER,看你的业务场景。

嗯,你想想看,整个流程其实不复杂,但每一步都有细节需要注意。我在实际开发中,最常见的问题就是随机数不够随机,还有就是 DER 编码时不小心写错了 OID。多测试几遍就没问题了。