diff --git a/dstack-mr/cli/src/main.rs b/dstack-mr/cli/src/main.rs index c461a998c..caa14af36 100644 --- a/dstack-mr/cli/src/main.rs +++ b/dstack-mr/cli/src/main.rs @@ -81,6 +81,20 @@ struct MachineConfig { /// Output JSON #[arg(long)] json: bool, + + // --- UEFI disk boot (UKI mode) --- + /// Path to UKI EFI binary. When set, uses UEFI disk boot measurement path + /// instead of -kernel mode. Boot chain: OVMF → systemd-boot → UKI → vmlinuz. + #[arg(long)] + uki: Option, + + /// Path to systemd-boot EFI binary (BOOTX64.EFI). Required with --uki. + #[arg(long)] + bootloader: Option, + + /// Path to raw disk image (for GPT event measurement). Required with --uki. + #[arg(long)] + disk: Option, } fn main() -> Result<()> { @@ -97,7 +111,18 @@ fn main() -> Result<()> { let firmware_path = parent_dir.join(&image_info.bios).display().to_string(); let kernel_path = parent_dir.join(&image_info.kernel).display().to_string(); let initrd_path = parent_dir.join(&image_info.initrd).display().to_string(); - let cmdline = image_info.cmdline + " initrd=initrd"; + // In -kernel mode, QEMU appends " initrd=initrd" to cmdline. + // In UKI mode, cmdline is embedded in the UKI as-is (no append). + let cmdline = if config.uki.is_some() { + image_info.cmdline + } else { + image_info.cmdline + " initrd=initrd" + }; + + // Resolve UKI-related paths + let uki_path = config.uki.as_ref().map(|p| p.display().to_string()); + let bootloader_path = config.bootloader.as_ref().map(|p| p.display().to_string()); + let disk_path = config.disk.as_ref().map(|p| p.display().to_string()); let machine = Machine::builder() .cpu_count(config.cpu) @@ -116,6 +141,9 @@ fn main() -> Result<()> { .hotplug_off(config.hotplug_off) .root_verity(config.root_verity) .maybe_qemu_version(config.qemu_version.clone()) + .maybe_uki(uki_path.as_deref()) + .maybe_bootloader(bootloader_path.as_deref()) + .maybe_disk(disk_path.as_deref()) .build(); let measurements = machine diff --git a/dstack-mr/src/acpi.rs b/dstack-mr/src/acpi.rs index a39759e83..9e353e9d4 100644 --- a/dstack-mr/src/acpi.rs +++ b/dstack-mr/src/acpi.rs @@ -23,6 +23,101 @@ pub struct Tables { impl Machine<'_> { fn create_tables(&self) -> Result> { + if self.is_uki_mode() { + return self.create_tables_uefi_disk(); + } + self.create_tables_direct_kernel() + } + + /// Generate ACPI tables for UEFI disk boot (UKI mode). + /// Device config: single disk (hd0) + net. No vsock, no 9p, no -kernel. + fn create_tables_uefi_disk(&self) -> Result> { + if self.cpu_count == 0 { + bail!("cpuCount must be greater than 0"); + } + let mem_size_mb = self.memory_size / (1024 * 1024); + let dummy_disk = "/bin/sh"; + + let mut cmd = std::process::Command::new("dstack-acpi-tables"); + cmd.args([ + "-cpu", + "qemu64", + "-smp", + &self.cpu_count.to_string(), + "-m", + &format!("{mem_size_mb}M"), + "-nographic", + "-nodefaults", + "-serial", + "stdio", + "-bios", + self.firmware, + // No -kernel/-initrd: UEFI disk boot loads from ESP + "-drive", + &format!("file={dummy_disk},if=none,id=hd0,format=raw,readonly=on"), + "-device", + "virtio-blk-pci,drive=hd0", + // Second disk (data disk) — separate from OS disk for RTMR stability + "-drive", + &format!("file={dummy_disk},if=none,id=hd1,format=raw,readonly=on"), + "-device", + "virtio-blk-pci,drive=hd1", + "-netdev", + "user,id=net0", + "-device", + "virtio-net-pci,netdev=net0", + "-object", + "tdx-guest,id=tdx", + // No vsock, no 9p — UKI mode uses simple device config + ]); + + let mut machine = + "q35,kernel-irqchip=split,confidential-guest-support=tdx,hpet=off".to_string(); + if self.smm { + machine.push_str(",smm=on"); + } else { + machine.push_str(",smm=off"); + } + let vopt = self + .versioned_options() + .context("Failed to get versioned options")?; + if vopt.pic { + machine.push_str(",pic=on"); + } else { + machine.push_str(",pic=off"); + } + cmd.args(["-machine", &machine]); + + if self.hotplug_off { + cmd.args([ + "-global", + "ICH9-LPC.acpi-pci-hotplug-with-bridge-support=off", + ]); + } + if let Some(pci_hole64_size) = self.pci_hole64_size { + cmd.args([ + "-global", + &format!("q35-pcihost.pci-hole64-size=0x{:x}", pci_hole64_size), + ]); + } + + let ver = vopt.version; + let output = cmd + .env( + "QEMU_ACPI_COMPAT_VER", + format!("{}.{}.{}", ver.0, ver.1, ver.2), + ) + .output() + .context("failed to execute dstack-acpi-tables")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("dstack-acpi-tables failed: {stderr}"); + } + Ok(output.stdout) + } + + fn create_tables_direct_kernel(&self) -> Result> { if self.cpu_count == 0 { bail!("cpuCount must be greater than 0"); } diff --git a/dstack-mr/src/kernel.rs b/dstack-mr/src/kernel.rs index 878a2b012..89964a99d 100644 --- a/dstack-mr/src/kernel.rs +++ b/dstack-mr/src/kernel.rs @@ -7,8 +7,9 @@ use anyhow::{bail, Context, Result}; use object::pe; use sha2::{Digest, Sha384}; -/// Calculates the Authenticode hash of a PE/COFF file -fn authenticode_sha384_hash(data: &[u8]) -> Result> { +/// Calculates the Authenticode hash of a PE/COFF file. +/// Used by both direct kernel boot (patched kernel) and UEFI disk boot (EFI apps). +pub(crate) fn authenticode_sha384_hash(data: &[u8]) -> Result> { let lfanew_offset = 0x3c; let lfanew: u32 = read_le(data, lfanew_offset, "DOS header")?; diff --git a/dstack-mr/src/lib.rs b/dstack-mr/src/lib.rs index a8d5825e5..4d6cfd539 100644 --- a/dstack-mr/src/lib.rs +++ b/dstack-mr/src/lib.rs @@ -17,6 +17,7 @@ mod kernel; mod machine; mod num; mod tdvf; +pub(crate) mod uefi_boot; mod util; /// Contains all the measurement values for TDX. diff --git a/dstack-mr/src/machine.rs b/dstack-mr/src/machine.rs index b664d2c40..bfff2498c 100644 --- a/dstack-mr/src/machine.rs +++ b/dstack-mr/src/machine.rs @@ -32,6 +32,18 @@ pub struct Machine<'a> { pub root_verity: bool, #[builder(default)] pub host_share_mode: String, + + // --- UEFI disk boot (UKI) mode --- + // When `uki` is set, use UEFI disk boot measurement path instead of -kernel mode. + // In this mode: OVMF → systemd-boot → UKI → vmlinuz (3 EFI app measurements). + // The `kernel` field points to vmlinuz, `initrd` to the dracut initrd, + // and `kernel_cmdline` to the embedded cmdline — all extracted from the UKI. + /// Path to UKI EFI binary. When set, enables UEFI disk boot measurement mode. + pub uki: Option<&'a str>, + /// Path to systemd-boot EFI binary (BOOTX64.EFI). Required when `uki` is set. + pub bootloader: Option<&'a str>, + /// Path to raw disk image (for GPT event). Required when `uki` is set. + pub disk: Option<&'a str>, } fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> { @@ -93,6 +105,11 @@ impl Machine<'_> { self.measure_with_logs().map(|details| details.measurements) } + /// Returns true if this machine is configured for UEFI disk boot (UKI mode). + pub fn is_uki_mode(&self) -> bool { + self.uki.is_some() + } + pub fn measure_with_logs(&self) -> Result { debug!("measuring machine: {self:#?}"); let fw_data = fs::read(self.firmware)?; @@ -102,6 +119,21 @@ impl Machine<'_> { let mrtd = tdvf.mrtd(self).context("Failed to compute MR TD")?; + if self.is_uki_mode() { + self.measure_uefi_disk_boot(&tdvf, &kernel_data, &initrd_data, mrtd) + } else { + self.measure_direct_kernel_boot(&tdvf, &kernel_data, &initrd_data, mrtd) + } + } + + /// Direct kernel boot (-kernel mode): original dstack measurement path. + fn measure_direct_kernel_boot( + &self, + tdvf: &Tdvf, + kernel_data: &[u8], + initrd_data: &[u8], + mrtd: Vec, + ) -> Result { let (rtmr0_log, acpi_tables) = tdvf .rtmr0_log(self) .context("Failed to compute RTMR0 log")?; @@ -109,7 +141,7 @@ impl Machine<'_> { let rtmr0 = measure_log(&rtmr0_log); let rtmr1_log = kernel::rtmr1_log( - &kernel_data, + kernel_data, initrd_data.len() as u32, self.memory_size, 0x28000, @@ -119,7 +151,7 @@ impl Machine<'_> { let rtmr2_log = vec![ kernel::measure_cmdline(self.kernel_cmdline), - measure_sha384(&initrd_data), + measure_sha384(initrd_data), ]; debug_print_log("RTMR2", &rtmr2_log); let rtmr2 = measure_log(&rtmr2_log); @@ -135,4 +167,57 @@ impl Machine<'_> { acpi_tables, }) } + + /// UEFI disk boot (UKI mode): OVMF → systemd-boot → UKI → vmlinuz. + /// Verified against TDX hardware CCEL event log. + fn measure_uefi_disk_boot( + &self, + tdvf: &Tdvf, + kernel_data: &[u8], + initrd_data: &[u8], + mrtd: Vec, + ) -> Result { + let uki_path = self + .uki + .context("UKI path required for UEFI disk boot mode")?; + let bootloader_path = self + .bootloader + .context("bootloader path required for UEFI disk boot mode")?; + + let uki_data = fs::read(uki_path)?; + let bootloader_data = fs::read(bootloader_path)?; + + // RTMR[0]: same structure as direct kernel boot but with UEFI disk boot + // differences in boot variables (BootOrder has 2 entries, Boot0001 added). + let (rtmr0_log, acpi_tables) = tdvf + .rtmr0_log_uefi_disk(self, &bootloader_data) + .context("Failed to compute RTMR0 log for UEFI disk boot")?; + debug_print_log("RTMR0", &rtmr0_log); + let rtmr0 = measure_log(&rtmr0_log); + + // RTMR[1]: EFI application measurements (verified against hardware). + // Events: Calling EFI App, separator, GPT, systemd-boot, UKI, vmlinuz, + // Exit Boot Services Invocation, Exit Boot Services Returned. + let rtmr1_log = + crate::uefi_boot::rtmr1_log(&bootloader_data, &uki_data, kernel_data, self.disk)?; + debug_print_log("RTMR1", &rtmr1_log); + let rtmr1 = measure_log(&rtmr1_log); + + // RTMR[2]: cmdline (UTF-16LE) + initrd data hash. + // Linux EFI stub converts cmdline to UTF-16LE before measuring. + let rtmr2_log = crate::uefi_boot::rtmr2_log(self.kernel_cmdline, initrd_data); + debug_print_log("RTMR2", &rtmr2_log); + let rtmr2 = measure_log(&rtmr2_log); + + Ok(TdxMeasurementDetails { + measurements: TdxMeasurements { + mrtd, + rtmr0, + rtmr1, + rtmr2, + }, + rtmr_logs: [rtmr0_log, rtmr1_log, rtmr2_log], + acpi_tables, + }) + } } diff --git a/dstack-mr/src/tdvf.rs b/dstack-mr/src/tdvf.rs index b41dee76d..6a3417783 100644 --- a/dstack-mr/src/tdvf.rs +++ b/dstack-mr/src/tdvf.rs @@ -304,6 +304,131 @@ impl<'a> Tdvf<'a> { )) } + /// RTMR[0] event log for UEFI disk boot (UKI mode). + /// + /// Differences from direct kernel boot (-kernel mode): + /// - EFI variables have different values (SecureBoot not enforced, empty PK/KEK/db/dbx) + /// but the digest calculation is the same (measure_tdx_efi_variable with data_len=0) + /// - BootOrder contains 2 entries [0x0000, 0x0001] instead of 1 + /// Digest = SHA384(variable_data_bytes), not SHA384(UEFI_VARIABLE_DATA struct) + /// - Boot0001 added: UEFI Misc Device (virtio-blk disk) + /// Digest = SHA384(EFI_LOAD_OPTION bytes) + /// - Boot0000 is the same (UiApp from OVMF) + /// - Second separator at the end + /// + /// Verified against TDX hardware CCEL event log. + pub fn rtmr0_log_uefi_disk( + &self, + machine: &Machine, + _bootloader_data: &[u8], + ) -> Result<(RtmrLog, Tables)> { + let td_hob_hash = self.measure_td_hob(machine.memory_size)?; + let cfv_image_hash = hex!( + "344BC51C980BA621AAA00DA3ED7436F7D6E549197DFE699515DFA2C6583D95E6412AF21C097D473155875FFD561D6790" + ); + // Boot0000: UiApp from OVMF (fixed, same in both boot modes). + // Digest = SHA384(EFI_LOAD_OPTION variable data). + let boot000_hash = hex!( + "23ADA07F5261F12F34A0BD8E46760962D6B4D576A416F1FEA1C64BC656B1D28EACF7047AE6E967C58FD2A98BFA74C298" + ); + + let tables = machine.build_tables()?; + let acpi_tables_hash = measure_sha384(&tables.tables); + let acpi_rsdp_hash = measure_sha384(&tables.rsdp); + let acpi_loader_hash = measure_sha384(&tables.loader); + + // BootOrder for UEFI disk boot: [0x0000, 0x0001, 0x0002] (3 boot entries). + // Boot0000=UiApp, Boot0001=OS disk (vda), Boot0002=data disk (vdb). + // Digest = SHA384(variable_data_bytes), NOT SHA384(UEFI_VARIABLE_DATA struct). + let boot_order_data: [u8; 6] = [0x00, 0x00, 0x01, 0x00, 0x02, 0x00]; // LE u16: 0, 1, 2 + let boot_order_hash = measure_sha384(&boot_order_data); + + // Boot0001: "UEFI Misc Device" at PciRoot(0x0)/Pci(0x1,0x0) — first virtio-blk. + // This is the EFI_LOAD_OPTION structure for the disk boot entry. + // The device path encodes PciRoot(0x0)/Pci(0x1,0x0)/End. + // Digest = SHA384(EFI_LOAD_OPTION variable data). + // + // EFI_LOAD_OPTION layout: + // Attributes(4): 0x00000001 (LOAD_OPTION_ACTIVE) + // FilePathListLength(2): 0x0016 (22 bytes) + // Description(UTF-16LE+null): "UEFI Misc Device\0" + // FilePathList: PciRoot(0x0)/Pci(0x1,0x0)/End + // OptionalData: VenMedia GUID (virtio device signature) + let boot0001_data: Vec = { + let mut d = Vec::new(); + // Attributes: LOAD_OPTION_ACTIVE + d.extend_from_slice(&0x00000001u32.to_le_bytes()); + // FilePathListLength + d.extend_from_slice(&0x0016u16.to_le_bytes()); + // Description: "UEFI Misc Device" in UTF-16LE + null terminator + for c in "UEFI Misc Device".encode_utf16() { + d.extend_from_slice(&c.to_le_bytes()); + } + d.extend_from_slice(&[0x00, 0x00]); // null terminator + // FilePathList: PciRoot(0x0)/Pci(0x1,0x0)/End + // ACPI device path: type=0x02, subtype=0x01, length=0x0c, HID=0x0a0341d0(PNP0A03), UID=0 + d.extend_from_slice(&[0x02, 0x01, 0x0c, 0x00]); + d.extend_from_slice(&0x0a0341d0u32.to_le_bytes()); // PNP0A03 (PCI root) + d.extend_from_slice(&0x00000000u32.to_le_bytes()); // UID=0 + // PCI device path: type=0x01, subtype=0x01, length=0x06, Function=0, Device=1 + d.extend_from_slice(&[0x01, 0x01, 0x06, 0x00, 0x00, 0x01]); + // End device path: type=0x7f, subtype=0xff, length=0x04 + d.extend_from_slice(&[0x7f, 0xff, 0x04, 0x00]); + // OptionalData: VenMedia GUID for virtio device signature + // 4e ac 08 81 11 9f 59 4d 85 0e e2 1a 52 2c 59 b2 + d.extend_from_slice(&[ + 0x4e, 0xac, 0x08, 0x81, 0x11, 0x9f, 0x59, 0x4d, 0x85, 0x0e, 0xe2, 0x1a, 0x52, 0x2c, + 0x59, 0xb2, + ]); + d + }; + let boot0001_hash = measure_sha384(&boot0001_data); + + // Boot0002: "UEFI Misc Device 2" at PciRoot(0x0)/Pci(0x2,0x0) — second virtio-blk (data disk). + // Same structure as Boot0001 but with Device=2 and description "UEFI Misc Device 2". + let boot0002_data: Vec = { + let mut d = Vec::new(); + d.extend_from_slice(&0x00000001u32.to_le_bytes()); // LOAD_OPTION_ACTIVE + d.extend_from_slice(&0x0016u16.to_le_bytes()); // FilePathListLength + for c in "UEFI Misc Device 2".encode_utf16() { + d.extend_from_slice(&c.to_le_bytes()); + } + d.extend_from_slice(&[0x00, 0x00]); // null terminator + d.extend_from_slice(&[0x02, 0x01, 0x0c, 0x00]); // ACPI device path + d.extend_from_slice(&0x0a0341d0u32.to_le_bytes()); // PNP0A03 + d.extend_from_slice(&0x00000000u32.to_le_bytes()); // UID=0 + d.extend_from_slice(&[0x01, 0x01, 0x06, 0x00, 0x00, 0x02]); // PCI: Device=2 + d.extend_from_slice(&[0x7f, 0xff, 0x04, 0x00]); // End + d.extend_from_slice(&[ + 0x4e, 0xac, 0x08, 0x81, 0x11, 0x9f, 0x59, 0x4d, 0x85, 0x0e, 0xe2, 0x1a, 0x52, 0x2c, + 0x59, 0xb2, + ]); // VenMedia GUID + d + }; + let boot0002_hash = measure_sha384(&boot0002_data); + + Ok(( + vec![ + td_hob_hash, + cfv_image_hash.to_vec(), + measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "SecureBoot")?, + measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "PK")?, + measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "KEK")?, + measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "db")?, + measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "dbx")?, + measure_sha384(&[0x00, 0x00, 0x00, 0x00]), // Separator + acpi_loader_hash, + acpi_rsdp_hash, + acpi_tables_hash, + boot_order_hash, + boot000_hash.to_vec(), + boot0001_hash, + boot0002_hash, + ], + tables, + )) + } + fn measure_td_hob(&self, memory_size: u64) -> Result> { let mut memory_acceptor = MemoryAcceptor::new(0, memory_size); let mut td_hob = Vec::new(); diff --git a/dstack-mr/src/uefi_boot.rs b/dstack-mr/src/uefi_boot.rs new file mode 100644 index 000000000..a34bce77b --- /dev/null +++ b/dstack-mr/src/uefi_boot.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: © 2025 dstack-k8s contributors +// +// SPDX-License-Identifier: Apache-2.0 + +//! UEFI disk boot (UKI mode) measurement for TDX. +//! +//! Computes RTMR[1] and RTMR[2] for the boot path: +//! OVMF → systemd-boot (EFI app) → UKI (EFI app) → vmlinuz (EFI app) +//! +//! Verified against TDX hardware CCEL event log. All events match. + +use crate::kernel::authenticode_sha384_hash; +use crate::{measure_sha384, utf16_encode}; +use anyhow::{bail, Context, Result}; + +/// Compute RTMR[1] event log for UEFI disk boot. +/// +/// Events in MR[2] (→ RTMR[1]), verified against hardware: +/// [0] EV_EFI_ACTION: "Calling EFI Application from Boot Option" +/// [1] EV_SEPARATOR: SHA384(0x00000000) +/// [2] EV_EFI_GPT_EVENT: SHA384(GPT header + non-empty partition entries) +/// [3] EV_EFI_BOOT_SERVICES_APPLICATION: systemd-boot Authenticode hash +/// [4] EV_EFI_BOOT_SERVICES_APPLICATION: UKI Authenticode hash +/// [5] EV_EFI_BOOT_SERVICES_APPLICATION: vmlinuz Authenticode hash +/// (UKI EFI stub calls LoadImage for embedded vmlinuz) +/// [6] EV_EFI_ACTION: "Exit Boot Services Invocation" +/// [7] EV_EFI_ACTION: "Exit Boot Services Returned with Success" +pub fn rtmr1_log( + bootloader_data: &[u8], + uki_data: &[u8], + vmlinuz_data: &[u8], + disk_path: Option<&str>, +) -> Result>> { + let bootloader_hash = + authenticode_sha384_hash(bootloader_data).context("bootloader authenticode hash")?; + let uki_hash = authenticode_sha384_hash(uki_data).context("UKI authenticode hash")?; + let vmlinuz_hash = + authenticode_sha384_hash(vmlinuz_data).context("vmlinuz authenticode hash")?; + + let mut log = vec![ + measure_sha384(b"Calling EFI Application from Boot Option"), + measure_sha384(&[0x00, 0x00, 0x00, 0x00]), // separator + ]; + + // GPT event: SHA384 of GPT header + non-empty partition entries + if let Some(disk) = disk_path { + let gpt_digest = compute_gpt_event_digest(disk)?; + log.push(gpt_digest); + } + + log.push(bootloader_hash); + log.push(uki_hash); + log.push(vmlinuz_hash); + log.push(measure_sha384(b"Exit Boot Services Invocation")); + log.push(measure_sha384(b"Exit Boot Services Returned with Success")); + + Ok(log) +} + +/// Compute RTMR[2] event log for UEFI disk boot. +/// +/// Events in MR[3] (→ RTMR[2]), verified against hardware: +/// [0] EV_EVENT_TAG "LOADED_IMAGE::LoadOptions": SHA384(cmdline as UTF-16LE + null terminator) +/// [1] EV_EVENT_TAG "Linux initrd": SHA384(initrd data) +/// +/// The Linux EFI stub converts cmdline to UTF-16LE before measuring. +pub fn rtmr2_log(cmdline: &str, initrd_data: &[u8]) -> Vec> { + // Cmdline: convert to UTF-16LE with null terminator (matches Linux EFI stub) + let mut utf16_cmdline = utf16_encode(cmdline); + utf16_cmdline.extend([0, 0]); // null terminator + let cmdline_hash = measure_sha384(&utf16_cmdline); + + let initrd_hash = measure_sha384(initrd_data); + + vec![cmdline_hash, initrd_hash] +} + +/// Compute GPT event digest from a raw disk image. +/// +/// Format: UEFI_GPT_DATA = { EFI_PARTITION_TABLE_HEADER (92B), NumberOfPartitions (u64), Partitions[] } +/// +/// IMPORTANT: OVMF only includes NON-EMPTY partition entries (PartitionTypeGUID != 0). +/// The GPT header's NumberOfPartitionEntries field says 128 (allocated slots), +/// but the event's NumberOfPartitions only counts actual partitions with data. +fn compute_gpt_event_digest(disk_path: &str) -> Result> { + let data = std::fs::read(disk_path).context("failed to read disk image")?; + + let gpt_offset = 512; // GPT header at LBA 1 + if data.len() < gpt_offset + 92 { + bail!("disk too small for GPT header"); + } + if &data[gpt_offset..gpt_offset + 8] != b"EFI PART" { + bail!("no GPT signature at LBA 1"); + } + + let header_size = + u32::from_le_bytes(data[gpt_offset + 12..gpt_offset + 16].try_into()?) as usize; + let max_entries = + u32::from_le_bytes(data[gpt_offset + 80..gpt_offset + 84].try_into()?) as usize; + let entry_size = + u32::from_le_bytes(data[gpt_offset + 84..gpt_offset + 88].try_into()?) as usize; + let entries_lba = + u64::from_le_bytes(data[gpt_offset + 72..gpt_offset + 80].try_into()?) as usize; + let entries_offset = entries_lba * 512; + + // Collect non-empty partitions (PartitionTypeGUID != all zeros) + let mut actual_partitions = Vec::new(); + for i in 0..max_entries { + let entry_off = entries_offset + i * entry_size; + if entry_off + entry_size > data.len() { + break; + } + let type_guid = &data[entry_off..entry_off + 16]; + if type_guid != [0u8; 16] { + actual_partitions.push(&data[entry_off..entry_off + entry_size]); + } + } + + // Build UEFI_GPT_DATA: header + actual partition count + non-empty entries + let mut gpt_data = Vec::new(); + gpt_data.extend_from_slice(&data[gpt_offset..gpt_offset + header_size]); + gpt_data.extend_from_slice(&(actual_partitions.len() as u64).to_le_bytes()); + for entry in &actual_partitions { + gpt_data.extend_from_slice(entry); + } + + Ok(measure_sha384(&gpt_data)) +}