密码学 11:一个秘密为什么派生多把密钥
10 篇里,两个临时密钥交换参与者算到同一份秘密。接下来的订单请求和服务端响应,能不能把这串值直接拿来当一把万能密钥,既负责客户端发送又负责服务端发送,未来连请求认证也复用?不该这样做。不同方向、阶段与用途需要隔离:一边的密钥、nonce 进度或使用权限变化,不应无端变成另一边的隐性输入。派生密钥是从一份已经足够不可猜测的输入得到明确用途的多份输出,不是把弱密码凭空升级成强秘密。
这篇用 HKDF 的标准向量验证算法输出,再对一份进程内临时秘密分别派生客户端订单、服务端订单和 RPC 用途密钥。不打印实际临时秘密与派生密钥;公开的十六进制输出仅限 RFC 官方测试向量。
提取与扩展各自输入什么
HKDF 使用 HMAC 构成两步:先对输入密钥材料 IKM 与 salt 进行提取,得到伪随机密钥 PRK;再以 PRK、表示上下文和用途的 info、指定输出长度 L 做扩展,得到所需密钥材料。接口可以简写如下,但真实调用一定要给出字节级参数:
1 | |
这里的 salt 是 RFC 5869 中 HKDF 提取的盐,不是 08 的密码哈希盐;名字相同但作用域和输入熵假设不同。info 是调用方用来把目的、方向、协议版本等绑定进输出的公开上下文;如果双方对它的编码不一致,输出不同,通信会失败。只写“order”却不写发送者、版本或参与双方身份时,可能在另一个用途里不小心派生出相同的键。于是派生树的标签规则本身就是协议设计的一部分,不能靠程序员脑补。
若 IKM 来自已认证的 DH 结果,这些派生键可供后续记录保护使用;但 HKDF 本身不能认出 DH 的公有 share 是否曾被中间人替换。甲与攻击者派生得再整齐,也依然是两人之间的会话,不会自动变成甲乙之间的会话。证书、握手签名、Finished、业务身份分别在对应层次解释。
相同输入和不同上下文各会怎样
实验用成熟的 Node.js node:crypto.hkdfSync 跑 HKDF-SHA256,先检查 RFC 5869 Appendix A.1:22 字节 0b IKM,13 字节 salt 000102...0c,10 字节 info f0f1...f9,长度 42。提取值 PRK 为 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5;完整输出 OKM 为 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865。这些都是早已公开的规范样例,可以逐字节核对,但不能拿来做服务密钥。
第二部分用系统随机接口生成 32 字节临时 IKM:lab:client-to-server:order、lab:server-to-client:order、lab:client-to-server:rpc 分别得到 32 字节输出。加入版本后缀 :v2,与原先客户端订单方向的输出也不相同;换公开 salt 亦不同。代码仅输出“不相等”的布尔断言,不把任何随机私有 IKM 或派生值放到 Git。它没有使用 TLS 1.3 真实的 HKDF-Expand-Label 字节编码,lab: 只是教学域标签;16 篇必须按协议的派生树重新核对。
提取、扩展、上下文分离是这里引入的三个新操作词。图里的标签和输出长度定义了所检查的任务,但测试通过仍不足以证明协议安全;真实系统还得管住如何获得 IKM、如何绑定正确身份、如何选 nonce、如何删除旧密钥。
KDF 不负责把弱密码变强
为了验证 08 的边界,脚本还把公开合成密码 book123 当作 IKM,给出公开 salt 与固定用途标签,随后对 password / book123 / book124 三个候选重算 HKDF。第二项匹配。HKDF 完全按规范工作,只是输入空间太小,攻击者仍能枚举;把输出做成 32 字节长也不会额外制造原输入缺少的熵。密码要按独立的 Argon2id 密码哈希与账号策略处理,不得拿 HKDF 冒充抗离线猜测的密码存储。
类似的错误还包括把一份含糊的“用户 ID”当 IKM;即使按方向扩展,也只能得到攻击者可以从公开 ID 重算的输出。先回答“原输入为何是秘密”,再问“怎样区分用途”,不要颠倒顺序。
已执行命令与未执行边界
1 | |
2026-10-06 UTC,Node 22.23.2 两条命令退出码均为 0,3 项测试通过。输出含上述 RFC PRK/OKM 向量、派生长度 32、方向/用途/版本相等检查皆为 false,合成弱密码的字典命中项为 book123。另一项测试单独把 salt 改掉,也要求输出不同。误把方向标签变成相同字符串,测试就应失败;若删除 book123 候选,弱密码可猜测性的实验断言失败——测试反例不成立并不代表 HKDF 把密码变强。
这里只运行了真实 HKDF 原语的标准向量和一份用途标签教学规则,没有运行 TLS 1.3 握手、真实流量密钥树或链钱包派生。真实 TLS 把不同阶段的握手摘要及专门的标签编码纳入派生,每一步的输入输出要对 RFC 9846 的具体公式;不要拿上图的自定义标签冒充 TLS 报文字段。
两道带答案的练习
画图题。 甲乙从同一个已认证且不可猜测的临时输入出发,分别要保护“客户端请求正文”和“服务端响应正文”,还有一个 RPC 请求用途。最少画出几条不同派生边?如果只用同一标签 record 会遗漏什么?
可核对答案: 至少标出客户端→服务端订单、服务端→客户端订单与客户端→RPC 三个独立上下文,再分别确定参与者、用途和长度;只写 record 容易把方向或用途混成同一键,即使双方得到相同的 IKM 也无法正确隔离。真正协议的标签应按其规范编码,不照抄本篇 lab: 字符串。
变更题。 将 11_hkdf.mjs 的 serverOrder 标签改为客户端订单标签,再运行脚本与测试。哪条可失败断言应触发?把输入 IKM 改为弱密码 book123 后,即使恢复两个不同标签,字典攻击还会不会成功?
可核对答案: 两方向派生输出相同,opposite_directions_equal 为真,脚本中的拒绝断言触发;测试如果仍独立使用原标签,要分别核对脚本与测试输入。弱密码的候选仍可按相同 salt/info 重算两种标签下的输出,方向分离不能代替 08 的密码哈希。
资料与导航
- RFC 5869,HKDF:提取、扩展及 Appendix A.1 完整字节向量。
- RFC 9846,§7:TLS 1.3 自己的派生树与上下文标签;本篇没有复现那套具体编码。
- 10 两端如何得到共同秘密 · 08 密码为什么不能直接做 SHA-256;12 正文未存在时不提前链接。





