跳到主要内容
加密解密2026-10-04Begin3 分钟阅读

HKDF:TLS 1.3 怎么把一个共享秘密派生成整棵密钥树

问题:一个秘密,一树密钥

TLS 握手结束时双方手里有一个共享秘密(X25519 的输出,或 ML-KEM 混合组的两个秘密拼接)。但一个 TLS 会话需要的密钥远不止一个:

  • 客户端/服务端各自的握手阶段写密钥与 IV(加密 Finished)
  • 客户端/服务端各自的应用数据阶段写密钥与 IV
  • 各阶段的 Finished 校验密钥
  • Exporter(给上层协议导出密钥材料的接口)
  • 会话恢复用的 resumption PSK 与 ticket 密钥

并且这些密钥必须互相独立——泄漏应用数据密钥不能危及握手完整性,旧会话密钥不能帮助解新会话。把一个秘密安全地「长」成一棵树,就是 HKDF 的工作。

两步走:Extract 收敛,Expand 分叉

HKDF(RFC 5869)建立在 HMAC 之上,两个阶段:

Extract(提取):PRK = HMAC(salt, IKM)——把来源可能分布不均匀的输入密钥材料(IKM)与一个盐值混合,收敛成密码学上均匀的伪随机密钥(PRK)。盐的作用是域分离:不同用途、不同会话用不同盐,即使 IKM 相同也得到不同 PRK。

Expand(扩展):OKM = HKDF-Expand(PRK, info, L)——以 info 作为标签,从 PRK 派生出任意长度的输出密钥材料。info 里写清楚这个密钥「是什么、给谁、干什么用」。

为什么不能直接 hash(secret || label)?三个硬理由:长度扩展类结构弱点(取决于所用哈希的构造)、无法安全地做长输出、以及最关键的——没有内置域分离。info 标签机制保证「client write key」和「server write key」哪怕内容巧合也不会碰撞成同一个密钥。

TLS 1.3 的三层密钥调度

TLS 1.3 把 HKDF 用成了一棵调度树:

Early Secret  ←  PSK(会话恢复路径)
   ↓ Derive-Secret
Handshake Secret  ←  ECDHE/ML-KEM 共享秘密注入
   ↓ "c hs traffic" / "s hs traffic"   → 握手阶段密钥
Master Secret
   ↓ "c ap traffic" / "s ap traffic"   → 应用数据密钥
   ↓ "exp master"                      → Exporter
   ↓ "res master"                      → 恢复 PSK

每一层都是一次 HKDF-Extract(注入新秘密)+ 多次带标签的 Expand。设计的精妙处:握手的每个阶段只解锁自己那层的密钥——即使握手密钥被攻破,应用数据层的密钥依然独立存在;反过来 Finished 消息用握手密钥覆盖了全部握手转录,中间人无法篡改协商过程。

快 KDF 与慢 KDF:同名的两种东西

一个常见混淆:HKDF 与 PBKDF2/argon2 都叫「密钥派生」,分工却完全相反——

HKDFPBKDF2 / bcrypt / argon2
输入已经高熵的秘密(DH 输出)低熵的人类密码
设计目标快(会话建立不卡顿)慢(拖垮穷举)
抗暴力破解不承担核心职责

用 HKDF 处理密码、或用 argon2 派生 TLS 会话密钥,都是范畴错误——前者毫无防御,后者白白增加几百毫秒延迟。

实现注记 / Implementer's note

浏览器里 HKDF 是 WebCrypto 一等公民(crypto.subtle.deriveBits 配 HKDF 算法),这也是我做站内工具时的直接体会:HMAC 原语(HMAC 工具)与 HKDF 只差两层薄封装——extract 就是一次 HMAC,expand 是带计数器的 HMAC 链。另一面的对照同样真实:AES 工具从口令派生密钥时必须走 PBKDF2 的慢路线(浏览器实测每秒只能跑几万次迭代,这是特性不是性能问题)。两套派生逻辑在同一个工具站里并存,恰好把这张分工表变成了可触摸的东西。

相关阅读