SM2 密钥交换协议深度解析:从 ZA 计算到 KDF 实现的完整构造

算法原理 · 2026-08-11


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$密钥协商公开传输
关键设计:SM2 要求长期签名密钥和加密密钥分离(双证书机制),但临时密钥对$(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 不是身份标识符本身,而是其长度(比特数)。编码规则如下:

PYTHON
# 伪代码:计算 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$ 拼接:

PYTHON
# ❌ 错误:缺少 ENTL 编码
za_data = id_a + curve_a + curve_b + Gx + Gy + Ax + Ay

正确实现必须包含 ENTL:

PYTHON
# ✅ 正确:包含 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 计算将出错。

密钥交换协议流程

完整消息流

CODE
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 生成临时密钥对

步骤 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'$:使用加密私钥计算(可选)
步骤 6:响应方 B 计算共享密钥 $Z_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 伪代码

KDF 实现的工程陷阱

陷阱 1:共享密钥的提取方式

共享密钥 $Z$ 是椭圆曲线上的点,KDF 只取其 x 坐标(32 字节),而非整个点坐标。遗漏这一步会导致密钥派生错误。

陷阱 2:身份标识的顺序

$ID_A$ 和 $ID_B$ 的顺序必须严格遵循标准定义,颠倒会导致派生密钥不同。

陷阱 3:计数器编码

CT 必须编码为1字节(而非4字节或8字节),且从 0x01 开始递增。

密钥确认机制

为什么需要密钥确认?

密钥交换协议完成后,双方各自计算出共享密钥 $K$,但无法直接确认对方也计算出相同的 $K$。如果攻击者在中间篡改了临时公钥,双方可能计算出不同的密钥,导致通信失败或安全漏洞。

密钥确认的作用:通过密码学手段验证双方拥有相同的共享密钥,同时向攻击者证明"我知道这个密钥"。

密钥确认协议流程

CODE
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 密钥交换常组合使用:

CODE
密钥交换协议:协商会话密钥 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$(曲线阶),则攻击者可以构造小群攻击,泄露部分密钥信息。

验证伪代码:

PYTHON
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 握手过程中:

CODE
ClientHello: 支持 TLS_SM4_GCM_SM3 等套件
    ↓
ServerHello: 选择套件,发送服务器临时公钥 R_S
    ↓
ServerKeyExchange: 签名确认(可选)
    ↓
ClientKeyExchange: 发送客户端临时公钥 R_C
    ↓
Finished: 密钥确认完成

密钥派生链

CODE
握手共享密钥 Z
    ↓
HKDF(Z, nist_sp_800_56c)
    ↓
主密钥 PM
    ↓
HKDF(PM, ... )
    ↓
会话密钥 K_enc, K_mac

注意:国密 TLS 使用 SM3-HKDF 而非标准 TLS 的 SHA-256-HKDF,这是国密改造的关键点之一。

工程实现验证

使用 gmssl 库验证协议

完整测试向量验证

使用标准测试向量验证实现正确性:

常见实现错误

错误 1:ZA 计算遗漏 ENTL

PYTHON
# ❌ 错误实现
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:临时公钥未验证阶

PYTHON
# ❌ 错误实现:直接使用对方发送的临时公钥
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 计数器编码错误

PYTHON
# ❌ 错误实现:使用 4 字节计数器
ct_bytes = struct.pack('>I', ct)

# ✅ 正确实现:使用 1 字节计数器
ct_bytes = struct.pack('B', ct)

错误 4:混淆签名私钥与加密私钥

PYTHON
# ❌ 错误实现:使用加密私钥计算共享密钥(标准规定应使用签名私钥)
Z = scalar_multiplication(R_B, e_A)

# ✅ 正确实现:使用签名私钥计算共享密钥
Z = scalar_multiplication(R_B, d_A)

总结

SM2 密钥交换协议(GM/T 0003.3-2012)是国密算法体系中技术含量最高的子协议之一。其核心挑战在于:

  • ZA 预处理的正确性:必须严格遵循 ENTL 编码规则,确保身份绑定。
  • 临时密钥的阶验证:防止小群攻击,确保临时公钥在正确子群上。
  • KDF 的实现细节:共享密钥提取、身份顺序、计数器编码都必须精确。
  • 密钥确认的选择:高安全场景必须启用,资源受限场景可省略。
在实际工程中,建议使用经过验证的国密库(如 gmssl、Tongsuo)实现 SM2 密钥交换,避免手写实现带来的安全隐患。对于国密 TLS 部署,务必确认密码套件包含 TLS_SM4_GCM_SM3,并确保 KDF 使用 SM3 而非 SHA-256。

相关实践

参考来源

  • 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》