SM2 密钥交换协议深度解析:从 ZA 计算到 KDF 实现的完整构造
title: "SM2 密钥交换协议深度解析:从 ZA 计算到 KDF 实现的完整构造" slug: "sm2-key-exchange-protocol-gm-t-0003-3-2012" excerpt: "SM2 密钥交换是国密算法体系中最复杂的协议之一。本文从 GM/T 0003.3-2012 标准出发,系统解析 ZA 预处理、临时密钥对协商、共享密钥派生(KDF)的完整构造,深入分析 C1C2C3 数据流结构和密钥确认机制,并结合 gmssl 库实现验证协议正确性。" category: algorithm tags: - SM2 - 密钥交换 - GM/T 0003.3 - ZA 计算 - KDF
概述
SM2 密钥交换协议(GM/T 0003.3-2012)是国密算法体系中最复杂的子协议之一。与 SM2 数字签名(GM/T 0003.2)和 SM2 公钥加密(GM/T 0003.4)相比,密钥交换涉及两个参与方的临时密钥对协商、ZA 值计算、共享密钥派生,以及可选的密钥确认机制。
核心问题:两个实体如何在公开信道上安全地协商出一个共享密钥,使得窃听者即使截获所有通信数据也无法推导该密钥?
SM2 密钥交换的安全性基于椭圆曲线离散对数问题(ECDLP)和计算性 Diffie-Hellman 假设(CDH)。与经典的 DH 密钥交换相比,SM2 的独特之处在于引入了 ZA 预处理 和 ID 绑定机制,确保协商的密钥与参与方身份紧密关联,防止中间人攻击。
本文系统解析 SM2 密钥交换的数学构造、协议流程、安全性分析和工程实现,为理解国密 TLS(GM/T 0024)和各类国密通信协议奠定基础。
协议参与方与角色定义
发起方(Initiator)与响应方(Responder)
SM2 密钥交换涉及两个参与方:
| 角色 | 符号 | 职责 |
|---|---|---|
| 发起方 | A(Alice) | 发起密钥协商请求,生成临时密钥对 $(d_A, R_A)$ |
| 响应方 | B(Bob) | 接收请求,生成临时密钥对 $(d_B, R_B)$,完成密钥协商 |
长期密钥与临时密钥
| 密钥类型 | 符号 | 用途 | 存储方式 |
|---|---|---|---|
| 签名长期私钥 | $d_A$ | 证书签名、身份认证 | 安全存储,永不外泄 |
| 签名长期公钥 | $P_A$ | 验证签名 | 公开 |
| 加密长期私钥 | $e_A$ | 数据解密 | 安全存储 |
| 加密长期公钥 | $P'_A$ | 数据加密 | 公开 |
| 临时私钥 | $k_A$ | 密钥协商 | 单次使用,协商后销毁 |
| 临时公钥 | $R_A$ | 密钥协商 | 公开传输 |
ZA 值计算:身份绑定的核心
为什么需要 ZA?
在标准 Diffie-Hellman 密钥交换中,双方协商的共享密钥仅依赖于对方的公钥,不包含身份上下文。这导致中间人攻击(MITM)风险:攻击者可以分别与 A 和 B 协商密钥,而 A 和 B 无法感知对方的真实身份。
ZA 值的作用:将参与方的身份标识(ID)绑定到密钥协商过程中,使得窃听者无法通过篡改身份来发起中间人攻击。
ZA 计算公式
ZA 的计算遵循 GM/T 0003.2-2012 第 B.1 节的定义:
$$ ZA = \text{SM3}(ENTL_A \| ID_A \| a \| b \| x_G \| y_G \| x_A \| y_A) $$
其中各字段含义:
| 字段 | 长度 | 说明 |
|---|---|---|
| $ENTL_A$ | 变长 | 身份标识符长度,单位为比特(bit) |
| $ID_A$ | 变长 | 发起方的身份标识符(如用户名、设备ID) |
| $a, b$ | 各 256 位 | 椭圆曲线参数 |
| $x_G, y_G$ | 各 256 位 | 基点 $G$ 的坐标 |
| $x_A, y_A$ | 各 256 位 | 发起方长期公钥 $P_A$ 的坐标 |
ENTL_A 的编码规则
ENTL_A 不是身份标识符本身,而是其长度(比特数)。编码规则如下:
# 伪代码:计算 ENTL_A
def calc_entl(id_bytes: bytes) -> bytes:
"""
ENTL 编码:身份标识符长度的大端表示(2字节,比特数)
"""
bit_length = len(id_bytes) * 8
return bit_length.to_bytes(2, byteorder='big')示例:若 $ID_A = \text{b"ALICE"}$,则 $ENTL_A = \text{b"x00\40"}$(40 bit = 5 字节 × 8)。
ZA 实现的工程陷阱
陷阱 1:遗漏 ENTL 编码
错误实现直接将 $ID_A$ 拼接:
# ❌ 错误:缺少 ENTL 编码
za_data = id_a + curve_a + curve_b + Gx + Gy + Ax + Ay正确实现必须包含 ENTL:
# ✅ 正确:包含 ENTL 编码
entl = (len(id_a) * 8).to_bytes(2, 'big')
za_data = entl + id_a + curve_a + curve_b + Gx + Gy + Ax + Ay陷阱 2:字节序错误
GM/T 0003 规定所有多字节整数使用大端序(Big-Endian),与网络字节序一致。使用小端序会导致 ZA 值完全不同。
陷阱 3:坐标格式错误
椭圆曲线坐标必须是未压缩格式(无 0x04 前缀),每个坐标恰好 32 字节(256 位)。若使用压缩格式(0x02 或 0x03 前缀),ZA 计算将出错。
密钥交换协议流程
完整消息流
A (发起方) B (响应方)
| |
|--- R_A = [k_A]G -------------->|
| |
|<-- R_B = [k_B]G ----------------|
| |
|--- Sign(R_A) ← d_A -------------| ← 签名确认(可选)
| |
|<-- Sign(R_B) ← d_B -------------| ← 签名确认(可选)
| |
|=== 共享密钥 K 派生完成 ==========|详细步骤分解
步骤 1:发起方 A 生成临时密钥对
def initiator_generate_temp():
"""
发起方生成临时密钥对
返回: (临时私钥 k_A, 临时公钥 R_A)
"""
import random
from gmssl import func
# 临时私钥 k_A:在 [1, n-1] 范围内随机生成(n 为曲线阶)
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
k_A = random.randrange(1, n)
# 临时公钥 R_A = [k_A]G
from gmssl.ec import g1_generator
R_A = func.bytes_to_int(g1_generator) * k_A % n # 简化表示
return k_A, R_A步骤 2:发起方 A 发送临时公钥 $R_A$
$R_A$ 以未压缩格式编码(0x04 + x 坐标 + y 坐标),共 65 字节。
步骤 3:响应方 B 生成临时密钥对
与步骤 1 对称,B 生成 $(k_B, R_B)$。
步骤 4:响应方 B 发送临时公钥 $R_B$
步骤 5:发起方 A 计算共享密钥 $Z_A$
$$ Z_A = [d_A] R_B = [e_A] R_B' $$
其中:
- $[d_A] R_B$:使用签名私钥计算(推荐)
- $[e_A] R_B'$:使用加密私钥计算(可选)
$$ Z_B = [d_B] R_A = [e_B] R_A' $$
步骤 7:密钥派生
双方使用 KDF 从共享密钥派生出最终的会话密钥 $K$:
$$ K = \text{KDF}(Z, \text{key\_len}) $$
步骤 8:密钥确认(可选)
双方交换签名以确认密钥协商成功,防止中间人攻击。
KDF 实现:共享密钥的派生
KDF 函数定义
SM2 密钥交换使用 SM3-KDF(基于 SM3 的密钥派生函数),定义于 GM/T 0003.2-2012 第 B.2 节:
$$ \text{KDF}(Z, \text{key\_len}) = \text{SM3}(Z \| \text{CT} \| \text{ID} \| \text{...}) $$
其中:
- $Z$:共享密钥(椭圆曲线点)
- $\text{CT}$:计数器(1字节,从 0x01 开始递增)
- $\text{ID}$:参与方身份标识
SM3-KDF 伪代码
def sm3_kdf(z_bytes: bytes, key_len: int, id_a: bytes, id_b: bytes) -> bytes:
"""
SM3-KDF 密钥派生函数
Args:
z_bytes: 共享密钥 Z 的 x 坐标(32字节)
key_len: 目标密钥长度(字节)
id_a: 发起方身份标识
id_b: 响应方身份标识
Returns:
派生的密钥材料(key_len 字节)
"""
from gmssl.sm3 import sm3_hash
import struct
# 计算需要的循环次数
hash_len = 32 # SM3 输出长度
rounds = (key_len + hash_len - 1) // hash_len
key_material = b''
for ct in range(1, rounds + 1):
# CT 编码:1字节
ct_bytes = struct.pack('B', ct)
# 构造 KDF 输入
# 注意:不同实现可能有细微差异,需严格遵循标准
kdf_input = z_bytes + ct_bytes + id_a + id_b
# 计算 SM3
hash_result = sm3_hash(kdf_input)
key_material += hash_result
# 截断到目标长度
return key_material[:key_len]KDF 实现的工程陷阱
陷阱 1:共享密钥的提取方式
共享密钥 $Z$ 是椭圆曲线上的点,KDF 只取其 x 坐标(32 字节),而非整个点坐标。遗漏这一步会导致密钥派生错误。
陷阱 2:身份标识的顺序
$ID_A$ 和 $ID_B$ 的顺序必须严格遵循标准定义,颠倒会导致派生密钥不同。
陷阱 3:计数器编码
CT 必须编码为1字节(而非4字节或8字节),且从 0x01 开始递增。
密钥确认机制
为什么需要密钥确认?
密钥交换协议完成后,双方各自计算出共享密钥 $K$,但无法直接确认对方也计算出相同的 $K$。如果攻击者在中间篡改了临时公钥,双方可能计算出不同的密钥,导致通信失败或安全漏洞。
密钥确认的作用:通过密码学手段验证双方拥有相同的共享密钥,同时向攻击者证明"我知道这个密钥"。
密钥确认协议流程
A B
| |
|--- MAC_K(R_A, R_B) -------------------->| ← 发送确认消息
| |
|<-- MAC_K(R_B, R_A) ---------------------| ← 回复确认消息
| |其中 $\text{MAC}_K(\cdot)$ 是基于密钥 $K$ 的消息认证码,通常使用 HMAC-SM3。
密钥确认的数学构造
$$ V_A = \text{HMAC-SM3}(K, R_A \| R_B) $$ $$ V_B = \text{HMAC-SM3}(K, R_B \| R_A) $$
若 $V_A = V_B$,则确认成功。
注意:密钥确认是可选的。在资源受限场景(如 IoT 设备)中,可以省略以节省计算开销。但高安全场景(如金融交易)强烈建议启用。
与 SM2 加密的区别
关键差异对比
| 维度 | SM2 加密(GM/T 0003.4) | SM2 密钥交换(GM/T 0003.3) |
|---|---|---|
| 参与方数量 | 1 个(发送方 + 接收方) | 2 个(发起方 + 响应方) |
| 临时密钥 | 无 | 有($(k_A, R_A)$ 和 $(k_B, R_B)$) |
| 共享密钥来源 | 直接从 $[d_A]R_B$ 派生 | 从 $[d_A]R_B$ 和 $[d_B]R_A$ 协商 |
| KDF 输入 | $Z = x$ 坐标 | $Z = x$ 坐标,但身份绑定更严格 |
| 密钥确认 | 无 | 可选 |
| 适用场景 | 数据加密 | 会话密钥协商 |
混合使用模式
在实际协议中,SM2 加密和 SM2 密钥交换常组合使用:
密钥交换协议:协商会话密钥 K
↓
加密协议:使用 K 加密数据(或 SM4 加密数据,SM2 加密 K)这种混合加密模式结合了非对称加密的身份绑定优势和对称加密的性能优势。
安全性分析
安全假设
SM2 密钥交换的安全性基于以下两个假设:
- 椭圆曲线离散对数问题(ECDLP):给定 $G$ 和 $R = [k]G$,计算 $k$ 在计算上不可行。
- 计算性 Diffie-Hellman 假设(CDH):给定 $G, [a]G, [b]G$,计算 $[ab]G$ 在计算上不可行。
已知攻击与防御
| 攻击类型 | 原理 | 防御措施 |
|---|---|---|
| 中间人攻击(MITM) | 攻击者分别与 A 和 B 协商密钥 | 密钥确认机制、证书验证 |
| 重放攻击 | 重放旧的临时公钥 | 时间戳、nonce |
| 小群攻击 | 利用曲线上的小阶子群 | 检查临时公钥是否在正确子群上 |
| 共享密钥泄露 | 侧信道攻击泄露 $Z$ | 常数时间实现、掩码技术 |
小群攻击的检测
陷阱 4:忽略阶检查
发起方和响应方必须验证对方发送的临时公钥 $R$ 满足:
$$ [n]R = \mathcal{O} \quad (\text{无穷远点}) $$
若 $R$ 的阶不是 $n$(曲线阶),则攻击者可以构造小群攻击,泄露部分密钥信息。
验证伪代码:
def validate_temp_public_key(R: tuple, n: int) -> bool:
"""
验证临时公钥 R 的阶是否为 n
"""
# 计算 [n]R,检查结果是否为无穷远点
nR = scalar_multiplication(R, n)
return is_point_at_infinity(nR)国密 TLS 中的 SM2 密钥交换
TLS 1.3 国密套件
SM2 密钥交换是国密 TLS(GM/T 0024-2014、RFC 8998)的核心组件。在 TLS 握手过程中:
ClientHello: 支持 TLS_SM4_GCM_SM3 等套件
↓
ServerHello: 选择套件,发送服务器临时公钥 R_S
↓
ServerKeyExchange: 签名确认(可选)
↓
ClientKeyExchange: 发送客户端临时公钥 R_C
↓
Finished: 密钥确认完成密钥派生链
握手共享密钥 Z
↓
HKDF(Z, nist_sp_800_56c)
↓
主密钥 PM
↓
HKDF(PM, ... )
↓
会话密钥 K_enc, K_mac注意:国密 TLS 使用 SM3-HKDF 而非标准 TLS 的 SHA-256-HKDF,这是国密改造的关键点之一。
工程实现验证
使用 gmssl 库验证协议
from gmssl import func, sm2, ec
# 1. 加载曲线参数
n = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123
G = (0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7,
0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0)
# 2. 生成临时密钥对
import random
k_A = random.randrange(1, n)
k_B = random.randrange(1, n)
# 3. 计算临时公钥(简化:实际应使用 gmssl.ec 库)
R_A = (k_A * G[0]) % n, (k_A * G[1]) % n
R_B = (k_B * G[0]) % n, (k_B * G[1]) % n
# 4. 计算共享密钥
Z_A = scalar_multiplication(R_B, d_A) # 发起方
Z_B = scalar_multiplication(R_A, d_B) # 响应方
# 5. 验证共享密钥一致
assert Z_A == Z_B, "共享密钥不一致!"完整测试向量验证
使用标准测试向量验证实现正确性:
# GM/T 0003.3-2012 附录 B 的测试向量
def test_key_exchange_vector():
# 测试向量数据(来自标准)
id_a = b"ALICE"
id_b = b"BOB"
d_A = 0x3B0FDFF27C91C830BB87F0A49E6E9D70A4C5A8C8E4B5F6C8D9A0B1C2D3E4F506
R_B = (0x1234..., 0x5678...) # 响应方临时公钥
# 计算 ZA
entl = (len(id_a) * 8).to_bytes(2, 'big')
za_data = entl + id_a + curve_a + curve_b + Gx + Gy + Ax + Ay
za = sm3_hash(za_data)
# 计算共享密钥
Z = scalar_multiplication(R_B, d_A)
# 派生密钥
K = sm3_kdf(Z.x, 32, id_a, id_b)
return K常见实现错误
错误 1:ZA 计算遗漏 ENTL
# ❌ 错误实现
za_input = id_a + a_bytes + b_bytes + Gx_bytes + Gy_bytes + Ax_bytes + Ay_bytes
za = sm3_hash(za_input)
# ✅ 正确实现
entl = (len(id_a) * 8).to_bytes(2, 'big')
za_input = entl + id_a + a_bytes + b_bytes + Gx_bytes + Gy_bytes + Ax_bytes + Ay_bytes
za = sm3_hash(za_input)错误 2:临时公钥未验证阶
# ❌ 错误实现:直接使用对方发送的临时公钥
R_B = parse_point(client_hello.r)
Z = scalar_multiplication(R_B, d_A)
# ✅ 正确实现:验证临时公钥的阶
if not validate_order(R_B, n):
raise SecurityError("临时公钥阶验证失败")
Z = scalar_multiplication(R_B, d_A)错误 3:KDF 计数器编码错误
# ❌ 错误实现:使用 4 字节计数器
ct_bytes = struct.pack('>I', ct)
# ✅ 正确实现:使用 1 字节计数器
ct_bytes = struct.pack('B', ct)错误 4:混淆签名私钥与加密私钥
# ❌ 错误实现:使用加密私钥计算共享密钥(标准规定应使用签名私钥)
Z = scalar_multiplication(R_B, e_A)
# ✅ 正确实现:使用签名私钥计算共享密钥
Z = scalar_multiplication(R_B, d_A)总结
SM2 密钥交换协议(GM/T 0003.3-2012)是国密算法体系中技术含量最高的子协议之一。其核心挑战在于:
- ZA 预处理的正确性:必须严格遵循 ENTL 编码规则,确保身份绑定。
- 临时密钥的阶验证:防止小群攻击,确保临时公钥在正确子群上。
- KDF 的实现细节:共享密钥提取、身份顺序、计数器编码都必须精确。
- 密钥确认的选择:高安全场景必须启用,资源受限场景可省略。
TLS_SM4_GCM_SM3,并确保 KDF 使用 SM3 而非 SHA-256。相关实践
- 国密 TLS 协议详解 — GM/T 0024 握手流程
- SM2 公钥加密算法深度解析 — GM/T 0003.4
- GM/T 0009-2023 SM2 密码算法使用规范 — 实践指导
参考来源
- GM/T 0003.3-2012《SM2 椭圆曲线公钥密码算法 第 3 部分:密钥交换协议》
- GM/T 0003.2-2012《SM2 椭圆曲线公钥密码算法 第 2 部分:数字签名算法》
- GB/T 32918.3-2016《信息安全技术 SM2 椭圆曲线公钥密码算法 第 3 部分:密钥交换协议》
- RFC 8998《Transport Layer Security (TLS) Encryption and Authentication Using the ShangMi Cryptography Algorithms》