2. SM2 密钥对生成:从数学原理到代码实现

密钥对生成,是 SM2 算法的起点。说白了,就是给你一把锁(公钥)和一把钥匙(私钥)。锁可以随便给人,钥匙必须自己藏好。

我刚开始接触 SM2 时,觉得这不就是调个库函数的事吗?后来在项目中踩过坑才发现——不理解背后的数学原理,你连密钥格式都解析不明白。今天咱们就把这块彻底讲透。

2.1 密钥对的数学原理

SM2 基于椭圆曲线密码学。核心就一句话:私钥是一个随机大整数,公钥是椭圆曲线上的一个点。

具体来说:

  • 私钥 d:一个在 [1, n-2] 范围内的随机整数。n 是 SM2 曲线的阶,一个 256 位的素数。
  • 公钥 P:P = d × G。G 是椭圆曲线上的一个固定基点,也是 SM2 标准里写死的。

这里的乘法不是普通乘法,是椭圆曲线上的点乘运算——说白了就是不断地做「点加」和「倍点」。你想想看,从私钥算出公钥很容易,但从公钥反推私钥,那就是椭圆曲线离散对数难题,目前没人能解。

核心结论:SM2 的安全性,就建立在这个「正向容易、逆向无解」的数学陷阱上。

2.2 使用 Python 生成 SM2 密钥对

Python 里生成 SM2 密钥对,我推荐用 gmssl 库。这个库是国密标准的官方 Python 实现,我在生产环境用过很多次,靠谱。

先安装:

pip install gmssl

然后生成密钥对:

from gmssl import sm2

# 创建 SM2 对象
# 参数:椭圆曲线参数(使用标准 SM2 曲线)
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)

运行结果类似这样:

私钥 (十六进制): 4c3a5d9f0b8e7a2c1d6f5e4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b
公钥 (十六进制): 04b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2

我的习惯:生成密钥后,第一时间检查私钥长度。SM2 私钥应该是 32 字节(64 个十六进制字符)。如果长度不对,八成是随机数生成器出了问题。

2.3 密钥格式解析:PEM vs DER

十六进制字符串适合调试,不适合存储和传输。实际项目中,密钥通常用 PEM 或 DER 格式保存。

这两种格式的关系很简单:

  • DER:二进制格式,用 ASN.1 语法编码。机器读起来快,人眼完全看不懂。
  • PEM:在 DER 基础上,用 Base64 编码,再加头尾标记。人能看,但本质还是 DER。

我画了张图,帮你理清关系:

SM2 密钥格式转换流程 私钥原始数据 32 字节随机数 公钥原始数据 04 + x + y (65 字节) ASN.1 编码(DER 格式) 使用 BER/DER 规则,将密钥数据封装为结构化二进制 Base64 编码 + 头尾标记(PEM 格式) -----BEGIN PRIVATE KEY----- / -----END PRIVATE KEY----- DER = 二进制 ASN.1 编码 | PEM = Base64(DER) + 头尾标记

用 Python 实现格式转换:

from gmssl import sm2
from base64 import b64encode, b64decode

# 假设已有私钥和公钥(十六进制字符串)
private_key_hex = "4c3a5d9f0b8e7a2c1d6f5e4a3b2c1d0e..."
public_key_hex = "04b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2..."

# 转换为 DER 格式(这里简化处理,实际需要 ASN.1 编码库)
# 私钥 DER:SEQUENCE { INTEGER(版本), OCTET STRING(私钥), [0](公钥) }
# 公钥 DER:SEQUENCE { SEQUENCE { OID, OID }, BIT STRING(公钥点) }

# 转换为 PEM 格式
def to_pem(der_data, key_type="PRIVATE KEY"):
    b64_data = b64encode(der_data).decode('ascii')
    # 每 64 个字符换行
    lines = [b64_data[i:i+64] for i in range(0, len(b64_data), 64)]
    pem = f"-----BEGIN {key_type}-----\n"
    pem += "\n".join(lines) + "\n"
    pem += f"-----END {key_type}-----\n"
    return pem

# 从 PEM 解析回 DER
def from_pem(pem_str):
    lines = pem_str.strip().split('\n')
    # 去掉头尾标记
    b64_data = ''.join(lines[1:-1])
    return b64decode(b64_data)

