Introduction
Part one of this series showed how to verify code running in a cloud-hosted TDX TEE without trusting the cloud provider or deployer. This requires independently reconstructing four values (MRTD, RTMR0, RTMR1, and RTMR2) separately from the TEE and verifying that they match. RTMR1 and RTMR2 are derived from VM image components, such as the filesystem and kernel. MRTD and RTMR0 are more complicated because they depend on Google’s proprietary UEFI firmware. RTMR0 also depends on the machine’s initial environment, which changes with external factors like the amount of RAM allocated to the VM.
Most events in RTMR0 can be reproduced without analyzing the firmware: the five labeled “Hardware Description” are derived from metadata sent with the quote, and another six come from Secure Boot (which is redundant in TDX). Google regularly updates the default Secure Boot values, but they can be overridden with fixed ones during deployment. That leaves only two RTMR0 events (and MRTD) that change when Google updates their firmware. These must be derived from the compiled UEFI firmware file.
Parsing UEFI Firmware
Fortunately, extracting useful data from a UEFI firmware file doesn’t require disassembling machine code. The files are organized as a tree of labeled regions rather than a single block of machine code. Reproducing these events doesn’t even require parsing the entire tree, since TDX-compatible firmware includes a metadata table listing every region used to build the VM’s initial memory.
To see how the table is used, it helps to know what “initial memory” means in TDX. A confidential VM’s memory is sealed off from the rest of the computer, so neither the hypervisor nor the cloud provider’s software can read or modify it. But something still has to set up the initial layout that the VM needs to start; the CPU performs this setup itself. It uses the TDX metadata table to allocate a portion of isolated memory to the confidential VM, recording each step it takes into MRTD.
The interactive diagram below shows the TDX metadata table from a recorded GCP TDX firmware file. Each entry contains a type field and a memory address range. Some entries also specify a range of bytes from the firmware file to copy into memory before starting the VM. Click any entry in the table at the top of the diagram to see where its data comes from and where it goes in the VM’s initial memory.
Three values change when Google updates their firmware: MRTD and the first two events of RTMR0. Two of these can be reproduced directly from the data in the table above.
As the CPU initializes the VM’s memory, it records each step into MRTD. This includes the memory ranges from every table entry and a hash of the BFV region’s file bytes. Since every TDX processor follows the same specification for this process, anyone can calculate the expected MRTD value using the data in the TDX metadata table.
The data hashed into MRTD does not include the CFV region’s contents, which is likely why Google modified their firmware to record it separately into RTMR0. Reproducing it just requires hashing the firmware region listed in the CFV row of the table.
Reconstructing the Hand-Off Block
That leaves the last event: the Hand-Off Block (HOB). Like MRTD, it is built from the regions in the metadata table. But while MRTD records only the initialized regions, the HOB also accounts for the memory between and above them, marking which parts of the VM’s memory start initialized and which are left for the OS to initialize later. And unlike MRTD, the HOB is not computed by the CPU. Instead, the hypervisor writes the HOB into the VM’s initial memory, then the firmware records a hash of its bytes into RTMR0.
Calculating the HOB is more involved than MRTD for two reasons. First, because it spans the VM’s entire memory range, including uninitialized parts, its contents vary with the VM’s RAM size. This is why Attest includes the RAM size with each quote. Second, the HOB is built by the hypervisor rather than copied from the firmware file. It just lists the regions from the metadata table, but the specification is loose enough that the exact bytes differ between hypervisors.
But Google’s hypervisor is proprietary, so there is no way to determine how it constructs the HOB. Patching Google’s firmware to print the HOB during boot isn’t an option either, since Google doesn’t allow running custom firmware. And the HOB is only used by the firmware, so its memory region is unallocated by the time the operating system starts. This means there is no direct way to read it from a running VM.
The data, however, remains in memory after it is unallocated. The TDX metadata table lists the HOB’s exact memory range, so I knew which address to read. I temporarily patched the Linux kernel to print the contents of that range to the kernel log as soon as the kernel boots. Running this across several VM sizes and comparing the results showed how Google’s hypervisor builds the...