第四章 · 实验 B:Seal/Unseal——把秘密绑在环境状态上
本章目标
上一章验证了 PCR 哈希链的真实性,但留下了一个尖锐的问题:记录历史有什么用?如果篡改者照样能进系统、照样能读数据,度量岂不成了摆设?
这一章回答这个问题。我们要用 TPM 最实用的能力——封存(Seal)与解封(Unseal):把一个秘密加密存放在 TPM 的保护之下,并且附加一个条件——只有当指定的 PCR 还保持封存时的值,秘密才取得出来。启动链被改动哪怕一个字节,PCR 值就会变,秘密就永久锁死。
这就是 Windows BitLocker、Linux 下 systemd-cryptenroll --tpm2 自动解锁全盘加密背后的原理。走完本章流程,你对“开机自动解锁但又防得住偷硬盘“这件事就不会再觉得神秘了。
背景知识:Seal 是怎么回事
封存的输入有三样:
- 要保护的秘密:一段不超过 128 字节的数据(TPM 内部对象的上限)。实际系统里封存的一般不是文件本体,而是一个对称密钥——比如 LUKS 的卷密钥。
- 一个父密钥:秘密最终被 TPM 里的密钥加密保护。我们通常先创建一个主密钥(primary key)当父密钥。
- 一个策略(policy):本章用的是 PCR 策略——“只有 PCR 的值等于某某,才允许解封”。
产出的密封信封(sealed object)是两个普通文件,里面装着加密后的秘密和元数据。信封本身可以随便拷贝、随便备份,偷走也没用——没有“正确的 PCR 状态 + 原来那颗 TPM“,它就是一堆随机数。
解封时,TPM 会先检查策略:当前 PCR 值算出的策略摘要和信封里记录的摘要比对,一致才放行密钥材料。注意,检查发生在芯片内部,操作系统只能问“行不行“,无法绕过去。
有人第一次接触 Seal 时会问:这和普通加密有什么区别?区别在于解密的条件。普通加密的条件是“你有没有密钥“——密钥偷走了,防护就没了。封存的条件是“你是不是在那颗芯片上、运行着那个没被动过的环境“——密钥本身根本不以文件形式存在,偷无可偷。TPM 把“密钥保管“这个最难的环节,替换成了“环境度量“这个可验证的环节。
思考:这里要分清两件事。度量(第三章)是“记录“,它本身不拦任何人;封存是“执行“,它把秘密的可用性押在度量结果上。可信启动的防护力,全部来自这两者咬合在一起。
准备工作
建个工作目录,回到 PCR 16 的“干净状态“(全零):
mkdir -p ~/tpm-lab && cd ~/tpm-lab
sudo tpm2_pcrreset 16
sudo tpm2_pcrread sha256:16
sha256:
16 : 0x0000000000000000000000000000000000000000000000000000000000000000
这里把 PCR 16 当道具用:它是可复位的调试寄存器,我们可以随时手动 extend 它来模拟“启动环境被篡改“,不用真的去改引导文件重启机器。真实系统绑定的是 PCR 7、PCR 11 这类不可复位的寄存器,道理完全一样,最后再说区别。
思考:为什么这就算“模拟篡改“?回忆第三章:启动链上任何一环被改动,对应的 PCR 就会被 extend 进一个不同的哈希,最终值必然不同。我们手动 extend 一次,制造的效果和“恶意程序混进启动链“在 TPM 眼里一模一样——都是 PCR 值偏离了封存时的状态。TPM 不需要知道恶意程序长什么样,这正是这套机制省力的地方。
第一步:创建主密钥
sudo tpm2_createprimary -C o -c primary.ctx
-C o 表示在 owner 层级(hierarchy)下创建,-c primary.ctx 把密钥上下文存成文件。输出较长,关键部分:
name-alg:
value: sha256
raw: 0xb
...
sym-keybits: 128
...
这个命令有个反直觉的性质值得讲透:你在同一颗 TPM 上重复执行它一万次,得到的都是同一把主密钥。主密钥不是随机生成的,而是从 TPM 出厂时烧进去的种子(seed)确定性派生出来的。这意味着:
- 主密钥不需要备份——它永远在芯片里,想要的时候重新派生一次就行;
- 换一颗 TPM,种子不同,主密钥就完全不同,旧芯片上封存的信封立刻作废。这是特性不是缺陷:秘密跟着芯片走。
primary.ctx 文件里存的只是这把密钥的句柄和公钥信息,不是私钥本体。私钥从生到死不出芯片。
顺带解释 -C o 里的“层级“:TPM 内部有几个权限域,owner 层级是日常使用的一个(还有 endorsement、platform 等,各有用途,第七章会展开)。现阶段只要知道“在 owner 层级下创建“是常规操作即可,不影响理解本章流程。
第二步:创建 PCR 策略
读出 PCR 16 的当前值(现在是全零),基于它创建策略:
sudo tpm2_createpolicy --policy-pcr -l sha256:16 -L policy.pcr16
-l sha256:16 指定锁定哪个 PCR,-L policy.pcr16 把策略摘要存成文件。这条命令做的事情是:读取 PCR 16 此刻的值,算出“只有它还是这个值才放行“的策略摘要。
policy.pcr16 里存的本质上是一个哈希值,可以叫它策略摘要(policy digest)。它由两部分信息决定:选了哪些 PCR、以及这些 PCR 此刻的值。之后封存时这个摘要被烙进信封,解封时 TPM 用同样的算法拿当时的 PCR 值重新算一遍摘要,两个摘要一致才放行。整个过程中信封里并不保存 PCR 值本身,只保存它的“指纹“——这也是为什么策略文件可以公开,它推不出任何秘密。
注意:策略绑定的是“值“,不是“数字 16 这个编号“。此刻 PCR 16 是全零,策略的含义就是“只有 PCR 16 还是全零才允许操作“。它什么时候变成别的值,策略就什么时候失效——至于为什么变、谁改的,TPM 不关心也不判断,它只认值。
第三步:封存秘密
echo -n "这是我的秘密" | sudo tpm2_create -C primary.ctx -L policy.pcr16 -u seal.pub -r seal.priv -i-
参数含义:
-C primary.ctx:用主密钥当父密钥保护这个信封;-L policy.pcr16:把上一步的策略烙进信封;-i-:从标准输入读秘密内容(echo -n的-n同样不能省);-u seal.pub -r seal.priv:输出信封的公钥部分和加密部分。
成功后会看到类似输出:
name-alg:
value: sha256
raw: 0xb
...
现在目录里的 seal.pub 和 seal.priv 就是密封信封。可以打开看一眼——十六进制的乱码,秘密本身一个字节都看不见。
坑:
tpm2_create能封存的秘密上限是 128 字节,超过会直接报size mismatch之类的错误。别想着把整份文件塞进去——正确姿势是封一个随机生成的对称密钥,再用这个密钥在 TPM 之外加密数据本体。真实系统里封存的就是 LUKS 卷密钥这类东西,第六章会完整演示。
第四步:正常解封
解封分两步:先把信封加载进 TPM,再请求解封:
sudo tpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctx
sudo tpm2_unseal -c seal.ctx -p pcr:sha256:16
第二条的 -p pcr:sha256:16 是声明授权方式:“这次操作我要用 PCR 16 策略来授权”。TPM 收到后检查当前 PCR 16 的值——还是全零,和策略吻合——放行。
注意:
-p传的不是密码,是“授权方式的声明“。它告诉 TPM 这次会话用哪套规则来验证;真正的验证数据(PCR 当前值)由 TPM 自己去芯片里读,你给不了也给不了假的。这和传统口令认证“你说什么就信什么“的思路完全不同。
输出:
这是我的秘密
秘密原样吐出来了。环境没变,锁就开。
第五步:模拟篡改
现在扮演攻击者。假设有人在启动链里动了手脚——往引导环节植入了一段恶意程序。在我们的道具实验里,这等价于 PCR 16 被 extend 了一次:
sudo tpm2_pcrextend 16:sha256=$(echo -n "被植入恶意引导程序" | sha256sum | cut -d' ' -f1)
再试解封:
sudo tpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctx
sudo tpm2_unseal -c seal.ctx -p pcr:sha256:16
这次 TPM 的回答是:
ERROR: Esys_Unseal(0x99D) - tpm:session(1):a policy check failed
a policy check failed——策略检查失败,拒绝。没有提示“是不是只改了一个字节“,没有“是否强制继续“的选项。TPM 不做善意推断,值不对就是不对。信封还在你手里,-p 参数你也照样传了,但秘密就是出不来。
坑:第一次踩这个报错时,我对着
0x99D查了半小时,以为是参数传错了。其实就是字面意思:PCR 值和策略不一致。这个错误码在后续的实验和真实排障中会反复出现,记住它的含义能省很多时间。
第六步:恢复环境,再次解封
把 PCR 16 复位回全零(恢复“干净的启动环境“),再试:
sudo tpm2_pcrreset 16
sudo tpm2_load -C primary.ctx -u seal.pub -r seal.priv -c seal.ctx
sudo tpm2_unseal -c seal.ctx -p pcr:sha256:16
这是我的秘密
又成功了。策略检查的是当前值,值回来了,锁就开——TPM 不记仇。
这说明什么
流程走完了,但有几个推论必须掰开说清楚,它们直接关系到你怎么理解真实系统的安全性。
第一,我们能“恢复“,纯属道具的特殊照顾。 PCR 16 是可复位的调试寄存器,所以我们篡改完还能按回去。真实系统绑定的是 PCR 7、PCR 11 这类运行时不可复位的寄存器(第三章已经验证过,root 也清不掉它们)。攻击者改了启动链之后,没有任何手段把 PCR 改回原值——extend 不可逆,重置被拒绝,重启会重走一遍被篡改的启动链、留下一样的痕迹。等待他的不是“解封失败,请重试“,而是秘密永久锁死。
第二,信封文件丢了也不心疼。 seal.pub 和 seal.priv 可以任意拷贝、备份、放在 U 盘里随身带。攻击者偷走它们,面对的依然是两道锁:正确的 PCR 状态(得原样复现你的启动链)和原来那颗 TPM(主密钥的种子烧在芯片里)。两个条件缺一不可,而这两个条件他一个都带不走。这就是为什么 BitLocker 加密的硬盘拆下来插到别的机器上就是一块砖头。
第三,主密钥不依赖任何文件。 我们从头到尾没有备份过密钥,因为不需要——主密钥从种子确定性派生,芯片不换它就一直在。这意味着防护体系的根不在操作系统的文件系统里,文件系统被整个替换、被离线改写,都伤不到它。
第四,“自动解锁“和“安全“并不矛盾。 这是很多人初接触时的直觉障碍:开机不输密码就能解锁磁盘,岂不是谁开机都能读数据?现在你能回答了——解锁的前提是 PCR 策略通过,也就是固件、引导器、内核都和绑定时一致。主人正常开机,环境没变,自动解锁;攻击者换个引导 U 盘、改个内核参数、拆掉硬盘挂到别处,PCR 或芯片必有一项对不上,秘密出不来。便利性来自“对主人透明”,安全性来自“对篡改零容忍“,两者由同一把锁实现。
思考:这套机制有一个诚实的边界——它防的是“启动环境被改动“,防不了“合法环境自己做坏事“。如果恶意软件是在度量之后的系统里正常跑起来的,PCR 一切正常,秘密照样能解封。TPM 保证的是“你启动的就是你安装的那套东西“,至于那套东西本身可不可信,是另一个层面的问题(第九章的 IMA 会把度量延伸到运行时,部分回应这个问题)。
小结
- 封存 = 秘密 + 主密钥保护 + PCR 策略;解封时 TPM 在芯片内部检查策略,不一致就直接拒绝
- 主密钥从出厂种子确定性派生,不用备份,换芯片即作废
- 信封文件可自由备份,脱离“正确 PCR + 原 TPM“就是废数据
- 绑不可复位 PCR 时,篡改的后果是秘密永久锁死,没有回头路
到目前为止,PCR 16 一直是我们手里的道具。真实启动时,固件、引导加载器、内核到底往各个 PCR 里写了什么?那些值背后对应哪些文件、哪些事件?下一章我们解剖真实的度量日志,把启动链的每一步摊开看。