keelOS: 虚拟机的拦路虎——Win11 需要 TPM 2.0
Windows 11 不认没有 TPM 2.0 的机器,虚拟机也一样。这篇讲 TPM 是什么、各家虚拟化平台怎么给虚拟机造一块"软 TPM",以及 keelOS 是怎么做的:尤其是在 keelOS 里,宿主系统重启时虚拟机不关机,TPM 的状态也得跟着活下来。
Windows 11 为什么非要 TPM
TPM(Trusted Platform Module)是主板上的一个小安全芯片,能做三件软件自己做不好的事:
- 记录开机过程。固件、引导程序、内核一层层启动时,每一层先把下一层的哈希"扩展"进 TPM 的 PCR 寄存器(
PCR0…PCR23)。PCR 只能扩展、不能改写,一次开机改过的东西会在 PCR 里留下痕迹。 - 把密钥封在"这台机器、这次正常开机"上。BitLocker 把磁盘密钥交给 TPM 封存,只有 PCR 的值和封存时一样,TPM 才肯交出来。硬盘被拆走、引导被篡改,都解不开。
- 证明身份。每块 TPM 出厂有一对背书密钥(EK)和证书,可以向远端证明"我是一块真 TPM"。Windows Hello、设备证明都用它。
Windows 11 把 TPM 2.0 列为硬性要求,安装程序开头就检查。以前可以改注册表(LabConfig)绕过去,新版 Windows 11 把这条路也堵了。所以虚拟机想跑 Windows 11,就得有一块像样的虚拟 TPM。
虚拟 TPM 由哪几块组成
给虚拟机造 TPM,要解决四件事:
- 接口:虚拟机看到的"硬件"。常用 CRB(Command Response Buffer):一页内存映射寄存器加一块命令缓冲区,外加 ACPI 的
TPM2表告诉操作系统它在哪。 - 引擎:真正执行 TPM 命令的程序。开源世界基本都用 IBM 的 libtpms,外面包一层 swtpm 作为独立进程。
- 状态:永久状态(EK、种子、NV 存储)要存盘;易失状态(PCR、会话、已加载的密钥)只活在内存里。
- 生命周期:虚拟机重启时 TPM 要复位;虚拟机挂起、快照、迁移时,易失状态要跟着走。
别家怎么做
| 平台 | 接口 | 引擎在哪 | 挂起、迁移时带上 TPM 状态 | 状态加密 |
|---|---|---|---|---|
| QEMU / libvirt / Proxmox | TIS、CRB | 外部 swtpm(数据 + 控制两个通道) | 有 | 可选 |
| Cloud Hypervisor | CRB | 外部 swtpm(数据 + 控制) | 有复位 | 交给上层 |
| VirtualBox 7 | TIS、CRB | 进程内 libtpms | 有 | 随虚拟机加密 |
| Hyper-V | 自有 | 隔离的安全域(VSM) | 有 | 强制 |
| VMware | 自有 | hypervisor 内部 | 有 | 强制 |
| bhyve(FreeBSD 14.3+) | CRB | 外部 swtpm,只接数据通道 | 没有 | 没有 |
| keelOS | CRB | 外部 swtpm(数据 + 控制) | 有,进挂起检查点 | 暂不加密 |
swtpm 有两条通道:数据通道传 TPM 命令本身;控制通道管 TPM 的"电源":上电复位(CMD_INIT)、停机、取消命令,以及把状态整块导出、导入(CMD_GET_STATEBLOB / SET_STATEBLOB)。QEMU 的热迁移就靠后者把 TPM 状态塞进迁移流。
起点:上游 bhyve 能用,但不够结实
keelOS 的虚拟机跑在 bhyve 上。FreeBSD 14.3 起,bhyve 有了 CRB 接口加 swtpm 后端,Windows 11 装得上。但读完代码,我们发现几处在边界情况下会出事:
- 虚拟机发一条格式不对的 TPM 命令,TPM 的工作线程直接退出,此后这台虚拟机的 TPM 永久失效。
- 虚拟机写一个只读寄存器、或者访问没实现的地址,bhyve 会把整台虚拟机停掉。真硬件只会忽略。
- 后端出错时,虚拟机读到一个全 0 的"响应",而不是 TPM 错误码。
- 从 swtpm 读响应只读一次,没按长度读完;swtpm 挂掉时 bhyve 直接退出。
- 只接了数据通道:虚拟机重启时 TPM 不一定复位,挂起、恢复时 TPM 状态全丢。
keelOS 的特殊难题:宿主重启,虚拟机不关
keelOS 的结构和一般虚拟化平台不一样:一个很小的 hypervisor(hyp)从 UEFI 启动后常驻内存,FreeBSD 只是它上面一个可以随时重启的"root 分区",负责驱动、存储和设备模拟。root 重启(升级内核、换驱动)的时候,虚拟机只是暂停二三十秒:内存留在 hyp 里不动,设备状态写进检查点,新的 root 起来后再恢复,TCP 连接都不断。
问题在于 swtpm 也跑在 root 里。root 一重启,swtpm 进程就没了,TPM 的易失状态(PCR、会话、已加载的密钥)跟着消失。虚拟机醒来会发现 TPM "失忆":轻则 TPM 服务报错,重则 BitLocker 在下次开机时要恢复密钥。所以 keelOS 的 vTPM 有一条别家没有的硬要求:TPM 的易失状态必须进挂起检查点,恢复时灌进一个全新的 swtpm。
keelOS 是怎么实现的
虚拟机只看到 CRB 寄存器;bhyve 同时接 swtpm 的数据通道和控制通道。
1. 把 bhyve 的 TPM 修结实
我们没有另起炉灶,而是在 bhyve 自己的 CRB 实现上修:格式不对的命令回一个标准的 TPM 错误(TPM_RC_FAILURE),工作线程继续跑;没实现的寄存器读回全 1、写入忽略,和真硬件一样,不再让虚拟机停机;响应按头部的长度读完;swtpm 出问题时把错误交给虚拟机里的驱动处理,bhyve 照常运行。我们实测过:把 swtpm 杀掉,虚拟机里读 PCR 只会得到一个 TPM 错误,虚拟机本身不受影响。
2. 接上 swtpm 的控制通道
bhyve 多了一个参数 tpm.ctrl=<socket>。每次 bhyve 启动,先经控制通道给 TPM"上电":停机、把缓冲区大小设成和 CRB 一样(3968 字节,否则 TPM 会以为自己能收 4096 字节的命令)、再发 CMD_INIT。bhyve 的每次启动都对应虚拟机的一次开机,所以虚拟机重启时 TPM 必然复位,PCR 从零开始重新度量。
3. TPM 状态进检查点
挂起时,bhyve 先等正在执行的 TPM 命令做完,再把 CRB 寄存器、数据缓冲区、PPI 页,以及经控制通道导出的三块 TPM 状态(永久、易失、保存状态)写进检查点。恢复时顺序反过来:
STOP → SET_STATEBLOB(permanent) → SET_STATEBLOB(volatile)
→ SET_STATEBLOB(savestate) → INIT
libtpms 看到有易失状态,就从那里接着跑,而不是冷启动。对虚拟机来说,TPM 从来没断过。
4. 管理程序替你打理 swtpm
在 keelOS 的管理程序里,给虚拟机的 config.json 加一行 "tpm": true(命令行是 keel vm create --tpm),剩下的都自动:
- 第一次开机时用
swtpm_setup生成 TPM 的身份:EK、EK 证书、平台证书,只开 SHA-256 这一组 PCR。 - 每次 bhyve 启动前(开机、虚拟机重启、恢复)起一个新的 swtpm;swtpm 带
terminate参数,bhyve 一退出它就跟着退出,不会留下孤儿进程。 - TPM 的永久状态放在虚拟机自己的目录
tpm/里。这个目录在虚拟机的 ZFS 数据集上,快照、复制、备份都自然带上它。 - 没开 TPM 的虚拟机完全不受影响:参数、检查点格式都和以前一样。
实测结果
测试都在真机上做(开发机,FreeBSD 14.5 root,swtpm 0.10.2):
- 通过:Linux 虚拟机看到
/dev/tpm0,固件的度量启动事件日志也在。 - 通过:往 PCR16 写入一个值后挂起,换一个全新的 swtpm 再恢复:PCR0、PCR7、PCR16 和挂起前一模一样,之后还能继续写。
- 通过:虚拟机自己重启:PCR16 归零,PCR0、PCR7 重新度量,结果和上次开机相同。
- 通过:swtpm 中途被杀:虚拟机里只得到 TPM 错误,虚拟机照常运行。
- 通过:Windows 11 25H2 安装程序自己的 TPM 2.0、Secure Boot 硬件检查,没用任何绕过手段。
- 通过:Windows 11 里 TPM 就绪,BitLocker 用 TPM 保护器(PCR 0、2、4、11)加密系统盘。
- 通过:Windows 11 重启:TPM 自动解封,没有要 BitLocker 恢复密钥。
- 通过:Windows 11 挂起再恢复(swtpm 换成新进程):暂停 3.7 秒,之后 TPM 和 BitLocker 照常。
还没做的:TPM 状态文件加密(个人和小办公室场景下,状态文件和磁盘在同一台机器上,加密能防的情形有限,先不做);Secure Boot 预置微软密钥;Windows 里"清除 TPM"这类 PPI 请求要到下次开机由固件执行,目前 bhyve 每次开机都是新进程,这类请求会丢,上游也一样。
顺带解决的几个 Windows 11 问题
- 只用了一半的 CPU。bhyve 默认一个 vCPU 算一个处理器插槽,Windows 11 专业版最多用两个插槽,4 个 vCPU 只起了 2 个,24H2 在多插槽下还特别慢。keel 新建的虚拟机现在默认把所有 vCPU 放进一个插槽。
- 重启时卡死。bhyve 模拟的 e1000 网卡在驱动屏蔽中断后不撤回中断线,Windows 关机卸载网卡驱动时变成中断风暴。keel 的 bhyve 已经修好。
- 多出一个"未知设备"。TPM 的事件日志要走 QEMU 的 fw_cfg,Windows 没有它的驱动。keel 像 QEMU 一样把这个节点标成"不在界面上显示"。
小结
vTPM 本身不新,QEMU、Hyper-V、VMware 早就有。keelOS 要解决的是自己结构带来的那一步:宿主系统可以随时重启,虚拟机不停,TPM 也不能失忆。做法是沿用业界最成熟的 swtpm 加控制通道,把 TPM 状态放进 keelOS 本来就有的挂起检查点里。对用户来说,这只是配置里的一行 "tpm": true。
keelOS 开发笔记。设计和调研细节见仓库 docs/26(vTPM 调研与实现)。