密钥生命周期管理:从生到死的安全闭环
聊到密钥安全,很多人第一反应就是「加密算法够强就行」。其实不然。我见过太多项目,算法选得挺硬,结果密钥在某个环节泄露了——生成时没熵、存储时裸奔、使用时不隔离、销毁时留尾巴。说白了,密钥的安全不是某一个点的安全,而是全生命周期的安全。
这一章,我就带你走一遍密钥从生到死的四个阶段:生成 → 存储 → 使用 → 销毁。每个阶段都有坑,咱们一个一个填。
2.1 密钥生成:起点决定终点
密钥生成是整个生命周期的起点。这个起点要是歪了,后面再怎么加固都没用。我个人习惯把生成阶段拆成三个关键点来检查。
2.1.1 熵源质量
随机数生成器(RNG)是密钥生成的「原材料」。TEE 环境里,我们通常用硬件真随机数生成器(TRNG)。为什么?因为软件伪随机数生成器(PRNG)的种子一旦被猜到,所有密钥就全废了。
我在项目中遇到过一件事:某厂商的 TEE 实现,TRNG 的熵源居然只依赖一个内部振荡器,没有做健康检测。结果在低温环境下,振荡器频率漂移,输出的随机数出现了明显的周期性。嗯,那批设备生成的密钥,我们后来全部强制轮换了。
2.1.2 生成位置
密钥在哪里生成,决定了它的初始安全等级。TEE 环境里,密钥必须在安全世界(Secure World)内部生成,绝不能由普通世界(Normal World)生成后再导入。
你想想看,如果密钥在普通世界生成,那它在内存里已经裸奔过了。就算后面再塞进 TEE,也保不齐已经被侧信道或者内存嗅探给抄走了。
2.1.3 生成后的初始化状态
密钥生成出来之后,不是直接就能用的。我习惯做三件事:
- 校验完整性: 对密钥做一次 HMAC,确保生成过程中没有位翻转。
- 标记用途: 给密钥打上标签——是用于签名、加密还是身份认证?后面使用时要校验这个标签。
- 设置生命周期属性: 比如过期时间、最大使用次数、允许的操作类型。
这些信息通常存在密钥的元数据里,和密钥本身一起密封存储。
2.2 密钥存储:把钥匙锁进保险柜
密钥生成完了,接下来就是存。存密钥这件事,说白了就是回答两个问题:存在哪?怎么存?
2.2.1 存储位置
TEE 环境里,密钥的存储位置分三个层级:
| 层级 | 存储位置 | 安全等级 | 典型用途 |
|---|---|---|---|
| L1 | TEE 内部安全内存(SRAM/OTP) | 最高 | 根密钥、平台密钥 |
| L2 | TEE 密封存储(加密后写入 Flash) | 高 | 应用密钥、会话密钥 |
| L3 | 外部安全元件(SE) | 极高 | 支付密钥、身份密钥 |
我个人的经验是:根密钥永远放在 L1,用 OTP(一次性可编程存储器)或者 eFuse 来存。应用密钥走 L2,用 TEE 的密封服务加密后存到 Flash 里。至于 L3,那是给金融级场景准备的,普通 IoT 设备用不上。
2.2.2 密封存储机制
密封存储(Sealed Storage)是 TEE 的看家本领。它的核心逻辑是:用 TEE 独有的根密钥,加密你的应用密钥,然后存到外部存储里。
解密的时候,只有同一个 TEE 实例(或者同一个设备上的同一个 TEE)才能解开。别人拿到加密后的密钥 blob,一点用都没有。
2.2.3 密钥导出策略
有时候,我们需要把密钥导出到另一个安全域(比如从 TEE 导出到 SE)。这时候要格外小心。我曾经见过一个方案:直接把密钥明文通过共享内存传出去——那跟把密码写在便利贴上贴电脑屏幕上一个意思。
正确的做法是:
- 在目标安全域(SE)生成一个临时公钥。
- TEE 用这个公钥加密要导出的密钥。
- 密文传给 SE,SE 用私钥解密。
- 整个过程密钥明文不出 TEE 和 SE 的安全边界。
2.3 密钥使用:用的时候最危险
密钥在存储状态是安全的,一旦开始使用,风险就来了。为什么?因为使用意味着密钥要参与运算,要出现在 CPU 寄存器、缓存、内存里。侧信道攻击、故障注入、时序分析,全盯着这个阶段。
2.3.1 使用边界
TEE 里使用密钥,有一条铁律:密钥明文永远不出 TEE 的安全边界。
具体来说:
- 加密/解密操作在 TEE 内部完成。
- 签名/验签操作在 TEE 内部完成。
- 密钥派生操作在 TEE 内部完成。
- 外部只能拿到运算结果(密文、签名值),拿不到密钥本身。
你可能会问:「那如果我的业务逻辑在普通世界,怎么用 TEE 里的密钥?」答案是:通过 TEE 提供的安全 API。你把要加密的数据传给 TEE,TEE 用密钥加密后把密文返回给你。整个过程你碰不到密钥。
2.3.2 使用过程中的防护
我在项目中踩过一个坑:TEE 里的 AES 引擎没有做时序恒定实现。攻击者通过分析加密操作的耗时,就能推断出密钥的部分比特。嗯,从那以后,我对所有密码学实现的审查都加了一条——必须恒定时间。
使用阶段的防护措施,我总结成一张表:
| 威胁 | 防护手段 | 说明 |
|---|---|---|
| 侧信道(时序/功耗) | 恒定时间实现、掩码 | 所有密码运算执行时间与密钥无关 |
| 故障注入 | 双重计算、校验和 | 运算结果做两次,对比一致性 |
| 缓存泄露 | 专用安全内存、缓存刷洗 | 使用后立即清除缓存中的密钥痕迹 |
| 调试接口泄露 | 熔断 JTAG/SWD | 量产设备必须永久禁用调试接口 |
2.3.3 使用计数与过期
密钥不能无限次使用。我习惯给每个密钥设置一个使用计数器和过期时间。比如:
- 会话密钥:最多使用 1000 次,或者 24 小时后过期。
- 签名密钥:最多签 10000 次,或者 30 天后过期。
- 根密钥:不直接使用,只用于派生子密钥。
一旦计数器达到上限或者密钥过期,TEE 会自动标记该密钥为「不可用」。下次有人想用它,直接返回错误。
2.4 密钥销毁:善终比善始更难
密钥销毁是生命周期里最容易被忽视的一环。很多人觉得「删掉文件不就行了?」——其实远远不够。
2.4.1 销毁的层次
密钥销毁分三个层次:
- 逻辑销毁: 删除密钥的元数据、标记为无效。这是最基础的,但不够。
- 物理销毁: 覆写存储密钥的物理介质。对于 Flash,要做整块擦除;对于 SRAM,要写全 0 再写全 1 再写随机数。
- 不可逆销毁: 对于 OTP/eFuse 里的根密钥,物理上无法擦除。这时候只能通过熔断密钥派生路径来「杀死」它。
我个人的标准是:至少做到物理销毁。逻辑销毁只能防君子,防不了攻击者。
2.4.2 安全擦除的实践
在 TEE 环境里,销毁密钥的典型流程是这样的:
// 伪代码:TEE 安全密钥销毁流程
void secure_destroy_key(key_handle_t handle) {
// 1. 验证调用者权限
if (!check_caller_permission()) return ERROR_ACCESS_DENIED;
// 2. 从安全内存加载密钥
uint8_t key[32];
load_key_from_secure_memory(handle, key);
// 3. 覆写密钥缓冲区(三次覆写)
memset(key, 0x00, 32); // 第一次:全0
memset(key, 0xFF, 32); // 第二次:全1
memset(key, 0xA5, 32); // 第三次:随机模式
// 4. 清除缓存和寄存器痕迹
clean_cache_line((uint32_t)key);
clean_register();
// 5. 擦除密封存储中的密钥 blob
erase_sealed_storage(handle);
// 6. 更新密钥状态为「已销毁」
update_key_status(handle, KEY_STATUS_DESTROYED);
}
2.4.3 紧急销毁场景
有些场景下,你需要一键销毁所有密钥。比如设备检测到物理入侵、或者远程收到销毁指令。
这时候,最有效的做法是:销毁根密钥。根密钥一没,所有派生密钥、密封密钥 blob 全部变成不可解密的垃圾。这比一个个销毁应用密钥快得多。
我记得有个客户做金融 POS 机,他们的设计是:机壳防拆开关一触发,直接给 TEE 发一个「自杀信号」,TEE 收到后立即擦除 OTP 里的根密钥。整个过程不到 50 毫秒。嗯,这才叫「死得干净」。
2.5 密钥轮换:生命周期的闭环
密钥轮换不是独立的阶段,它是「销毁旧密钥 → 生成新密钥」的循环。轮换策略通常分两种:
| 轮换策略 | 触发条件 | 适用场景 |
|---|---|---|
| 时间驱动 | 固定周期(如 90 天) | 长期密钥、身份密钥 |
| 事件驱动 | 使用次数超限、安全事件 | 会话密钥、临时密钥 |
我个人更推荐事件驱动 + 时间驱动混合。既设一个最长有效期,也设一个最大使用次数。哪个先到,就触发轮换。
轮换的时候要注意:旧密钥不能立即销毁。因为可能还有用旧密钥加密的数据没解密完。正确的做法是:
- 生成新密钥,开始用新密钥加密新数据。
- 旧密钥保留一段时间(称为「宽限期」),只用于解密旧数据。
- 宽限期结束后,销毁旧密钥。