SM2 密钥交换协议实战:基于 GM/T 0003.3-2012 的密钥协商与 KDF 派生完整实现

实践教程 · 2026-07-09 · 15 阅读

前言

SM2 算法家族包含三个独立部分:数字签名(GM/T 0003.1)、密钥交换(GM/T 0003.3)、公钥加密(GM/T 0003.4)。其中密钥交换协议(Key Exchange Protocol, KEP)是国密体系中少有的专门针对两方密钥协商而设计的协议,不同于 TLS 中将密钥交换嵌入握手的做法。

GM/T 0003.3-2012 定义的协议可以实现:

  • 两方无需预先共享秘密即可协商出共享密钥
  • 提供基于 ID 的身份认证(双方可互相确认对方身份)
  • 通过可选的密钥确认步骤防止中间人攻击
  • 支持前向保密(通过临时密钥)
但实际工程中你可能会遇到这些困惑:ZA 预处理中的 ENTL 是什么?为什么协商前要交换临时公钥?KDF 派生的密钥如何与 SM4 对齐?本文逐一给出代码级的解答。

一、协议概述与标准结构

1.1 GM/T 0003.3-2012 的适用范围

该标准适用于 SM2 椭圆曲线公钥密码算法的密钥交换,规定了:

  • 密钥交换的基本流程(单轮 vs 认证轮次)
  • 用户其他信息(ZA)的计算方法
  • 共享密钥的计算公式
  • 密钥派生函数(KDF)的使用规范
标准基于 256 比特素数域上的椭圆曲线(与 GM/T 0003.1 共用曲线参数)。

1.2 协议参与方

协议定义两个参与方:

  • 发起方 A:主动发起密钥交换请求的一方
  • 响应方 B:接收请求并完成协商的一方
双方各自持有:
  • 长期私钥 $d_A$ / $d_B$(仅本方知道)
  • 长期公钥 $P_A = [d_A]G$ / $P_B = [d_B]G$(对方已知)
  • 唯一的标识 ID_A / ID_B(长度不超过 65535 比特)

1.3 协议完整流程

协议最终产出:协商密钥 $K$(通过 KDF 从共享点导出),以及可选的密钥确认 MAC 标签。

二、ZA 预处理:用户其他信息

2.1 ZA 的计算公式

ZA 是将用户身份绑定到密钥交换的关键步骤,防止身份替换攻击:

$$ZA = \text{SM3}\left( \text{ENTL}_A \| ID_A \| a \| b \| x_G \| y_G \| x_A \| y_A \right)$$

各参数含义:

  • $\text{ENTL}_A$:ID_A 的比特长度,以两个字节大端序表示
  • $ID_A$:用户标识(字节串)
  • $a, b$:椭圆曲线方程 $y^2 = x^3 + ax + b$ 的系数
  • $x_G, y_G$:基点 G 的坐标
  • $x_A, y_A$:用户 A 的公钥坐标

2.2 Python 实现

2.3 踩坑提醒

坑 1:ENTL 的单位是比特而非字节

GM/T 0003.3 中 ENTL 是 "比特长度",不是字节长度。如果你的 ID 是 16 字节,ENTL 应该是 128(0x0080),而不是 16。这是最常见的实现错误。

坑 2:坐标值必须补零到 32 字节

曲线上的点坐标范围是 $[0, p-1]$,其中 $p$ 是略小于 $2^{256}$ 的素数。某些点的坐标高位可能是 0x00,如果直接 int.to_bytes(32, 'big') 转换会正确补零。但如果你用的是其他库(如 cryptographypublic_numbers()),返回的整数可能不足 32 字节——必须手动检查并补零。

坑 3:ZA 依赖于完整的公钥坐标

ZA 中同时包含了发送方公钥坐标和曲线基点。如果你只持有 x 坐标(压缩格式),必须先从 x 恢复 y 坐标(取偶数 y),否则 ZA 值会错误。

三、临时密钥生成与协商计算

3.1 发起方 A 的步骤

3.2 响应方 B 的共享密钥计算

3.3 发起方 A 的共享密钥计算

类似地,发起方 A 计算:

四、KDF 密钥派生函数

4.1 KDF 的定义

GM/T 0003.3 中的 KDF 基于 SM3 哈希,采用简单的迭代构造:

$$K = \text{KDF}(Z, klen) = H_1 \| H_2 \| \cdots \| H_m$$

其中:

  • $H_i = \text{SM3}(Z \| \text{cnt}_i)$
  • $\text{cnt}_i$ 是一个 32 比特的大端序计数器,起始值为 1
  • $m = \lceil klen / 32 \rceil$(最后一段可能需要截断)

4.2 Python 实现

4.3 KDF 的选择

GM/T 0003.3 指定使用上述迭代 SM3 KDF。在实际项目中,你也可以使用:

  • GB/T 32918.3(等价于 GM/T 0003.3)中的 KDF
  • HKDF-SM3(基于 RFC 5869,使用 HMAC-SM3 作为 PRF)
