4. 密钥派生与密钥层次结构设计
在受限设备上,密钥不能“一个用到底”。如果设备只烧录一把密钥,既用它做身份认证,又用它加密数据、生成会话,那么一旦某次会话或某个用途的密钥泄露,整台设备的所有安全能力都会崩塌。密钥派生(Key Derivation)要解决的核心问题就是:用一把高熵的根密钥,按用途、按设备、按会话派生出彼此隔离的子密钥,让泄露影响被限制在单个分支内,同时避免在设备上存储大量独立密钥。
受限设备的约束很具体:RAM 常以 KB 计,Flash 有限,缺少硬件乘法器或只有低主频,且很多低端 MCU 没有真随机数发生器。因此派生方案必须满足三点:计算量小、状态少、可复现。KDF(Key Derivation Function)正是为此设计的确定性函数——相同输入必然得到相同输出,不需要保存派生结果,需要时重新算即可。
KDF 的两种主流选择
HKDF(RFC 5869)基于 HMAC,分为 Extract 和 Expand 两步。Extract 把可能不均匀的输入密钥材料(IKM)压缩成固定长度的伪随机密钥 PRK;Expand 再用 PRK 和可选信息 info 扩展出任意长度的输出密钥材料 OKM。对受限设备而言,如果根密钥本身已经是均匀随机的(例如出厂时由安全产线注入的 128/256 位随机数),可以跳过 Extract,直接进入 Expand,省下一次 HMAC 计算。
NIST SP 800-108则定义了基于 PRF 的计数器模式、反馈模式和双管道模式。计数器模式最常用:把密钥 K、标签 Label、上下文 Context 和递增计数器 i 拼接后送入 PRF,逐块产生输出。它天然支持“一个主密钥派生多个带标签的子密钥”,非常适合层次化设计。
两者在受限设备上的取舍可以这样看:HKDF 实现更紧凑、有成熟测试向量、适合派生少量定长子密钥;SP 800-108 计数器模式在需要派生大量带明确用途标签的密钥时更直观。实际工程中,很多方案用 HKDF-Expand 作为底层 PRF,再按 SP 800-108 的“标签+上下文”思路组织 info 字段,兼顾两者优点。
三层密钥层次结构
推荐的层次为:根密钥 RK → 设备密钥 DK → 会话密钥 SK,必要时再加一层用途密钥。
- 根密钥 RK:设备唯一、长期不变,由产线注入或安全芯片内部生成,永不导出。它是整棵密钥树的信任锚点。
- 设备密钥 DK:由 RK 结合设备唯一标识(如芯片 UID、序列号)派生,用于设备身份认证、固件签名验证等设备级用途。不同设备即使 RK 相同(实际不会),DK 也因 UID 不同而不同。
- 会话密钥 SK:由 DK 结合会话随机数(nonce)和用途标签派生,一次一密,用完即弃。用于加密通信、消息认证等。
这种层次的关键在于:每一层的派生输入都包含“区分信息”。设备层用 UID 区分设备,会话层用 nonce 区分会话,用途层用 label 区分功能。这样即使某一层的一个子密钥泄露,攻击者也无法反推上层密钥,更无法横向推出同层其他子密钥。
受限设备上的轻量化实现要点
在 Cortex-M0/M3 这类没有硬件加密加速的 MCU 上,HMAC-SHA256 一次约需 1~3 ms(48 MHz 主频下),完全可以接受。真正需要控制的是栈空间和临时缓冲区。HKDF-Expand 的典型实现只需要一个 HMAC 上下文(约 200~300 字节)和一个输出缓冲区,不需要保存中间 PRK 之外的额外状态。
一个常见的轻量化技巧是预计算并缓存设备密钥 DK。RK 只在首次启动或安全启动阶段使用一次,派生出的 DK 存入受保护的 Flash 区域或安全芯片的密钥槽。后续每次建立会话时,只用 DK 派生 SK,避免频繁访问根密钥。这样既减少了根密钥的暴露面,也降低了每次会话的派生开销。
另一个要点是info 字段的构造必须无歧义。如果直接把字符串拼接,可能出现 "ab"+"c" 与 "a"+"bc" 相同的情况。推荐使用长度前缀编码,例如:len(UID)‖UID‖len(label)‖label‖len(nonce)‖nonce。这样即使各字段内容可变,拼接结果也唯一可解析。
实战:从主密钥派生多用途子密钥
下面给出一个基于 HKDF-Expand 的 C 语言风格伪代码示例,展示如何从设备密钥 DK 派生加密密钥和认证密钥。假设 DK 为 32 字节,目标子密钥各 16 字节。
// 输入:dk[32] 设备密钥,uid[8] 设备标识,nonce[8] 会话随机数
// 输出:enc_key[16], mac_key[16]
uint8_t info_enc[64];
uint8_t info_mac[64];
size_t pos;
// 构造 info_enc = len(uid)‖uid‖len("enc")‖"enc"‖len(nonce)‖nonce
pos = 0;
info_enc[pos++] = 8; // uid 长度
memcpy(info_enc + pos, uid, 8); pos += 8;
info_enc[pos++] = 3; // "enc" 长度
memcpy(info_enc + pos, "enc", 3); pos += 3;
info_enc[pos++] = 8; // nonce 长度
memcpy(info_enc + pos, nonce, 8); pos += 8;
// 构造 info_mac,仅标签不同
memcpy(info_mac, info_enc, pos);
info_mac[12] = 3; // 标签长度位置
memcpy(info_mac + 13, "mac", 3); // 替换标签
// HKDF-Expand:T(1) = HMAC(dk, info ‖ 0x01)
hkdf_expand(dk, 32, info_enc, pos, enc_key, 16);
hkdf_expand(dk, 32, info_mac, pos, mac_key, 16);
执行后,enc_key 和 mac_key 就是两个完全独立的 16 字节子密钥。即使攻击者拿到了 enc_key,由于 HMAC 的单向性,他无法反推出 dk,也无法计算出 mac_key。这就是密钥隔离的实际效果。
层次结构设计的检查清单
| 检查项 | 合格标准 | 常见问题 |
|---|---|---|
| 根密钥保护 | 永不导出,仅安全启动阶段使用 | 明文存 Flash,可被读出 |
| 设备密钥唯一性 | 派生输入包含 UID,设备间不重复 | 所有设备共用同一 DK |
| 会话密钥新鲜性 | 每次会话 nonce 不同,SK 用完即弃 | SK 长期复用 |
| 用途隔离 | 不同用途 label 不同,密钥不可互换 | 加密和认证共用一把密钥 |
| info 编码 | 长度前缀,无歧义拼接 | 直接字符串拼接 |
| 算法验证 | 通过标准测试向量 | 自行发明 KDF 逻辑 |
把这张检查清单落到具体设备上时,建议先用一台样机跑通完整链路:产线注入 RK → 首次启动派生 DK 并写入安全区 → 建立会话时派生 SK → 用 SK 加密一条测试消息 → 在服务器侧用相同参数复现派生结果并解密。如果服务器能正确解密,说明整条派生链的输入、编码和算法都一致;如果失败,优先检查 info 字段的字节序和长度前缀是否两端一致。