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。
我画了张图,帮你理清关系:
用 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 文件,就知道里面装的是什么了。