4. SM2公钥分发与证书:公钥格式与编码、X.509证书结构、自签名证书生成

公钥算法有个绕不开的问题——你怎么证明这把公钥真的是对方的?

我早年做项目时,就遇到过公钥被中间人替换的情况。那时候还没用证书体系,大家直接交换公钥文件。结果有人伪造了公钥,数据全被解密了。从那以后,我深刻理解了证书存在的意义。

这一章,我们就来聊聊SM2公钥怎么编码、X.509证书长什么样、以及如何自己动手生成一张自签名证书。

4.1 SM2公钥的格式与编码

SM2公钥本质上是一个椭圆曲线上的点,包含(x, y)两个大整数。但直接传两个大数太原始了,得有个标准格式。

4.1.1 未压缩格式

最直接的编码方式,就是把x和y坐标原样拼接。SM2的密钥长度是256位,所以x和y各占32字节。加上一个前缀字节04,总共65字节。

结构很简单:

  • 前缀:1字节,固定为04,表示未压缩格式
  • x坐标:32字节,大端序
  • y坐标:32字节,大端序

公钥编码示例(十六进制)

04
a7 8b 9c d2 3e 4f 5a 6b 7c 8d 9e 0f 1a 2b 3c 4d
5e 6f 7a 8b 9c 0d 1e 2f 3a 4b 5c 6d 7e 8f 9a 0b
c1 d2 e3 f4 a5 b6 c7 d8 e9 f0 1a 2b 3c 4d 5e 6f
7a 8b 9c 0d 1e 2f 3a 4b 5c 6d 7e 8f 9a 0b c1 d2

4.1.2 压缩格式

椭圆曲线有个特性——知道x坐标和y的奇偶性,就能算出y。所以我们可以只存x坐标加一个奇偶标记位,把65字节压缩到33字节。

压缩格式的前缀:

  • 02:y为偶数
  • 03:y为奇数

我的建议:存储和传输时优先用压缩格式,能省一半空间。但计算时记得先解压成完整坐标,否则没法做签名验算。

4.1.3 PEM编码

二进制格式虽然紧凑,但肉眼没法看,复制粘贴也容易出错。PEM编码就是把二进制数据用Base64包装一下,加上头尾标记。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

不过要注意,标准PEM格式用的是SubjectPublicKeyInfo结构,里面包含了算法标识。SM2的OID是1.2.156.10197.1.301,得正确填写。

踩坑提醒:我曾经遇到一个项目,对方把SM2公钥直接塞进RSA的PEM模板里,解析时算法OID对不上,折腾了两天才排查出来。公钥编码一定要带上正确的算法标识,别偷懒。

4.2 X.509证书结构

X.509是公钥证书的国际标准。说白了,它就是一个结构化的数据包,把公钥、身份信息、有效期、签名绑在一起。

4.2.1 证书的核心字段

字段 说明 示例
版本号 目前主流是v3 2(表示v3)
序列号 CA颁发的唯一编号 0x12345678
签名算法 签发证书用的算法 SM3withSM2
颁发者 CA的名称 CN=MyCA, O=Example
有效期 起止时间 2024-01-01 ~ 2025-01-01
主体 证书持有者名称 CN=Alice, O=Example
公钥 SM2公钥信息 04 + x + y
扩展 v3新增的附加信息 密钥用途、基本约束等
签名 CA对上述内容的签名 SM2签名值

你想想看,这就像一张身份证。照片是你的公钥,姓名是你的身份信息,发证机关是CA,有效期和防伪标识就是签名。

4.2.2 证书链与信任模型

单张证书不够用,因为你需要信任一个根。实际部署中,证书是链式结构的:

  • 根证书:自签名,信任的锚点
  • 中间证书:由根签发,用于签发终端证书
  • 终端证书:最终用户或服务器使用的证书

验证时,从终端证书往上追溯,直到信任的根证书。只要链上任何一环断了,证书就不可信。

证书链验证的核心逻辑

1. 用父证书的公钥验证子证书的签名
2. 检查子证书的有效期
3. 检查子证书的密钥用途是否匹配
4. 递归向上,直到根证书
5. 根证书必须在本地信任库中

