第二章:密钥生命周期管理
密钥这东西,说白了就是一把锁的钥匙。但跟普通钥匙不一样,密钥从出生到销毁,每个环节都可能出问题。我做了这么多年嵌入式安全,见过太多因为密钥管理不当导致整个系统被攻破的案例。
今天咱们就聊聊密钥的全生命周期模型。嗯,就是密钥从生成、分发、存储、使用到销毁的完整过程。
2.1 密钥生成:起点决定安全高度
密钥生成是第一步,也是最容易被忽视的一步。我个人习惯把密钥生成比作「生孩子」——底子不好,后面再怎么折腾也白搭。
核心原则:密钥必须具有足够的熵(Entropy),且生成过程不可预测。
我在项目中遇到过不少厂商直接用软件随机数生成器(PRNG)来生成密钥。你想想看,PRNG 的种子如果被猜到,那所有密钥就等于公开了。所以我的建议是:
- 硬件真随机数生成器(TRNG):利用芯片内部的物理噪声源,比如热噪声、振荡器抖动等
- 混合模式:TRNG 生成种子 + 密码学安全的 PRNG(CSPRNG)扩展
- 避免使用:标准库的 rand()、时间戳、设备序列号等可预测源
// 不推荐的密钥生成方式
uint8_t key[16];
srand(time(NULL)); // 种子可预测!
for (int i = 0; i < 16; i++) {
key[i] = rand() % 256;
}
// 推荐的硬件 TRNG 方式
uint8_t key[16];
if (HAL_TRNG_GenerateRandomNumber(&htrng, (uint32_t*)key, 4) != HAL_OK) {
// 处理错误,不能使用降级方案!
Error_Handler();
}
⚠️ 我曾经见过一个产品,密钥生成用的是设备 MAC 地址 + 出厂时间戳的哈希值。攻击者只要拿到一台设备,就能推算出所有同批次设备的密钥。这种低级错误,千万别犯。
2.2 密钥分发:传输中的生死考验
密钥生成好了,怎么送到目标设备上?这就是分发阶段要解决的问题。说白了,密钥分发就是「如何在不可信的信道上安全地传递秘密」。
我习惯把分发方式分为两类:
| 分发方式 | 适用场景 | 安全等级 |
|---|---|---|
| 预置烧录(工厂烧录) | 量产设备 | 高(物理安全可控) |
| 密钥协商(如 ECDH) | 动态配对 | 中高 |
| 安全通道分发(TLS/DTLS) | OTA 更新 | 中 |
| 明文传输 | 绝对不要用 | 无 |
这里有个坑:很多人觉得工厂烧录就万事大吉了。其实不然。我记得有个项目,工厂的烧录脚本直接把密钥明文打印在日志里。嗯,你猜怎么着?那批设备的密钥全被泄露了。
💡 我的经验:工厂烧录时,一定要用加密通道把密钥传给烧录器。烧录器本身也要有防篡改机制。最好用 HSM(硬件安全模块)来管理烧录过程中的密钥。
2.3 密钥存储:最容易被攻破的环节
密钥存储是整个生命周期中最脆弱的环节。为什么?因为密钥一旦存到设备里,就脱离了我们的直接控制。攻击者可以物理接触设备,用各种手段提取密钥。
你想想看,如果密钥直接以明文形式存在 Flash 里,那跟没加密有什么区别?
常见的存储方式安全等级如下:
- ❌ 明文存储:直接写在 Flash 或 EEPROM 中
- ⚠️ 软件加密存储:用另一个密钥加密后存储(鸡生蛋问题)
- ✅ 硬件安全单元(HSM/Secure Element):密钥永远不离开安全芯片
- ✅ 物理不可克隆函数(PUF):利用芯片物理特性生成唯一密钥
// 错误示范:密钥明文存储
const uint8_t secret_key[32] = {
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
// ... 密钥直接暴露在二进制文件中
};
// 正确做法:使用安全元件 API
uint8_t key_handle;
if (SE_GenerateKey(SE_KEY_TYPE_AES_128, &key_handle) != SE_OK) {
Error_Handler();
}
// 后续操作使用 key_handle 引用密钥,而非直接读取密钥值
⚠️ 我曾经拆解过一款 IoT 设备,用逻辑分析仪抓 SPI 总线,直接读到了 Flash 中存储的 AES 密钥。因为厂商把密钥放在了一个固定的偏移地址,而且没有做任何加密保护。这种设计,说白了就是给攻击者送人头。
2.4 密钥使用:运行时安全
密钥在运行时被使用,这个阶段最容易出现侧信道攻击。比如功耗分析、电磁辐射分析、时序分析等。
我个人习惯把密钥使用安全归结为三点:
- 最小权限原则:每个密钥只用于特定用途,不要复用
- 隔离执行:密钥操作在安全环境中执行(如 TrustZone、TEE)
- 防侧信道:使用常数时间实现,避免基于数据的分支
// 不安全的比较方式(时序侧信道)
int memcmp_unsafe(const uint8_t *a, const uint8_t *b, size_t len) {
for (size_t i = 0; i < len; i++) {
if (a[i] != b[i]) return 0; // 一旦不匹配就返回,时序可被分析
}
return 1;
}
// 安全的常数时间比较
int memcmp_safe(const uint8_t *a, const uint8_t *b, size_t len) {
volatile uint8_t result = 0;
for (size_t i = 0; i < len; i++) {
result |= a[i] ^ b[i]; // 始终执行完整循环
}
return (result == 0);
}
💡 避坑指南:我曾经调试过一个加密模块,发现每次加密操作的时间都不一样。后来一查,是因为代码里根据密钥的某个比特位做了条件分支。攻击者只要测量几百次加密时间,就能还原出完整的密钥。嗯,从那以后我写密码学代码,都会刻意检查有没有非恒定时间的操作。
2.5 密钥销毁:善始善终
密钥销毁是生命周期中最后一步,也是最容易被忽略的一步。很多人觉得「设备不用了,密钥自然就没了」。其实不然。
你想想看,如果设备被回收、转卖或者丢弃,密钥还留在 Flash 里,那后续的持有者就能轻易读取。这在金融支付、身份认证等场景下是致命的。
密钥销毁的几个要点:
- 物理销毁:最彻底的方式,但成本高
- 覆写销毁:多次写入随机数据(至少 3 次,参考 NIST 标准)
- 安全擦除:利用芯片提供的安全擦除指令(如 Flash 的批量擦除)
- 不可逆:销毁后必须无法恢复
// 安全的密钥销毁函数
void secure_key_erase(uint8_t *key_buffer, size_t len) {
volatile uint8_t *ptr = key_buffer;
// 第一步:写入全 0
for (size_t i = 0; i < len; i++) ptr[i] = 0x00;
// 第二步:写入全 1
for (size_t i = 0; i < len; i++) ptr[i] = 0xFF;
// 第三步:写入随机数据
for (size_t i = 0; i < len; i++) ptr[i] = (uint8_t)rand();
// 第四步:再次写入全 0
for (size_t i = 0; i < len; i++) ptr[i] = 0x00;
// 防止编译器优化掉这些写入操作
__asm volatile("" : : "r"(ptr) : "memory");
}
⚠️ 注意:对于 NAND Flash,简单的覆写可能无法彻底擦除数据。因为 Flash 的磨损均衡机制会把数据映射到不同物理块。我建议使用芯片厂商提供的安全擦除 API,或者直接销毁整个存储芯片。
2.6 全生命周期模型总结
好了,咱们把密钥从生到死的五个阶段都过了一遍。我画了一张图,把这五个阶段串起来:
这张图把五个阶段串成了一个闭环。注意看,销毁之后有一条虚线箭头指向生成——但这其实是个警示:密钥一旦销毁,就不能再被使用。如果需要新密钥,必须重新走一遍完整的生命周期。
我在实际项目中总结了一个经验:密钥生命周期管理,说白了就是「信任链」的管理。从生成时的信任根(Root of Trust),到分发时的信任传递,再到存储和使用的信任维持,最后到销毁时的信任终结。任何一个环节断裂,整个安全体系就崩塌了。
核心要点回顾:
- 生成:用硬件 TRNG,别用软件伪随机
- 分发:加密通道 + 身份认证,别明文传输
- 存储:硬件隔离(HSM/SE/PUF),别放 Flash 明文
- 使用:常数时间实现,别留侧信道漏洞
- 销毁:多次覆写 + 物理销毁,别留残余数据
嗯,密钥生命周期管理就聊到这儿。记住一句话:密钥安全不是某一个环节的事,而是整个链条的事。链条的强度,取决于最弱的那一环。