设备和服务器第一次连接时,通常没有一把已经约好的会话密钥。它们需要在开放网络上协商出一把后续通信用的对称密钥。
很多人会把这个过程压缩成一句话:“用 ECDH 算一个密钥。”
这句话少了两层关键边界:ECDH/X25519 算出来的是共享秘密,不是最终可直接喂给 AES-GCM 的会话密钥;而这条链路的安全性又依赖随机数和上下文绑定。
一条更完整的路径是:
本地临时私钥 + 对端公钥
-> ECDH/X25519
-> 共享秘密
-> HKDF/KDF 结合 salt、info、握手上下文
-> 发送密钥、接收密钥、nonce 基础值
-> AEAD 保护后续数据
真正要理解的是:密钥交换、KDF 和随机数分别解决不同问题。
密钥交换解决“双方怎样得到同一个秘密”
ECDH 和 X25519 的目标,是让双方在不直接传输秘密的情况下,算出同一个共享秘密。
简化模型是:
设备私钥 a + 服务器公钥 B -> shared secret S
服务器私钥 b + 设备公钥 A -> shared secret S
旁路观察者只能看到公钥交换,看不到双方私钥,因此不能直接算出 S。
这解决的是开放网络里的一个核心问题:双方没有预共享会话密钥,但希望协商出后续通信使用的秘密材料。
但 S 通常不能直接当成最终密钥。它只是一个原始共享秘密,还需要经过 KDF。
共享秘密不能直接当密钥
直接把 ECDH 输出塞进 AES 或 HMAC,是常见错误。
原因包括:
- 原始共享秘密格式和长度不一定适合目标算法
- 不同用途需要不同密钥
- 协议上下文需要绑定进去
- 握手 transcript 需要参与密钥派生
- 需要分离发送方向和接收方向
- 需要避免同一秘密材料在多个用途里复用
KDF 的作用是把原始秘密变成“按用途分开的密钥材料”。
shared secret
-> KDF
-> client_write_key
-> server_write_key
-> iv / nonce base
-> exporter key
这也是 TLS 这类协议不会直接使用 ECDH 输出的原因。
HKDF 把秘密变成带上下文的密钥
HKDF 是常见 KDF。它通常可以理解成两步:
Extract:把原始秘密和 salt 混合成伪随机密钥
Expand:结合 info 派生指定长度和用途的输出
其中:
salt帮助把输入秘密规整到安全状态info用来绑定用途和上下文- 输出长度由具体用途决定
info 很关键。它可以包含:
- 协议版本
- 握手 transcript hash
- 设备和服务器角色
- 使用方向
- 算法套件
- 会话 ID
- 应用用途标签
这样派生出来的密钥不是孤立字节串,而是绑定到这次协议、这次会话和这个用途。
随机数决定临时密钥是否真的临时
密钥交换通常需要临时私钥。临时私钥必须来自可靠随机数。
如果随机数出问题,后果很严重:
- 临时私钥重复,可能复用会话秘密
- 私钥可预测,攻击者能算出共享秘密
- 设备批量出厂时多个设备生成相同密钥
- 早期启动熵不足导致密钥质量差
- 恢复出厂后重复生成旧密钥
IoT 设备尤其容易遇到 RNG 问题。设备刚上电时,网络还没连,用户输入没有,外设噪声不足,系统可能还没有收集到足够熵。
安全系统不能在 RNG 未就绪时生成长期密钥或临时握手密钥。否则后面所有算法都选对了,也是在错误秘密上运行。
长期密钥和会话密钥要分离
设备通常会有长期身份密钥,例如设备私钥、PSK、PUF 派生密钥或安全芯片里的密钥。
这些长期密钥不应该直接用于大量数据加密。
更稳的模型是:
长期密钥:用于身份认证或密钥协商
会话密钥:用于当前连接的数据保护
分离有几个好处:
- 会话结束后可以丢弃会话密钥
- 不同连接使用不同密钥和 nonce 空间
- 长期密钥暴露面更小
- 可以支持前向安全
- 可以按方向和用途派生不同密钥
例如 TLS 里,证书私钥主要用于身份认证,真正加密应用数据的是握手后派生出的会话密钥。
前向安全依赖临时密钥
前向安全的意思是:即使长期身份密钥以后泄露,过去已经完成的会话内容仍然不应被解密。
这通常依赖临时 ECDH:
每次会话生成临时密钥对
-> 用临时私钥完成密钥交换
-> 会话结束后销毁临时私钥和会话密钥
如果系统直接用长期密钥加密数据,或者长期 ECDH 密钥长期复用,一旦长期密钥泄露,历史流量也可能被解密。
设备资源有限时,有些协议会在性能、功耗和前向安全之间取舍。但这个取舍必须显式知道,不能误以为“用了公钥算法”就自动有前向安全。
PSK 也需要 KDF 和上下文
有些设备不用证书,而是预共享密钥 PSK。
PSK 不等于可以直接当 AEAD key。更好的方式是:
PSK + nonce/challenge/session context
-> KDF
-> session keys
原因一样:要分离用途、绑定上下文、避免长期密钥直接暴露在大量数据保护路径上。
PSK 方案要特别注意:
- 每台设备是否独立 PSK
- PSK 是否能被服务器按设备区分
- PSK 泄露影响范围
- 是否有重放防护
- 是否支持密钥轮换
- 是否避免多协议复用同一 PSK
所有设备共享一个 PSK 是非常危险的设计。一台设备泄露,整批设备都可能被伪造。
KDF 的 info 不是随便填
KDF 的 info 字段常被随便写成空字符串或固定 "key"。这会丢掉重要上下文。
更好的做法是把用途写进派生过程:
info = "iot-v1 client write key"
info = "iot-v1 server write key"
info = "firmware encryption key"
info = "device attestation session"
不同用途应该派生不同 key。不要一把 key 同时做:
- 加密
- MAC
- 固件解密
- 日志认证
- 设备认证
密钥复用会让一个用途里的漏洞影响另一个用途。KDF 的意义之一就是把同一个根秘密扩展成互相隔离的用途密钥。
排查顺序
遇到握手失败、会话密钥不一致、重启后通信异常、批量设备密钥重复时,可以按这条链看:
随机数是否可用
-> 临时私钥是否每次新生成
-> ECDH/X25519 输入公钥是否校验
-> shared secret 是否经过 KDF
-> salt/info 是否绑定协议上下文
-> 发送和接收密钥是否分离
-> AEAD nonce 空间是否随会话重置
-> 长期密钥是否只用于身份或协商
-> PSK 是否按设备独立
如果问题只在刚上电、恢复出厂、批量生产或低熵环境下出现,优先看 RNG 和密钥生成时机。
真正要记住的边界
密钥交换、KDF 和随机数是一条链。
ECDH/X25519 解决双方如何得到同一个原始秘密。KDF 把原始秘密变成按用途、方向和上下文分离的会话密钥。随机数保证临时密钥、nonce 和挑战不可预测、不重复。
设备安全里,最危险的做法是把这条链压缩成“算个密钥然后加密”。真正可靠的系统会明确区分长期密钥、临时密钥、共享秘密、会话密钥和 nonce 状态。