4.3 自签名证书生成

自签名证书,就是自己给自己签发。开发测试、内部系统经常用。虽然浏览器会报警,但自己知道根在哪就行。

4.3.1 生成SM2私钥

首先得有一对SM2密钥。我用Python的gmssl库演示:

from gmssl import sm2

# 生成SM2密钥对
private_key = sm2.SM2Key.generate()
public_key = private_key.get_public_key()

# 保存私钥(PEM格式)
with open('sm2_private.pem', 'wb') as f:
    f.write(private_key.export_pem())

# 保存公钥
with open('sm2_public.pem', 'wb') as f:
    f.write(public_key.export_pem())

4.3.2 构造X.509证书

有了密钥,接下来构造证书内容。我们需要填写证书的各个字段,然后用私钥签名。

from gmssl import x509, sm2
import datetime

# 加载私钥
with open('sm2_private.pem', 'rb') as f:
    private_key = sm2.SM2Key.import_pem(f.read())

# 构造证书主体信息
cert = x509.X509Cert()
cert.set_version(2)  # v3版本
cert.set_serial_number(0x12345678)

# 设置颁发者和主体(自签名,所以相同)
issuer = x509.X509Name()
issuer.CN = 'My SM2 Root CA'
issuer.O = 'Example Corp'
cert.set_issuer(issuer)

subject = x509.X509Name()
subject.CN = 'My SM2 Root CA'
subject.O = 'Example Corp'
cert.set_subject(subject)

# 设置有效期
cert.set_not_before(datetime.datetime(2024, 1, 1))
cert.set_not_after(datetime.datetime(2029, 1, 1))

# 设置公钥
cert.set_pubkey(private_key.get_public_key())

# 添加扩展(CA标志)
cert.add_extension('basic_constraints', 'critical,CA:TRUE')
cert.add_extension('key_usage', 'critical,keyCertSign,cRLSign')

# 用私钥签名
cert.sign(private_key, 'SM3withSM2')

# 导出证书
with open('sm2_root_ca.pem', 'wb') as f:
    f.write(cert.export_pem())

个人经验:自签名证书的序列号我习惯用时间戳加随机数,避免重复。另外,CA标志一定要加,否则证书没法用来签发其他证书。

4.3.3 验证自签名证书

生成完了,得验证一下是不是真的能用:

# 加载证书
with open('sm2_root_ca.pem', 'rb') as f:
    cert = x509.X509Cert.import_pem(f.read())

# 自验证(用证书自己的公钥验证自己的签名)
if cert.verify(cert.get_pubkey()):
    print('证书验证通过')
    print(f'颁发者: {cert.get_issuer().CN}')
    print(f'有效期: {cert.get_not_before()} ~ {cert.get_not_after()}')
else:
    print('证书验证失败')

4.4 知识体系总览

说了这么多,我把这一章的核心逻辑画成了一张图。你看一眼就能明白公钥、证书、自签名之间的关系。

SM2公钥分发与证书知识体系 公钥编码 • 未压缩格式 (04 + x + y) • 压缩格式 (02/03 + x) • PEM编码 (Base64包装) X.509证书结构 • 版本/序列号/算法 • 颁发者/主体/有效期 • 公钥/扩展/签名 自签名证书 • 生成SM2密钥对 • 构造证书字段 • 自签名并验证 证书信任链 根证书(自签名) → 中间证书(由根签发) → 终端证书(由中间签发) 证书验证流程 ① 用父证书公钥验证子证书签名 ② 检查有效期、密钥用途、基本约束 ③ 递归验证直到根证书,根必须在本地信任库

重要提醒:自签名证书只适合开发和内部测试。生产环境一定要用受信任的CA签发的证书,否则客户端会报安全警告。我见过有人把自签名证书直接部署到生产环境,结果用户投诉了整整一周。

嗯,公钥编码、证书结构、自签名生成,这三块搞明白了,SM2公钥分发的基础就算打牢了。实际项目中,你还会遇到证书撤销、CRL、OCSP这些进阶话题,但那是后话了。