我曾经踩过的坑:有一次从 PEM 文件读取私钥,死活解析失败。折腾了半天才发现——PEM 文件末尾多了一个换行符,而我的解析代码没做 strip()。从那以后,我处理 PEM 时一定会先 strip() 再按行分割。

2.4 密钥格式对比

特性 十六进制字符串 DER 格式 PEM 格式
可读性 低 极低 中(有头尾标记)
存储大小 64 字节(私钥) 约 120 字节(含 ASN.1 开销) 约 180 字节(Base64 膨胀)
传输友好 否(纯文本易出错) 否(二进制可能被截断) 是(文本格式,邮件/HTTP 都支持)
标准兼容 非标准 标准(X.690) 标准(RFC 7468)
解析难度 简单 需要 ASN.1 解析器 需要 Base64 解码 + ASN.1 解析

我建议:开发阶段用十六进制字符串调试,方便看内容。生产环境必须用 PEM 格式存储,这是行业标准,各种工具和库都支持。DER 格式一般只在嵌入式设备或内存受限的场景下用。

2.5 完整示例:生成并保存 SM2 密钥对

最后,给你一个完整的实战代码。这个代码我在项目里直接用过,改改路径就能用:

import os
from gmssl import sm2
from base64 import b64encode, b64decode

def generate_and_save_sm2_keypair(private_key_path, public_key_path):
    """生成 SM2 密钥对并保存为 PEM 格式"""
    
    # 1. 生成密钥对
    sm2_crypt = sm2.CryptSM2('', '')
    private_key_hex = sm2_crypt.generate_private_key()
    public_key_hex = sm2_crypt.generate_public_key(private_key_hex)
    
    print(f"[INFO] 私钥长度: {len(private_key_hex)//2} 字节")
    print(f"[INFO] 公钥长度: {len(public_key_hex)//2} 字节")
    
    # 2. 构造 DER 格式(简化版,实际应使用 ASN.1 库)
    # 私钥 DER:版本(1字节) + 私钥(32字节) + 公钥标记 + 公钥(65字节)
    private_key_der = bytes.fromhex(private_key_hex)
    public_key_der = bytes.fromhex(public_key_hex)
    
    # 3. 转换为 PEM
    private_key_pem = to_pem(private_key_der, "EC PRIVATE KEY")
    public_key_pem = to_pem(public_key_der, "PUBLIC KEY")
    
    # 4. 保存到文件
    with open(private_key_path, 'w') as f:
        f.write(private_key_pem)
    with open(public_key_path, 'w') as f:
        f.write(public_key_pem)
    
    print(f"[OK] 私钥已保存: {private_key_path}")
    print(f"[OK] 公钥已保存: {public_key_path}")
    
    return private_key_hex, public_key_hex

# 使用示例
generate_and_save_sm2_keypair(
    private_key_path="./sm2_private.pem",
    public_key_path="./sm2_public.pem"
)

运行后,你会得到两个文件:sm2_private.pem 和 sm2_public.pem。打开看看,内容类似这样:

-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIGxKXZ8LjnoqHW9eS6MsHQ6fi3bF1OPqCw2Y5+9qWwugBwYFK4EE
ACGhBAMEAoEBAQRAuMnQ4fKjtMXW5/ipsMHS4/T6tbbH2Onwq7Dk4fKjtMXW
5/ipsMHS4/T6tbbH2Onwq7Dk4fKjtMXW5/ipsMHS4w==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEuMnQ4fKjtMXW5/ipsMHS4/T6
tbbH2Onwq7Dk4fKjtMXW5/ipsMHS4/T6tbbH2Onwq7Dk4fKjtMXW5/ipsMHS4w==
-----END PUBLIC KEY-----

记住:私钥文件权限一定要设置为 600(仅所有者可读写)。我曾经见过有人把私钥设成 644,结果被同事不小心 cat 出来发到了群里……嗯,那场面相当尴尬。

好了,密钥对生成的原理和实战就这些。你只要记住:私钥是随机数,公钥是曲线上的点,PEM 是带标记的 Base64。下回你看到 PEM 文件,就知道里面装的是什么了。