3. 密钥派生原理:KDF 的定义、规范与安全要求

密钥派生函数,简称 KDF,是 SM2 算法体系中一个容易被忽视、但极其关键的环节。说白了,它的任务就是:把一份共享的秘密,变成你真正能用的密钥。

我早年做嵌入式安全方案时,就踩过 KDF 的坑。当时觉得“不就是算个哈希嘛”,结果产品在互操作性测试中直接翻车。嗯,从那以后,我对 KDF 的每一个字节都不敢马虎。

3.1 什么是 KDF?为什么需要它?

SM2 密钥交换协议完成后,通信双方会得到一个共享的椭圆曲线点 (x, y)。这个点是一个大整数对,长度通常在 512 位以上。但实际加密、签名或会话保护,需要的往往是 128 位、192 位或 256 位的对称密钥。

你想想看,直接把坐标 x 截断当密钥用?不行。一来长度不匹配,二来椭圆曲线点的坐标并不是均匀随机的,直接截断会有安全风险。

KDF 的作用就是:将输入的共享秘密(以及可能的上下文信息),通过一个确定的、安全的算法,扩展或压缩成任意长度的密钥材料。

核心原则:KDF 必须保证输出在计算上不可与随机数区分。哪怕输入只差一个比特,输出也必须完全不一样。

3.2 SM2 KDF 算法规范

SM2 使用的 KDF 定义在 GB/T 32918.1-2016 中。它基于 SM3 哈希函数,采用了一种经典的“计数器模式”构造。

我个人习惯把它的流程总结为三步:

  1. 初始化:确定输出密钥长度 klen(比特数),计算需要的迭代次数 n = ceil(klen / v),其中 v 是 SM3 的输出长度(256 位)。
  2. 迭代计算:对 i = 1 到 n,计算 Ha(i) = SM3(Z || ct(i))。其中 Z 是输入的共享秘密(通常包含坐标 x 和 y 的拼接),ct(i) 是 32 位大端表示的计数器。
  3. 截断输出:将 Ha(1) || Ha(2) || ... || Ha(n) 拼接,取前 klen 位作为最终密钥。

用代码表示会更直观:

// SM2 KDF 核心实现(伪代码)
// 输入:Z - 共享秘密(字节串)
//       klen - 期望输出的密钥长度(比特)
// 输出:K - 派生密钥(字节串)

function SM2_KDF(Z, klen):
    v = 256               // SM3 输出长度(比特)
    n = ceil(klen / v)    // 迭代次数
    K = empty_bytes
    
    for i = 1 to n:
        ct = int_to_32bit_big_endian(i)
        Ha = SM3(Z || ct)
        K = K || Ha
    
    // 截断到 klen 比特
    return K[0 : klen/8]

我的一点经验:计数器 ct(i) 的编码方式一定要和协议严格对齐。我曾经见过一个项目,因为大小端搞反了,两边的派生密钥始终对不上。排查了整整两天,最后发现是字节序的问题。

3.3 派生场景:KDF 在 TEE 中的典型用法

在 TEE 环境中,SM2 KDF 主要出现在以下几个场景:

场景 输入 Z 输出用途 安全要求
密钥交换 共享点 (x, y) 会话加密密钥 需包含双方身份标识
加密派生 临时公钥 + 共享秘密 对称加密密钥 需绑定消息上下文
身份认证 长期密钥派生值 认证令牌 需加入时间戳防重放
密钥封装 封装的密钥材料 数据保护密钥 需验证封装完整性

在 TEE 里,KDF 的执行环境很特殊。所有中间计算结果——包括计数器、哈希中间值、最终密钥——都必须在安全内存中处理,不能泄露到普通世界。

3.4 安全要求与避坑指南

KDF 看起来简单,但安全细节很多。我总结了几个必须注意的点:

  • 输入唯一性:每次派生使用的 Z 必须不同。如果两次派生用了相同的 Z,攻击者可以通过比较输出获得信息。
  • 上下文绑定:在 TEE 中,我建议把会话 ID、角色标识、算法标识都拼进 Z 里。这样即使共享秘密相同,不同上下文的派生密钥也不同。
  • 侧信道防护:KDF 的计算路径不能依赖输入值。SM3 的循环次数、内存访问模式都应该是固定的。
  • 密钥生命周期:派生出的密钥用完后,必须立即擦除。在 TEE 里,我习惯用专门的 memset_secure 函数,防止编译器优化掉清零操作。

我曾经踩过的坑:在一个 IoT 项目中,KDF 的输入 Z 只包含了共享点的 x 坐标,没有包含 y。结果攻击者可以通过选择不同的 y 值(对应同一个 x)来构造多个有效共享秘密,从而获得多个不同的派生密钥。这直接破坏了密钥的唯一性。

正确的做法是:始终使用完整的 (x, y) 坐标对作为 KDF 输入。

3.5 KDF 流程全景图

下面这张图展示了 SM2 KDF 的完整流程,以及它在 TEE 中的安全边界:

SM2 KDF 流程与 TEE 安全边界 共享秘密 Z (x || y) 初始化 i=1, n=ceil(klen/256) i ≤ n ? (迭代判断) 是 SM3(Z || ct(i)) 输出 Ha(i) K = K || Ha(i) i++ 否 截断至 klen 位 派生密钥 K TEE 安全边界:所有中间变量(Z、ct、Ha、K)必须在安全内存中处理,禁止泄露到 REE 侧信道防护:固定循环次数、常量时间比较、使用安全清零函数

3.6 小结

KDF 是 SM2 密钥派生中的“翻译官”——把椭圆曲线上的共享秘密,翻译成对称密码能用的密钥。SM2 规范选择了 SM3 加计数器模式,这个设计很务实:既利用了 SM3 的伪随机性,又通过计数器保证了输出长度可扩展。

在 TEE 里实现 KDF,技术本身不难。真正的挑战在于:输入要绑上下文,计算要防侧信道,输出要及时擦除。这三个点,任何一个出问题,整个密钥体系都会崩塌。