密钥生命周期管理:从生到死的安全闭环

聊到密钥安全,很多人第一反应就是「加密算法够强就行」。其实不然。我见过太多项目,算法选得挺硬,结果密钥在某个环节泄露了——生成时没熵、存储时裸奔、使用时不隔离、销毁时留尾巴。说白了,密钥的安全不是某一个点的安全,而是全生命周期的安全。

这一章,我就带你走一遍密钥从生到死的四个阶段:生成 → 存储 → 使用 → 销毁。每个阶段都有坑,咱们一个一个填。

密钥生命周期管理流程 密钥生成 密钥存储 密钥使用 密钥销毁 硬件熵源 / TRNG TEE 内部生成 安全存储 / 密封 加密导出 仅 TEE 内操作 不出安全边界 安全擦除 / 覆写 不可恢复 密钥轮换:销毁旧密钥 → 生成新密钥,形成闭环

2.1 密钥生成:起点决定终点

密钥生成是整个生命周期的起点。这个起点要是歪了,后面再怎么加固都没用。我个人习惯把生成阶段拆成三个关键点来检查。

2.1.1 熵源质量

随机数生成器(RNG)是密钥生成的「原材料」。TEE 环境里,我们通常用硬件真随机数生成器(TRNG)。为什么?因为软件伪随机数生成器(PRNG)的种子一旦被猜到,所有密钥就全废了。

我在项目中遇到过一件事:某厂商的 TEE 实现,TRNG 的熵源居然只依赖一个内部振荡器,没有做健康检测。结果在低温环境下,振荡器频率漂移,输出的随机数出现了明显的周期性。嗯,那批设备生成的密钥,我们后来全部强制轮换了。

我的建议: 生产环境中,一定要对 TRNG 做 在线健康检测(NIST SP 800-90B 的重复计数测试、自适应比例测试)。别信「硬件随机数肯定随机」这种话。

2.1.2 生成位置

密钥在哪里生成,决定了它的初始安全等级。TEE 环境里,密钥必须在安全世界(Secure World)内部生成,绝不能由普通世界(Normal World)生成后再导入。

你想想看,如果密钥在普通世界生成,那它在内存里已经裸奔过了。就算后面再塞进 TEE,也保不齐已经被侧信道或者内存嗅探给抄走了。

核心原则: 密钥生成必须发生在 TEE 内部,且使用硬件 TRNG 作为熵源。生成后的密钥明文,永远不允许离开 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,一点用都没有。

注意: 密封存储的密钥 blob 是绑定设备 + TEE 身份的。如果你把设备 A 的密封密钥 blob 拷贝到设备 B 上,B 的 TEE 是解不开的。这不是 bug,是 feature。

2.2.3 密钥导出策略

有时候,我们需要把密钥导出到另一个安全域(比如从 TEE 导出到 SE)。这时候要格外小心。我曾经见过一个方案:直接把密钥明文通过共享内存传出去——那跟把密码写在便利贴上贴电脑屏幕上一个意思。

正确的做法是:

  1. 在目标安全域(SE)生成一个临时公钥。
  2. TEE 用这个公钥加密要导出的密钥。
  3. 密文传给 SE,SE 用私钥解密。
  4. 整个过程密钥明文不出 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 会自动标记该密钥为「不可用」。下次有人想用它,直接返回错误。

一个小技巧: 使用计数器的存储要和密钥本身一起密封。我曾经见过有人把计数器存在普通 Flash 里,攻击者通过回滚计数器,让过期密钥「复活」了。嗯,这属于低级错误,但确实会发生。

2.4 密钥销毁:善终比善始更难

密钥销毁是生命周期里最容易被忽视的一环。很多人觉得「删掉文件不就行了?」——其实远远不够。

2.4.1 销毁的层次

密钥销毁分三个层次:

  1. 逻辑销毁: 删除密钥的元数据、标记为无效。这是最基础的,但不够。
  2. 物理销毁: 覆写存储密钥的物理介质。对于 Flash,要做整块擦除;对于 SRAM,要写全 0 再写全 1 再写随机数。
  3. 不可逆销毁: 对于 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);
}
警告: 不要只依赖 memset 来销毁密钥。编译器优化可能会把 memset 优化掉(因为「后面不再使用这块内存」)。建议使用 volatile 指针或者专门的 secure_memset 函数。

2.4.3 紧急销毁场景

有些场景下,你需要一键销毁所有密钥。比如设备检测到物理入侵、或者远程收到销毁指令。

这时候,最有效的做法是:销毁根密钥。根密钥一没,所有派生密钥、密封密钥 blob 全部变成不可解密的垃圾。这比一个个销毁应用密钥快得多。

我记得有个客户做金融 POS 机,他们的设计是:机壳防拆开关一触发,直接给 TEE 发一个「自杀信号」,TEE 收到后立即擦除 OTP 里的根密钥。整个过程不到 50 毫秒。嗯,这才叫「死得干净」。

2.5 密钥轮换:生命周期的闭环

密钥轮换不是独立的阶段,它是「销毁旧密钥 → 生成新密钥」的循环。轮换策略通常分两种:

轮换策略 触发条件 适用场景
时间驱动 固定周期(如 90 天) 长期密钥、身份密钥
事件驱动 使用次数超限、安全事件 会话密钥、临时密钥

我个人更推荐事件驱动 + 时间驱动混合。既设一个最长有效期,也设一个最大使用次数。哪个先到,就触发轮换。

轮换的时候要注意:旧密钥不能立即销毁。因为可能还有用旧密钥加密的数据没解密完。正确的做法是:

  1. 生成新密钥,开始用新密钥加密新数据。
  2. 旧密钥保留一段时间(称为「宽限期」),只用于解密旧数据。
  3. 宽限期结束后,销毁旧密钥。
一句话总结: 密钥生命周期管理,就是确保密钥在生成时有足够的熵、存储时有可靠的壳、使用时不出安全边界、销毁时不留痕迹。这四个环节,环环相扣,缺一不可。