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 区分功能。这样即使某一层的一个子密钥泄露,攻击者也无法反推上层密钥,更无法横向推出同层其他子密钥。

受限设备密钥层次派生结构 根密钥 RK 256-bit · 永不导出 · 产线注入 HKDF-Expand(RK, info=UID‖"device") 设备密钥 DK_A UID = A1B2… 设备密钥 DK_B UID = C3D4… 设备密钥 DK_C UID = E5F6… + nonce + nonce + nonce 会话密钥 SK_A1 nonce = 0x7F3A 会话密钥 SK_B1 nonce = 0x2C81 会话密钥 SK_C1 nonce = 0x9D04 + label + label + label 加密密钥 label="enc" 认证密钥 label="mac" 固件校验密钥 label="fw" 派生输入要素: 根密钥(高熵,不导出) 设备唯一标识 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。这就是密钥隔离的实际效果。

验证方法:用 RFC 5869 附录 A 的测试向量验证 HKDF-Expand 实现是否正确。例如 Test Case 1 中,PRK = 0x077709362c2e32df0ddc3f0dc47bba63…,info = 0xf0f1f2f3f4f5f6f7f8f9,L = 42,期望 OKM 前 16 字节为 0x3cb25f25faacd57a90434f64d0362f2a。如果实现输出一致,说明 Expand 逻辑正确。
常见错误:不要把 nonce 直接当密钥用,也不要用计数器简单异或代替 KDF。这些做法会破坏密钥的伪随机性和独立性。另外,如果设备没有 TRNG,nonce 必须来自可信的单调计数器或由服务器下发,不能使用可预测的启动时间戳。

层次结构设计的检查清单

检查项合格标准常见问题
根密钥保护永不导出,仅安全启动阶段使用明文存 Flash,可被读出
设备密钥唯一性派生输入包含 UID,设备间不重复所有设备共用同一 DK
会话密钥新鲜性每次会话 nonce 不同,SK 用完即弃SK 长期复用
用途隔离不同用途 label 不同,密钥不可互换加密和认证共用一把密钥
info 编码长度前缀,无歧义拼接直接字符串拼接
算法验证通过标准测试向量自行发明 KDF 逻辑

把这张检查清单落到具体设备上时,建议先用一台样机跑通完整链路:产线注入 RK → 首次启动派生 DK 并写入安全区 → 建立会话时派生 SK → 用 SK 加密一条测试消息 → 在服务器侧用相同参数复现派生结果并解密。如果服务器能正确解密,说明整条派生链的输入、编码和算法都一致;如果失败,优先检查 info 字段的字节序和长度前缀是否两端一致。