关键区别:
特性GM/T 0003.3 KDFHKDF-SM3
算法SM3 迭代HMAC-SM3
salt无(Z 直接输入)支持
info无(仅 Z)支持(用于区分用途)
形式化分析标准定义HKDF 论文证明
建议:在 GM/T 0003.3 标准定义的协议中使用标准 KDF;在自研协议中使用 HKDF-SM3 获得更好的可组合安全性。

五、密钥确认(可选步骤)

5.1 为什么需要密钥确认?

标准两轮密钥交换可以抵御被动攻击,但不能完全防止主动中间人攻击。密钥确认(Key Confirmation)通过交换 MAC 标签确保双方持有相同密钥。

5.2 确认标签的计算

环境说明cryptography 42+ 的 hmac.HMAC 支持 hashes.SM3(),但由于 cryptography.hazmat.primitives.hmac 子模块只有 HMAC 类,hmac.compare_digest() 不可用(这是 stdlib hmac 的方法)。生产代码中应分开导入:from cryptography.hazmat.primitives.hmac import HMAC + from hmac import compare_digest

六、椭圆曲线辅助函数

上述代码依赖两个辅助函数,这里给出基于 gmssl 的实现:

注意:GM/T 0003.3 的完整协议实现涉及大量椭圆曲线运算细节。生产环境中建议使用完整实现库(如 python-gmss库),或参考 Tongsuo-Project 的 C 实现。上述代码为教育目的展示核心计算逻辑。

七、前向保密与安全性分析

7.1 前向保密(Forward Secrecy)

GM/T 0003.3 通过临时密钥 $r_A$ / $r_B$ 提供前向保密:

  • 每次交换使用独立的临时密钥对
  • 即使攻击者获取长期私钥 $d_A$(或 $d_B$),也无法计算过去的共享密钥
  • 因为共享密钥依赖于 $r_A$ 和 $r_B$,而临时私钥在交换完成后被销毁
实际部署建议
  • 临时密钥对每次交换必须重新生成(不能复用)
  • 临时私钥在内存中安全擦除(Python 中使用 del + 依赖 GC,C 中使用 memset_s
  • 确保随机数生成器为密码学安全(/dev/urandomCryptGenRandom

7.2 中间人攻击防护

标准协议通过以下机制防御中间人攻击:

  • ZA 绑定:将用户 ID 和公钥绑定到共享密钥计算中
  • 双向认证:双方互相计算 ZA,中间人无法伪造
  • 可选确认标签:S1/SA 标签验证密钥一致性

7.3 常见攻击面

攻击类型风险等级缓解措施
临时密钥复用每次交换后销毁临时私钥
RA/RB 不在曲线上验证接收的点在曲线上
小子群攻击验证接收点不是无穷远点
时间侧信道使用常量时间标量乘法

八、实战:完整密钥交换示例

将上述步骤整合为一个完整的可运行示例:

九、常见错误与排错清单

9.1 协商出的密钥不一致

  • 原因 1:ZA 计算时 ENTL 字节序错误(小端 vs 大端)
  • 原因 2:坐标值未补零到 32 字节
  • 原因 3:w 计算错误(应为 128 而非 127)
  • 原因 4:KDF 计数器起始值错误(应从 1 开始,不是 0)

9.2 "Temporary point not on curve"

接收方验证临时公钥不在椭圆曲线上。检查:

  • 坐标确实在素数域 $[0, p-1]$ 内
  • 坐标满足曲线方程 $y^2 \equiv x^3 + ax + b \pmod{p}$

9.3 前向保密失效

  • 临时密钥复用:确保每次交换生成新的 $(r, R)$
  • 内存泄露:临时私钥在交换完成后应立即从内存中清除

十、总结

GM/T 0003.3-2012 定义的 SM2 密钥交换协议是一个精巧的两方协商协议,通过 ZA 预处理实现身份绑定,通过临时密钥提供前向保密,通过可选的确认标签防御中间人攻击。

在实际部署中请务必注意:

  • ENTL 必须是比特长度,不是字节长度
  • 坐标必须补零到 32 字节,保证 ZA 计算一致
  • w = 128(对于 256-bit SM2 曲线)
  • 临时密钥每次必须重新生成,确保前向保密
  • 生产代码需完整椭圆曲线运算,不要自行实现所有底层运算

参考来源

  • GM/T 0003.3-2012《SM2 椭圆曲线公钥密码算法 第 3 部分:密钥交换协议》
  • GM/T 0003.1-2012《SM2 椭圆曲线公钥密码算法 第 1 部分:总则》
  • GM/T 0004-2012《SM3 密码杂凑算法》
  • GB/T 32918.3-2016(国家标准版 SM2 第 3 部分)
  • GMSSL 开源实现

*本文实现代码基于 Python 3.10 + cryptography 42.x(SM3/SM4 支持)+ gmssl 3.2.x(SM2 底层运算)。部分辅助函数为生产环境伪代码,需根据实际椭圆曲线库调整。*