TLS 1.3 中的密钥派生过程:从共享秘密到应用密钥的 HKDF 链
题目描述
在 TLS 1.3 协议中,一旦客户端和服务器通过密钥交换(如基于椭圆曲线的迪菲-赫尔曼密钥交换 ECDHE)生成了一个共享密钥(Pre-Shared Key, PSK),并不能直接用作加密或认证的密钥。为了生成多个用于不同用途(如加密客户端到服务器的流量、服务器到客户端的流量、计算握手消息的完整性校验码等)的独立且安全的密钥,TLS 1.3 使用了基于 HMAC 的密钥派生函数(HKDF)来构建一个层次化的密钥派生链。本题将详细解析这个过程:如何从初始的共享秘密出发,经过一系列确定的、不可逆的步骤,派生出最终的应用层加密密钥。
解题过程
整个密钥派生过程可以看作一条单向的、可扩展的派生链。其核心是 HKDF 的两个组成部分:HKDF-Extract 和 HKDF-Expand。我们用“=>”表示派生关系。
步骤 1: 起点——初始密钥材料的确定
在 TLS 1.3 握手中,密钥派生的起点被称为“早期密钥”或初始“输入密钥材料”(Input Keying Material, IKM)。对于最常见的(EC)DHE 握手,这个初始秘密是两个计算得到的值经过 HKDF-Extract 后的结果:
- (EC)DHE 共享秘密(SS): 由客户端和服务器各自计算得到的相同值。
- “0”值盐(Salt): 在第一次调用 HKDF-Extract 时,通常使用一个全零的字节串作为盐(Salt),或者,如果使用了预共享密钥(PSK),这个盐会是一个之前派生的值。
这个初始提取步骤可以形式化表示为:
Early Secret = HKDF-Extract(salt=0, IKM=(EC)DHE Shared Secret)
这里的 HKDF-Extract(salt, IKM) 本质上是 HMAC-Hash(salt, IKM),其输出是一个密码学意义上强度足够、均匀分布的伪随机密钥(PRK)。这个 Early Secret 是整个密钥派生树的根。
步骤 2: 派生握手阶段密钥——密钥树的第一次分支
接下来,需要为握手消息的完整性(MAC)派生一个密钥。这是通过 HKDF-Expand-Label 函数实现的,它是 HKDF-Expand 的一个封装,方便在 TLS 1.3 的特定上下文中添加标签。
Derived Secret = HKDF-Expand-Label(PRK, label, Context, Length)
其中:
PRK: 输入密钥,这里是上一步的Early Secret。label: 一个ASCII字符串标签,明确标识派生密钥的用途,例如"derived"。Context: 可选的上下文信息,在握手阶段通常为空或包含握手哈希。Length: 期望的输出密钥长度。
具体过程如下:
- 计算
Handshake Secret的中间派生值:Derived Secret = HKDF-Expand-Label(Early Secret, "derived", "", Hash.length)。这个Derived Secret用于“重置”派生链,作为下一次 HKDF-Extract 的盐。 - 将 (EC)DHE 共享秘密与这个新的盐再次提取,得到
Handshake Secret:Handshake Secret = HKDF-Extract(salt=Derived Secret, IKM=(EC)DHE Shared Secret)。 - 从
Handshake Secret派生出两个实际使用的密钥:- 客户端握手流量密钥(client_handshake_traffic_secret):
= HKDF-Expand-Label(Handshake Secret, "c hs traffic", Handshake Context, Hash.length) - 服务器握手流量密钥(server_handshake_traffic_secret):
= HKDF-Expand-Label(Handshake Secret, "s hs traffic", Handshake Context, Hash.length) - 这里的
Handshake Context是到“Server Hello”消息为止(但不包含证书等加密后的消息)所有握手消息的哈希值,用于绑定密钥到当前特定的握手过程。
- 客户端握手流量密钥(client_handshake_traffic_secret):
步骤 3: 派生物流阶段密钥——密钥树的第二次分支
握手消息使用上述密钥加密认证后,双方计算到目前为止(包含所有明文握手消息和加密的证书等)的所有握手消息的哈希,称为 Handshake Hash。然后用类似步骤2的过程,从 Handshake Secret 派生出应用数据阶段的根密钥。
- 计算
Master Secret的中间派生值:再次使用“derived”标签派生一个新的盐:Derived Secret' = HKDF-Expand-Label(Handshake Secret, "derived", "", Hash.length)。 - 用这个新盐和一个“0”值的 IKM 进行提取,得到
Master Secret(在TLS 1.3规范中也称为application_traffic_secret_0的父秘密):Master Secret = HKDF-Extract(salt=Derived Secret', IKM=0)。这里的 IKM 是0,意味着我们不再引入新的熵,而是对现有的Handshake Secret进行转换,以物理上隔离握手密钥和应用密钥。 - 从
Master Secret派生出最终的、用于加密应用数据的两个密钥:- 客户端应用流量密钥(client_application_traffic_secret_0):
= HKDF-Expand-Label(Master Secret, "c ap traffic", Handshake Hash, Hash.length) - 服务器应用流量密钥(server_application_traffic_secret_0):
= HKDF-Expand-Label(Master Secret, "s ap traffic", Handshake Hash, Hash.length) - 这里的
Handshake Hash是完整的握手消息哈希,确保了应用密钥与整个握手过程唯一绑定。
- 客户端应用流量密钥(client_application_traffic_secret_0):
步骤 4: 从流量密钥到实际加密密钥
上一步得到的 *_traffic_secret 还不是直接用于 AES 或 ChaCha20 的密钥。它们是“密钥块”(Key Block)。TLS 1.3 会从这些“密钥块”中,通过 HKDF-Expand 派生出具体的对称密钥和初始化向量(IV):
- 写密钥(client_write_key):
= HKDF-Expand-Label(client_application_traffic_secret_N, "key", "", key_length) - 写初始向量(client_write_iv):
= HKDF-Expand-Label(client_application_traffic_secret_N, "iv", "", iv_length) - 这里的
client_application_traffic_secret_N中的 N 从0开始,可以随着密钥更新(Key Update)而递增,以实现前向安全。
核心安全设计思想
- 密钥分离:通过 HKDF 的链式结构和独特的标签(
“c hs traffic”,“s ap traffic”,“key”,“iv”),确保了不同方向、不同阶段的密钥是独立且不相关的。即使一个密钥泄露,也不会危及其他密钥。 - 上下文绑定:在
HKDF-Expand-Label中引入握手消息的哈希值作为上下文,确保了密钥与当前特定的通信会话绑定。即使相同的预主密钥被重用,只要握手的消息有细微差别,派生出的密钥也会完全不同,防止重放攻击和会话重复。 - 前向安全:每次密钥派生都是单向的。应用层密钥(
Master Secret及其派生物)与握手阶段密钥(Handshake Secret及其派生物)通过一次以Derived Secret'为盐、0为IKM的 HKDF-Extract 操作进行了密码学意义上的“切割”。一旦握手完成,Handshake Secret和 (EC)DHE 共享秘密可以被安全地删除,即使它们后来泄露,攻击者也无法回推出应用流量密钥,因为 HKDF 具有单向性。
通过这条从 Early Secret -> Handshake Secret -> Master Secret -> *_traffic_secret -> write_key/write_iv 的清晰、结构化的 HKDF 派生链,TLS 1.3 实现了从单一的共享秘密到多个安全、独立、会话绑定的工作密钥的稳健转换。