4、可信平台模块(TPM)实战:TPM规范与功能、TPM密钥层次结构、使用TPM生成和存储根密钥
聊到根密钥的物理存储,TPM 是个绕不开的话题。我最早接触 TPM 是在做一款金融安全终端的时候,当时厂商给的文档厚得像本字典,读完后最大的感受是——这东西真能藏东西,但用起来也真讲究。今天咱们就把它拆开看看。
4.1 TPM 规范与核心功能
TPM,全称 Trusted Platform Module,说白了就是一个焊在主板上的专用安全芯片。它有自己的处理器、非易失性存储、甚至独立的随机数发生器。你想想看,密钥放在操作系统里,再强的加密也怕内存 dump;但放在 TPM 里,连操作系统都读不到它的内部状态。
TPM 的规范由 TCG(Trusted Computing Group)维护,目前主流的是 TPM 2.0。相比 1.2 版本,2.0 最大的变化是算法灵活性——不再死磕 SHA-1 和 RSA,而是支持了 ECC、AES、SM3/SM4 等更多选择。
TPM 的核心能力就三件事:
- 密钥生成与存储——在芯片内部生成密钥,私钥永远不离开 TPM
- 平台完整性度量——通过 PCR(平台配置寄存器)记录系统启动状态
- 密封与证明——将数据绑定到特定软硬件状态,只有状态匹配才能解封
我个人习惯把 TPM 理解成一个「保险箱」:你把密钥放进去,它给你一个句柄(handle),以后你拿着句柄去操作密钥,但永远看不到密钥本身。嗯,这里要注意——TPM 不是用来做高速运算的,它的加解密速度远不如 CPU,但它胜在安全隔离。
4.2 TPM 密钥层次结构
这是 TPM 最核心的设计,也是很多人容易搞混的地方。TPM 的密钥不是平铺的,而是分层的树状结构。我画了一张图,你看完就明白了。
这张图里最关键的是最顶层的 SRK(Storage Root Key,存储根密钥)。SRK 在 TPM 出厂时或首次初始化时生成,私钥永远锁死在芯片内部。它下面的所有密钥,私钥都是被 SRK 加密后存在硬盘上的——这叫「密钥封装」。你每次要用某个子密钥,TPM 会先用 SRK 解密,加载到芯片内部使用,用完再封存回去。
我的经验: 在项目中,我建议把 SRK 的密码(authValue)单独保存在离线密码保险柜里。一旦丢失,所有子密钥都无法加载——我曾经见过一个团队因为把 SRK 密码写在代码注释里,结果代码泄露后整个密钥体系报废。
4.3 使用 TPM 生成和存储根密钥
好了,理论讲完,咱们上手实操。下面我用 tpm2-tools 演示如何在 TPM 中生成根密钥并存储。这套工具是 Linux 下操作 TPM 2.0 的事实标准。
4.3.1 检查 TPM 可用性
先确认你的机器有没有 TPM,以及驱动是否正常加载。
# 检查 TPM 设备是否存在
ls /dev/tpm* # 应该看到 /dev/tpm0 或 /dev/tpmrm0
# 查看 TPM 版本和厂商信息
tpm2_getcap properties-fixed
# 检查是否已存在 SRK
tpm2_getcap handles-persistent
如果输出为空,说明 TPM 还没初始化。我第一次在服务器上跑这个命令时,发现 TPM 被 BIOS 禁用了——折腾了半天才发现是主板设置的问题。所以,先检查 BIOS 里 TPM 有没有开启。
4.3.2 创建存储根密钥(SRK)
SRK 的创建是一次性的。TPM 2.0 支持多种算法,我个人推荐用 ECC,性能更好,密钥更短。
# 创建 ECC 类型的 SRK,持久化到句柄 0x81000001
tpm2_createprimary -C o -G ecc -g sha256 \
-a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|decrypt" \
-p "your_srk_password" \
-c srk.ctx
# 将 SRK 持久化到 TPM 的 NVRAM 中
tpm2_evictcontrol -C o -c srk.ctx 0x81000001
这里解释几个关键参数:
-C o:使用 TPM 所有者授权(platform hierarchy)-G ecc:密钥算法为 ECC(NIST P-256)-a "...":密钥属性,决定了密钥的用途和限制-p:SRK 的授权密码,务必妥善保管
警告: 持久化句柄 0x81000001 是 SRK 的常用约定句柄。但不同厂商的 TPM 可能占用不同范围,建议先查一下可用句柄范围(tpm2_getcap handles-persistent)。我曾经在 Intel 的 TPM 上用了 0x81000000,结果和 BIOS 预留的冲突,导致系统启动时 TPM 报错。
4.3.3 在 SRK 下创建子密钥
有了 SRK,我们就可以创建受它保护的子密钥了。下面创建一个用于数据加密的存储密钥。
# 加载 SRK 到 TPM 会话
tpm2_load -C 0x81000001 -u srk.pub -r srk.priv -c srk_load.ctx
# 在 SRK 下创建子密钥(存储密钥)
tpm2_create -C srk_load.ctx -G aes -g sha256 \
-a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt" \
-p "child_key_password" \
-u child.pub -r child.priv
# 加载子密钥到 TPM
tpm2_load -C srk_load.ctx -u child.pub -r child.priv -c child.ctx
注意看,子密钥的私钥文件 child.priv 是加密后存在磁盘上的。没有 SRK 和正确的密码,谁也解不开。这就是 TPM 的「密钥层次」在实战中的体现。
4.3.4 使用子密钥加密数据
密钥生成好了,怎么用?下面演示用子密钥加密一段敏感数据。
# 准备要加密的数据
echo "This is my root key seed material" > secret.txt
# 使用子密钥加密(对称加密)
tpm2_encryptdecrypt -c child.ctx -o secret.enc secret.txt
# 解密验证
tpm2_encryptdecrypt -c child.ctx -d -o secret.dec secret.enc
cat secret.dec
整个过程,AES 密钥只在 TPM 内部使用,外部永远看不到明文密钥。你想想看,就算攻击者拿到了 child.priv 和 secret.enc,没有 SRK 和密码,他什么都做不了。
4.4 实战中的避坑指南
做 TPM 开发这几年,我踩过不少坑。下面列几个最常见的,你遇到了能少走弯路。
| 问题场景 | 原因分析 | 解决方案 |
|---|---|---|
| TPM 返回 TPM_RC_NV_DEFINED | NVRAM 索引已被占用 | 换一个索引,或先释放旧的 |
| 加载子密钥时密码错误 | authValue 不匹配 | 创建时用 -p 设置,加载时用 -P 传入 |
| SRK 持久化失败 | 句柄冲突或权限不足 | 检查 TPM 所有者授权是否启用 |
| 加密数据无法在其他机器解密 | 子密钥绑定到特定 TPM | 这是设计特性,需迁移密钥时用 tpm2_duplicate |
我的习惯: 每次创建密钥后,立即用 tpm2_getcap handles-persistent 确认持久化成功,并把句柄和用途记录到密钥清单中。项目大了之后,密钥一多,光靠脑子记肯定乱套。
4.5 小结
TPM 的密钥层次结构,说白了就是「一级保一级」的信任链。SRK 是锚点,它锁在芯片里;子密钥由 SRK 保护,存在磁盘上。这种设计的好处是:即使硬盘被拔走,密钥依然是安全的。坏处是:一旦 SRK 损坏或密码丢失,所有子密钥都跟着报废。
在实际项目中,我建议把 TPM 生成的根密钥作为「密钥加密密钥」(KEK),真正的业务密钥再由 KEK 派生。这样即使业务密钥需要轮换,根密钥可以保持不变。嗯,这个思路在后面的章节里还会深入展开。