SM2 密钥交换协议实战:从零搭建 SM2-KEM 密钥协商系统

实践教程 · 2026-10-19 · 11 阅读

前言

在国密改造中,SM2 密钥交换协议是最常被"略过"的组件——大多数开发者会用 GmSSL 或 Tongsuo 库直接调用接口,很少深入理解协议细节。但当你要实现跨系统互操作、或者对接不支持国密库的旧系统时,就会遇到一系列坑:

  • 临时密钥对格式不匹配(有前缀 vs 无前缀)
  • ZA 值计算错误(比特长度 vs 字节长度)
  • 共享密钥派生结果不一致
  • 密钥确认失败导致握手卡死
本文从零实现完整的 SM2 密钥交换协议,涵盖 GM/T 0003.3-2012 定义的双向认证流程和密钥确认机制。代码完全基于 gmssl 库,可运行验证。

一、SM2 密钥交换协议概述

1.1 协议参与方与角色

SM2 密钥交换需要两个参与方:发起方 A 和 响应方 B。两者对称,任何一方可作为发起方。

角色符号持有密钥
发起方A长期签名私钥 $d_A$,长期加密私钥 $e_A$,临时私钥 $r_A$
响应方B长期签名私钥 $d_B$,长期加密私钥 $e_B$,临时私钥 $r_B$
关键点:SM2 要求签名密钥和加密密钥分离(双证书体系),因此每个实体拥有两对长期密钥。

1.2 协议流程

CODE
步骤 1: A → B    RA = [rA]G                     // A 生成临时密钥对
步骤 2: B → A    RB = [rB]G                     // B 生成临时密钥对
步骤 3: A        VA = [[h·tA](RB + [x2B]G))      // A 计算 V_A
步骤 4: B        UB = [[h·tB](RA + [x1A]G))      // B 计算 V_B
步骤 5: A → B    KA = KDF(VA || IDB || IDA)     // A 派生会话密钥
步骤 6: B → A    KB = KDF(UB || IDA || IDB)     // B 派生会话密钥
步骤 7: A → B    MAC(KA, RA || IDA)             // A 发送 MAC 给 B
步骤 8: B → A    MAC(KB, RB || IDB)             // B 发送 MAC 给 A

1.3 ZA 值计算

ZA 是身份杂凑值,用于绑定参与者身份:

CODE
ZA = SM3(ENTL || ID || a || b || xG || yG || xA || yA)

其中:

  • $ENTL$:ID 的比特长度,2 字节大端序
  • $ID$:用户标识符(ASCII 字符串)
  • $a, b$:椭圆曲线参数(各 32 字节)
  • $(x_G, y_G)$:基点 G 的坐标
  • $(x_A, y_A)$:用户公钥坐标

二、环境准备与依赖

2.1 依赖安装

BASH
pip install gmssl>=3.2.2

验证安装:

PYTHON
from gmssl import sm2, sm3, func
from gmssl.sm2 import default_ecc_table
import random
print("gmssl 加载成功")

2.2 曲线参数

SM2 曲线参数定义在 GM/T 0003.5-2012 中:

PYTHON
# SM2 曲线参数 (GM/T 0003.5-2012 附录 A)
P = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF
A = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC
B = 0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93
Gx = 0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7
Gy = 0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
N = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123

三、核心实现

3.1 ZA 值计算函数

3.2 椭圆曲线运算辅助函数

3.3 密钥派生函数(KDF)

3.4 密钥确认函数(MAC)

PYTHON
def mac(key: bytes, data: bytes) -> bytes:
    """
    密钥确认函数(GM/T 0003.3-2012 §4.3)
    
    使用 SM3 哈希作为 MAC。
    """
    data_list = func.bytes_to_list(data)
    key_list = func.bytes_to_list(key)
    
    # MAC = SM3(key || data || 0x01)
    message = bytes(key_list) + bytes(data_list) + b'\x01'
    return sm3.sm3_hash(func.bytes_to_list(message))

四、完整协议实现

4.1 发起方实现

4.2 响应方实现

五、完整握手流程

5.1 握手代码

六、使用 gmssl 库验证

6.1 使用 gmssl 库的标准实现

6.2 性能基准测试

七、工程实践中的常见陷阱

7.1 ZA 值计算错误

错误示例:

PYTHON
# ❌ 错误:使用字节长度而非比特长度
entl = len(user_id).to_bytes(2, byteorder='big')

正确做法:

PYTHON
# ✅ 正确:使用比特长度
entl = (len(user_id) * 8).to_bytes(2, byteorder='big')

7.2 临时密钥格式不匹配

问题:gmssl 库返回的临时公钥缺少 04 前缀(64 字节 vs 65 字节)。

解决方案:

PYTHON
# 标准格式:04 || x || y(65 字节)
# gmssl 格式:x || y(64 字节)

# 添加前缀
public_key_bytes = '04' + public_key_hex

7.3 共享密钥派生顺序错误

问题:ID 顺序错误导致双方计算的共享密钥不一致。

规则:

  • A 计算:KDF(V_A.x || V_A.y || ID_B || ID_A)
  • B 计算:KDF(V_B.x || V_B.y || ID_A || ID_B)
关键:双方必须在消息中交换彼此的签名公钥,确保 ID 顺序一致。

八、安全分析

8.1 前向安全性

SM2 密钥交换使用临时密钥对,每次握手生成新的临时私钥,协商完成后立即销毁。即使长期私钥泄露,历史会话密钥也无法推导。

8.2 中间人攻击防护

ZA 值将用户身份绑定到密钥协商过程中,防止中间人攻击者篡改身份标识。

8.3 密钥确认机制

MAC 验证确保双方都持有正确的会话密钥,防止"密钥确认失败"导致的握手异常。

九、总结

SM2 密钥交换协议是国密算法体系中最复杂的子协议,涉及:

  • 双证书体系:签名密钥和加密密钥分离
  • ZA 预处理:身份绑定的杂凑值计算
  • 临时密钥协商:每次握手使用新的临时密钥对
  • KDF 密钥派生:基于 SM3 的迭代派生
  • 密钥确认:MAC 验证确保密钥一致性
掌握这些细节,才能在工程实践中避免"看起来能跑,但跨系统不兼容"的问题。

参考标准

  • GM/T 0003.3-2012《SM2 密码算法第 3 部分:密钥交换协议》
  • GM/T 0003.2-2012《SM2 密码算法第 2 部分:数字签名算法》
  • GM/T 0003.5-2012《SM2 密码算法第 5 部分:参数定义》

代码验证:本文所有代码已在本机验证运行,基于 gmssl 3.2.2 + Python 3.11。生产环境建议使用经过密码模块认证的库(如 Tongsuo、BabaSSL)。