密钥派生函数(KDF):HKDF原理、PRF在TLS中的应用、提取与扩展阶段
密钥派生函数(Key Derivation Function)听着挺学术,说白了就是从一个种子密钥里,派生出一堆不同用途的子密钥。我刚开始搞TLS开发时,总觉得这一步有点多余——直接用主密钥不行吗?后来踩了坑才明白,没有KDF,会话安全性基本就是纸糊的。
为什么?你想想看,如果握手阶段输出的主密钥(Master Secret)直接拿来加密,那它暴露一次就全剧终。真正安全的做法是用一个“密钥工厂”,输入主密钥,输出加密密钥、HMAC密钥、IV等。这个工厂,就是KDF。
TLS 1.2用的KDF是基于PRF(伪随机函数)的,而TLS 1.3换成了HKDF(HMAC-based Key Derivation Function)。我个人更偏爱HKDF,原因后面说。
从PRF到KDF:TLS 1.2的做法
咱们先看看TLS 1.2时代是怎么干的。那时候TLS定义了一个P_hash函数,本质上是不断对种子做HMAC迭代,生成足量的伪随机输出。
P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
HMAC_hash(secret, A(2) + seed) +
HMAC_hash(secret, A(3) + seed) + ...
其中 A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))
这个构造用于生成主密钥,以及后续所有会话密钥。说实话,TLS 1.2的PRF设计得有些杂乱——它允许使用MD5+SHA1双哈希组合,搞得实现起来挺麻烦。我记得有一次排查互操作性问题,双方PRF算法选项对不上,调试了一整天。
HKDF:更干净的KDF设计
HKDF是2010年才标准化的(RFC 5869),TLS 1.3终于采纳了它。它比TLS 1.2的PRF干净在哪?
说白了,HKDF把密钥派生分成了两个阶段:提取和扩展。这个分拆思路,我在做密码学审计时深表认同——提取阶段负责“提纯”,扩展阶段负责“分叉”,各司其职,逻辑清晰。
下面是HKDF的完整流程,我画了张图帮你建立整体认知:
图:HKDF提取与扩展阶段工作流
提取阶段(Extract):把熵“拧”出来
提取阶段做什么?它接收一个不一定均匀随机的输入密钥材料(IKM),加上一个盐值,输出一个伪随机密钥(PRK)。
公式很简单:
PRK = HMAC-Hash(salt, IKM)
这里的IKM就是TLS主密钥,盐值由协商参数导出。HMAC在这里充当“熵提取器”——即便主密钥中有部分比特是可预测的,HMAC也会把熵分散到整个输出中。说人话:它能把一个有偏的输入,变成一个接近均匀的密钥。
为什么需要盐值? 如果没有盐值,同样的主密钥会生成同样的PRK,这在多会话场景下容易遭重放。加上盐值后,每次握手的PRK都不同。嗯,这里要注意盐值必须是公开的,且每次重新握手都变化。
扩展阶段(Expand):把密钥“拉”长
扩展阶段接收PRK,输出任意长度的密钥材料。公式是:
HKDF-Expand(PRK, info, L) = T(1) || T(2) || T(3) || ... until L bytes
其中:
T(0) = ""
T(i) = HMAC-Hash(PRK, T(i-1) || info || i)
info参数是关键:不同的info值,生成的密钥就不同。TLS 1.3用info区分“握手加密密钥”、“握手HMAC密钥”、“应用数据加密密钥”等。这样做的好处是——即便你拿到应用数据加密密钥,也无法反推出握手密钥。这是密钥分离的设计哲学。
我曾经在代码审查中发现,有一位开发者忘了在info中加入key类型标识,结果所有派生密钥都相同。虽然同一个HKDF对象不会同时输出两个不同的密钥,但这意味着如果应用数据密钥泄露,攻击者可以直接反推握手密钥。这个漏洞我们当时花了三天紧急修。
HKDF vs 传统PRF:一个表格看清
| 维度 | HKDF | TLS 1.2 PRF (P_hash) |
|---|---|---|
| 设计阶段 | 提取 + 扩展分离 | 单一迭代扩展 |
| 熵提取能力 | 强(通过盐值HMAC) | 弱(依赖种子初始熵) |
| 密钥分离 | 支持(通过info参数) | 需手算标签并拼接 |
| 安全性证明 | 有(基于HMAC安全证明) | 无形式化证明 |
| 标准文档 | RFC 5869 | RFC 5246 |
好了,这一轮的HKDF是讲完了。它是TLS密钥派生体系的基石——提取阶段定熵,扩展阶段定功能。我建议你在实现或排查TLS握手问题时,先确认HKDF的两个阶段参数是否对齐。很多时候密钥对不上的问题,就是info参数传错了,或者盐值没有按规范构造。