适用场景:使用 systemd-cryptenroll 将 LUKS2 密钥封存到 TPM2 实现免密码自动解锁,因 Secure Boot 状态、固件、或 Windows 内核安全设置变化导致 PCR 测量值改变,自动解锁失效。
背景:
TPM 自动解锁的原理是把 LUKS 密钥封存(seal)到 TPM 芯片内,并用一组 PCR(Platform Configuration Register) 值作为解封条件。每次启动时,固件、引导链、内核都会向特定 PCR “测量”自身状态。只有当 PCR 值与封存时完全一致,TPM 才会释放密钥。
常见 PCR 含义(部分):
| PCR | 测量内容 | 变化触发 |
|---|---|---|
| 0 | 固件代码 | BIOS/UEFI 更新 |
| 2 | 可选 ROM | 显卡固件、外设固件更新 |
| 4 | 引导加载器 | bootloader 更换 |
| 7 | Secure Boot 状态与策略 | 开关 SB、DBX 更新、密钥变更 |
| 8/9 | 内核命令行、initramfs | 内核或 initramfs 更新 |
| 11 | UKI(Unified Kernel Image) | UKI 内容变化 |
PCR 7 是最常用的绑定目标。它只关心 Secure Boot 是否启用以及策略是否被篡改,对常规系统更新足够宽容。
触发失效的典型操作
最常见的几类原因,按”为什么会动到 PCR 7″分类:
直接动 Secure Boot 配置
- BIOS 中关闭再开启 Secure Boot
- 更换 / 重新登记 Secure Boot 密钥(PK/KEK/db)
- Windows 推送的 DBX 撤销列表更新(Microsoft 不定期通过 KB 推送)
Windows 改了引导配置,间接触发 EFI 变量变化
这是双系统用户最容易出现的问题,也是本文作者本次的问题所在。比如:
- 为了开/关 Hyper-V、WSL2、虚拟机平台、Windows 沙盒,调整了核心隔离 / 内存完整性 / VBS / HVCI
- 切换 hypervisor 启动模式(
bcdedit /set hypervisorlaunchtype) - Windows 大版本更新
这类操作本身不直接进 PCR 7。但 Windows 检测到安全配置变化后会自动修改启动链配置,重写 BCD、调整 winload.efi 的加载流程、写入 EFI 变量(BootOrder、BootXXXX 等)。EFI 变量变化会被测量进 PCR 7,导致 Linux 这边的 TPM 解锁失效。
固件 / 硬件层面
- BIOS / UEFI 固件更新(同时影响 PCR 0 和可能影响 PCR 7)
- 进入 BIOS Setup 后保存退出(部分主板会触发)
Windows 的 BitLocker 也用 TPM,但走的是独立的 NV index 和密钥封装机制,两套互不干扰。修复 Linux 的 TPM 解锁不会影响 BitLocker,反之亦然。所以双系统场景下,Windows 调整安全设置后通常 BitLocker 自己也会要求恢复密钥一次,恢复后两边都能正常工作。
作者配置环境:UKI + 签名 PCR 策略
本文以一种较为完善的 Arch 加密启动方案为参考。如果你的配置更简单(比如 GRUB + 传统 initramfs + 纯 PCR 绑定),原理相同,只是少了 PCR 11 签名那部分。
组件构成
| 组件 | 作用 |
|---|---|
mkinitcpio + sd-encrypt hook |
生成 initramfs,启动时通过 systemd 解锁 LUKS |
| UKI(Unified Kernel Image) | 把 kernel + initramfs + cmdline + osrel 打包成单个签名的 .efi |
| sbctl | 管理用户的 Secure Boot 密钥,自动给 UKI 签名 |
| systemd-measure | 用私钥对 UKI 的预期 PCR 11 值签名,生成 .pcrsig |
| systemd-cryptenroll | 把 LUKS 密钥封存到 TPM,绑定 PCR 7 + PCR 11 签名策略 |
| systemd-boot | 直接加载 /boot/EFI/Linux/ 下的 UKI,无需配置文件 |
启动链与信任传递
UEFI 固件
└─ Secure Boot 验证(PCR 7 测量 SB 状态)
└─ systemd-boot(sbctl 签名)
└─ UKI(sbctl 签名,PCR 11 测量内容)
└─ initramfs 内 systemd
└─ 向 TPM 请求解封 LUKS 密钥
├─ 检查 PCR 7 == 封存时的值? ← 硬绑定
└─ 检查 PCR 11 是否有合法签名? ← 签名策略
└─ 两个条件都满足 → TPM 释放密钥
└─ LUKS 解锁 → 挂载根分区
双重 TPM 策略的设计意图
LUKS2 token 里同时包含两类策略:
tpm2-hash-pcrs: 7(硬绑定)- 必须当前 PCR 7 == 封存时的值
- 防御 Secure Boot 被关闭、密钥被篡改等攻击
- 代价:Secure Boot 状态变化就要重新 enroll
tpm2-pubkey-pcrs: 11+tpm2-pubkey(签名策略)- 不要求 PCR 11 等于固定值
- 但要求”存在一个由
tpm2-pubkey对应私钥签名的、声明此 PCR 11 值合法”的签名 - 签名以
.pcrsig文件形式由 mkinitcpio 在重建 UKI 时自动生成 - 代价:私钥必须妥善保管
关键收益:内核更新后 UKI 的 PCR 11 会变,但 mkinitcpio 的 sbctl post-hook 会自动重新签名。只要内核是你自己签的、Secure Boot 状态没变,自动解锁就持续有效,无需人工 re-enroll。
关键文件位置
/etc/tpm/pcr_policy_private.key # PCR 签名私钥(绝不能泄露)
/etc/tpm/pcr_policy_public.key # PCR 签名公钥(嵌入 LUKS token)
/etc/kernel/uki.conf # ukify 配置,指定签名密钥路径
/etc/mkinitcpio.d/*.preset # 各内核的 UKI 输出路径配置
/boot/EFI/Linux/*.efi # 生成的 UKI 文件
/var/lib/sbctl/keys/ # sbctl 的 Secure Boot 签名密钥
mkinitcpio -P 内核更新自动重新计算PCR 11 值
输出里这一行是核心:
+ /usr/lib/systemd/systemd-measure sign \
--linux=/boot/vmlinuz-linux \
--initrd=/tmp/mkinitcpio.xxx \
--pcrpkey=/etc/tpm/pcr_policy_public.key \
--private-key=/etc/tpm/pcr_policy_private.key \
--public-key=/etc/tpm/pcr_policy_public.key \
--bank=sha256 --phase=enter-initrd
systemd-measure 计算这个 UKI 在 enter-initrd 阶段的预期 PCR 11 值,用私钥签名后嵌入 UKI 的 .pcrsig 节区。然后 sbctl post-hook 用 Secure Boot 密钥再签一次外层 PE 签名。
启动时,systemd 在 initramfs 里读取 .pcrsig,连同当前 PCR 11 值一起提交给 TPM。TPM 验证签名合法且声明的 PCR 11 与当前一致后,按签名策略释放密钥。
修复流程
第 1 步:用 LUKS 密码手动启动
自动解锁失败时,输入当初创建 LUKS 时设置的恢复密码进入系统。
第 2 步:定位加密分区
lsblk -f
sudo cryptsetup status <你的 mapper 名称>
输出里 device: 一行就是底层分区,例如 /dev/nvme1n1p2。后续命令以此为例。
第 3 步:查看当前 TPM 绑定参数(重要)
sudo cryptsetup luksDump /dev/nvme1n1p2
在 Tokens: 段查找 systemd-tpm2 类型的 token,记下:
tpm2-hash-pcrs:硬绑定的 PCR 列表(常见为7)tpm2-pubkey-pcrs:公钥签名策略的 PCR(常见为11,仅 UKI + 签名策略方案有此字段)tpm2-pubkey:是否存在(决定是否需要--tpm2-public-key参数)
同时确认现有密钥槽:
sudo systemd-cryptenroll /dev/nvme1n1p2
应至少有一个 password 类型的槽作为后备——不要在没有密码后备槽的情况下清除 TPM 槽。
第 4 步:清除旧 TPM 槽
sudo systemd-cryptenroll --wipe-slot=tpm2 /dev/nvme1n1p2
第 5 步:用相同参数重新登记
方案 A:纯 PCR 绑定(最常见的 systemd-cryptenroll 默认配置)
sudo systemd-cryptenroll \
--tpm2-device=auto \
--tpm2-pcrs=7 \
/dev/nvme1n1p2
方案 B:PCR + 公钥签名策略(UKI + sbctl 用户常见配置)
sudo systemd-cryptenroll \
--tpm2-device=auto \
--tpm2-pcrs=7 \
--tpm2-public-key=/etc/tpm/pcr_policy_public.key \
--tpm2-public-key-pcrs=11 \
/dev/nvme1n1p2
--tpm2-pcrs= 后面的值要和 luksDump 里看到的 tpm2-hash-pcrs 一致。如果原来是 0+7 就写 0+7。
执行时会要求输入一个现有的 LUKS 密码做授权。
第 6 步:验证
sudo systemd-cryptenroll /dev/nvme1n1p2
应出现新的 tpm2 槽(槽号不一定和原来相同,无影响)。重启测试自动解锁。
配置文件查找指南
不同的 Arch 配置方案,根分区的 TPM 解锁参数存放位置不同:
| 配置方案 | 参数位置 |
|---|---|
传统 /etc/crypttab.initramfs + mkinitcpio |
/etc/crypttab.initramfs 对应行的 options |
| sd-encrypt hook + 内核命令行 | /proc/cmdline 或 /etc/kernel/cmdline 里的 rd.luks.* 参数 |
| 任意方案 | cryptsetup luksDump 输出的 Tokens 段 |
当不确定时,以 luksDump 的 Token 元数据为准,这是 LUKS 头部里实际生效的配置。
PCR 选择建议
| 选择 | 安全性 | 稳定性 | 适用场景 |
|---|---|---|---|
| 仅 7 | 中等(防 evil maid) | 高 | 推荐默认,平衡安全与维护成本 |
| 0+7 | 较高 | 中等,固件更新需重做 | 在意固件被替换的场景 |
| 7 + 公钥签名 11 | 高 | 高(内核更新自动适配) | UKI + sbctl 方案,最稳健 |
| 加 8/9 | 高 | 低,每次内核更新都要重做 | 不推荐,除非用 UKI + PCR 11 |
公钥签名策略(--tpm2-public-key-pcrs)的精妙之处在于内核更新后,systemd-measure sign 会用私钥对新 UKI 的预期 PCR 11 值签名生成 .pcrsig,TPM 凭签名释放密钥。这样只要内核是你签的、Secure Boot 状态没变,就能持续自动解锁,无需每次重新 enroll。
故障排查
重新登记后仍要输密码
- 再次发生 PCR 7 变化:检查是否在重启过程中又动了 BIOS/Secure Boot。重复修复流程。
- initramfs 缺少 TPM 支持:确认
/etc/mkinitcpio.confHOOKS 包含sd-encrypt,并已安装tpm2-tss包。 - 公钥签名策略下的 .pcrsig 失效:执行
sudo mkinitcpio -P重建并重新签名。
查看实际 PCR 值
sudo systemd-analyze pcrs
# 或
cat /sys/class/tpm/tpm0/pcr-sha256/7
可以对比修复前后 PCR 7 的变化,确认是否是 PCR 漂移导致。
TPM 完全不可用
systemctl status systemd-cryptsetup@*.service
journalctl -b | grep -i tpm
如果显示 TPM 设备不存在,检查 BIOS 里 TPM/PTT/fTPM 是否启用。
安全性说明
本流程涉及的所有 luksDump 输出(包括 token 元数据、salt、policy hash、TPM blob、公钥)均可安全公开。LUKS 与 TPM 的安全性建立在密钥保密上,配置元数据公开不削弱安全性。
绝不能公开的内容:
- LUKS passphrase 本身
- 公钥签名策略的私钥(如
/etc/tpm/pcr_policy_private.key) - 任何 keyfile 文件内容
- sbctl 的 Secure Boot 签名私钥