Skip to content

Commit 69313bf

Browse files
committed
feat(dstack-mr): add UEFI disk boot (UKI) measurement support
Add --uki flag to enable UEFI disk boot measurement path: OVMF → systemd-boot → UKI → vmlinuz (3 EFI app measurements) When --uki is set, RTMR calculations use the UEFI disk boot event model instead of the -kernel direct boot model: RTMR[0]: Same TD HOB/CFV/EFI vars/ACPI, but BootOrder has 2 entries and Boot0001 (disk device) is added. Boot variable digests use SHA384(variable_data_bytes). RTMR[1]: 8 events — Calling EFI App, separator, GPT event (non-empty partition entries only), systemd-boot/UKI/vmlinuz Authenticode hashes, ExitBootServices. RTMR[2]: 2 events — cmdline as UTF-16LE (Linux EFI stub behavior), initrd data hash. No "initrd=initrd" append (unlike -kernel). Verified against TDX hardware: MRTD, RTMR[1], RTMR[2] all match. RTMR[0] matches except ACPI tables (dstack-acpi-tables QEMU version difference — needs matching QEMU_ACPI_COMPAT_VER). Usage: dstack-mr measure metadata.json -c 2 -m 4G \ --uki dstack-k8s.efi \ --bootloader BOOTX64.EFI \ --disk dstack-k8s.raw
1 parent cce5ff2 commit 69313bf

6 files changed

Lines changed: 348 additions & 5 deletions

File tree

dstack-mr/cli/src/main.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ struct MachineConfig {
8181
/// Output JSON
8282
#[arg(long)]
8383
json: bool,
84+
85+
// --- UEFI disk boot (UKI mode) ---
86+
/// Path to UKI EFI binary. When set, uses UEFI disk boot measurement path
87+
/// instead of -kernel mode. Boot chain: OVMF → systemd-boot → UKI → vmlinuz.
88+
#[arg(long)]
89+
uki: Option<PathBuf>,
90+
91+
/// Path to systemd-boot EFI binary (BOOTX64.EFI). Required with --uki.
92+
#[arg(long)]
93+
bootloader: Option<PathBuf>,
94+
95+
/// Path to raw disk image (for GPT event measurement). Required with --uki.
96+
#[arg(long)]
97+
disk: Option<PathBuf>,
8498
}
8599

86100
fn main() -> Result<()> {
@@ -97,7 +111,18 @@ fn main() -> Result<()> {
97111
let firmware_path = parent_dir.join(&image_info.bios).display().to_string();
98112
let kernel_path = parent_dir.join(&image_info.kernel).display().to_string();
99113
let initrd_path = parent_dir.join(&image_info.initrd).display().to_string();
100-
let cmdline = image_info.cmdline + " initrd=initrd";
114+
// In -kernel mode, QEMU appends " initrd=initrd" to cmdline.
115+
// In UKI mode, cmdline is embedded in the UKI as-is (no append).
116+
let cmdline = if config.uki.is_some() {
117+
image_info.cmdline
118+
} else {
119+
image_info.cmdline + " initrd=initrd"
120+
};
121+
122+
// Resolve UKI-related paths
123+
let uki_path = config.uki.as_ref().map(|p| p.display().to_string());
124+
let bootloader_path = config.bootloader.as_ref().map(|p| p.display().to_string());
125+
let disk_path = config.disk.as_ref().map(|p| p.display().to_string());
101126

102127
let machine = Machine::builder()
103128
.cpu_count(config.cpu)
@@ -116,6 +141,9 @@ fn main() -> Result<()> {
116141
.hotplug_off(config.hotplug_off)
117142
.root_verity(config.root_verity)
118143
.maybe_qemu_version(config.qemu_version.clone())
144+
.maybe_uki(uki_path.as_deref())
145+
.maybe_bootloader(bootloader_path.as_deref())
146+
.maybe_disk(disk_path.as_deref())
119147
.build();
120148

121149
let measurements = machine

dstack-mr/src/kernel.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use anyhow::{bail, Context, Result};
77
use object::pe;
88
use sha2::{Digest, Sha384};
99

10-
/// Calculates the Authenticode hash of a PE/COFF file
11-
fn authenticode_sha384_hash(data: &[u8]) -> Result<Vec<u8>> {
10+
/// Calculates the Authenticode hash of a PE/COFF file.
11+
/// Used by both direct kernel boot (patched kernel) and UEFI disk boot (EFI apps).
12+
pub(crate) fn authenticode_sha384_hash(data: &[u8]) -> Result<Vec<u8>> {
1213
let lfanew_offset = 0x3c;
1314
let lfanew: u32 = read_le(data, lfanew_offset, "DOS header")?;
1415

dstack-mr/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod kernel;
1717
mod machine;
1818
mod num;
1919
mod tdvf;
20+
pub(crate) mod uefi_boot;
2021
mod util;
2122

2223
/// Contains all the measurement values for TDX.

dstack-mr/src/machine.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ pub struct Machine<'a> {
3232
pub root_verity: bool,
3333
#[builder(default)]
3434
pub host_share_mode: String,
35+
36+
// --- UEFI disk boot (UKI) mode ---
37+
// When `uki` is set, use UEFI disk boot measurement path instead of -kernel mode.
38+
// In this mode: OVMF → systemd-boot → UKI → vmlinuz (3 EFI app measurements).
39+
// The `kernel` field points to vmlinuz, `initrd` to the dracut initrd,
40+
// and `kernel_cmdline` to the embedded cmdline — all extracted from the UKI.
41+
/// Path to UKI EFI binary. When set, enables UEFI disk boot measurement mode.
42+
pub uki: Option<&'a str>,
43+
/// Path to systemd-boot EFI binary (BOOTX64.EFI). Required when `uki` is set.
44+
pub bootloader: Option<&'a str>,
45+
/// Path to raw disk image (for GPT event). Required when `uki` is set.
46+
pub disk: Option<&'a str>,
3547
}
3648

3749
fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> {
@@ -93,6 +105,11 @@ impl Machine<'_> {
93105
self.measure_with_logs().map(|details| details.measurements)
94106
}
95107

108+
/// Returns true if this machine is configured for UEFI disk boot (UKI mode).
109+
pub fn is_uki_mode(&self) -> bool {
110+
self.uki.is_some()
111+
}
112+
96113
pub fn measure_with_logs(&self) -> Result<TdxMeasurementDetails> {
97114
debug!("measuring machine: {self:#?}");
98115
let fw_data = fs::read(self.firmware)?;
@@ -102,14 +119,29 @@ impl Machine<'_> {
102119

103120
let mrtd = tdvf.mrtd(self).context("Failed to compute MR TD")?;
104121

122+
if self.is_uki_mode() {
123+
self.measure_uefi_disk_boot(&tdvf, &kernel_data, &initrd_data, mrtd)
124+
} else {
125+
self.measure_direct_kernel_boot(&tdvf, &kernel_data, &initrd_data, mrtd)
126+
}
127+
}
128+
129+
/// Direct kernel boot (-kernel mode): original dstack measurement path.
130+
fn measure_direct_kernel_boot(
131+
&self,
132+
tdvf: &Tdvf,
133+
kernel_data: &[u8],
134+
initrd_data: &[u8],
135+
mrtd: Vec<u8>,
136+
) -> Result<TdxMeasurementDetails> {
105137
let (rtmr0_log, acpi_tables) = tdvf
106138
.rtmr0_log(self)
107139
.context("Failed to compute RTMR0 log")?;
108140
debug_print_log("RTMR0", &rtmr0_log);
109141
let rtmr0 = measure_log(&rtmr0_log);
110142

111143
let rtmr1_log = kernel::rtmr1_log(
112-
&kernel_data,
144+
kernel_data,
113145
initrd_data.len() as u32,
114146
self.memory_size,
115147
0x28000,
@@ -119,7 +151,7 @@ impl Machine<'_> {
119151

120152
let rtmr2_log = vec![
121153
kernel::measure_cmdline(self.kernel_cmdline),
122-
measure_sha384(&initrd_data),
154+
measure_sha384(initrd_data),
123155
];
124156
debug_print_log("RTMR2", &rtmr2_log);
125157
let rtmr2 = measure_log(&rtmr2_log);
@@ -135,4 +167,57 @@ impl Machine<'_> {
135167
acpi_tables,
136168
})
137169
}
170+
171+
/// UEFI disk boot (UKI mode): OVMF → systemd-boot → UKI → vmlinuz.
172+
/// Verified against TDX hardware CCEL event log.
173+
fn measure_uefi_disk_boot(
174+
&self,
175+
tdvf: &Tdvf,
176+
kernel_data: &[u8],
177+
initrd_data: &[u8],
178+
mrtd: Vec<u8>,
179+
) -> Result<TdxMeasurementDetails> {
180+
let uki_path = self
181+
.uki
182+
.context("UKI path required for UEFI disk boot mode")?;
183+
let bootloader_path = self
184+
.bootloader
185+
.context("bootloader path required for UEFI disk boot mode")?;
186+
187+
let uki_data = fs::read(uki_path)?;
188+
let bootloader_data = fs::read(bootloader_path)?;
189+
190+
// RTMR[0]: same structure as direct kernel boot but with UEFI disk boot
191+
// differences in boot variables (BootOrder has 2 entries, Boot0001 added).
192+
let (rtmr0_log, acpi_tables) = tdvf
193+
.rtmr0_log_uefi_disk(self, &bootloader_data)
194+
.context("Failed to compute RTMR0 log for UEFI disk boot")?;
195+
debug_print_log("RTMR0", &rtmr0_log);
196+
let rtmr0 = measure_log(&rtmr0_log);
197+
198+
// RTMR[1]: EFI application measurements (verified against hardware).
199+
// Events: Calling EFI App, separator, GPT, systemd-boot, UKI, vmlinuz,
200+
// Exit Boot Services Invocation, Exit Boot Services Returned.
201+
let rtmr1_log =
202+
crate::uefi_boot::rtmr1_log(&bootloader_data, &uki_data, kernel_data, self.disk)?;
203+
debug_print_log("RTMR1", &rtmr1_log);
204+
let rtmr1 = measure_log(&rtmr1_log);
205+
206+
// RTMR[2]: cmdline (UTF-16LE) + initrd data hash.
207+
// Linux EFI stub converts cmdline to UTF-16LE before measuring.
208+
let rtmr2_log = crate::uefi_boot::rtmr2_log(self.kernel_cmdline, initrd_data);
209+
debug_print_log("RTMR2", &rtmr2_log);
210+
let rtmr2 = measure_log(&rtmr2_log);
211+
212+
Ok(TdxMeasurementDetails {
213+
measurements: TdxMeasurements {
214+
mrtd,
215+
rtmr0,
216+
rtmr1,
217+
rtmr2,
218+
},
219+
rtmr_logs: [rtmr0_log, rtmr1_log, rtmr2_log],
220+
acpi_tables,
221+
})
222+
}
138223
}

dstack-mr/src/tdvf.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,106 @@ impl<'a> Tdvf<'a> {
304304
))
305305
}
306306

307+
/// RTMR[0] event log for UEFI disk boot (UKI mode).
308+
///
309+
/// Differences from direct kernel boot (-kernel mode):
310+
/// - EFI variables have different values (SecureBoot not enforced, empty PK/KEK/db/dbx)
311+
/// but the digest calculation is the same (measure_tdx_efi_variable with data_len=0)
312+
/// - BootOrder contains 2 entries [0x0000, 0x0001] instead of 1
313+
/// Digest = SHA384(variable_data_bytes), not SHA384(UEFI_VARIABLE_DATA struct)
314+
/// - Boot0001 added: UEFI Misc Device (virtio-blk disk)
315+
/// Digest = SHA384(EFI_LOAD_OPTION bytes)
316+
/// - Boot0000 is the same (UiApp from OVMF)
317+
/// - Second separator at the end
318+
///
319+
/// Verified against TDX hardware CCEL event log.
320+
pub fn rtmr0_log_uefi_disk(
321+
&self,
322+
machine: &Machine,
323+
_bootloader_data: &[u8],
324+
) -> Result<(RtmrLog, Tables)> {
325+
let td_hob_hash = self.measure_td_hob(machine.memory_size)?;
326+
let cfv_image_hash = hex!(
327+
"344BC51C980BA621AAA00DA3ED7436F7D6E549197DFE699515DFA2C6583D95E6412AF21C097D473155875FFD561D6790"
328+
);
329+
// Boot0000: UiApp from OVMF (fixed, same in both boot modes).
330+
// Digest = SHA384(EFI_LOAD_OPTION variable data).
331+
let boot000_hash = hex!(
332+
"23ADA07F5261F12F34A0BD8E46760962D6B4D576A416F1FEA1C64BC656B1D28EACF7047AE6E967C58FD2A98BFA74C298"
333+
);
334+
335+
let tables = machine.build_tables()?;
336+
let acpi_tables_hash = measure_sha384(&tables.tables);
337+
let acpi_rsdp_hash = measure_sha384(&tables.rsdp);
338+
let acpi_loader_hash = measure_sha384(&tables.loader);
339+
340+
// BootOrder for UEFI disk boot: [0x0000, 0x0001] (2 boot entries).
341+
// Digest = SHA384(variable_data_bytes), NOT SHA384(UEFI_VARIABLE_DATA struct).
342+
let boot_order_data: [u8; 4] = [0x00, 0x00, 0x01, 0x00]; // LE u16: 0, 1
343+
let boot_order_hash = measure_sha384(&boot_order_data);
344+
345+
// Boot0001: "UEFI Misc Device" at PciRoot(0x0)/Pci(0x1,0x0) — first virtio-blk.
346+
// This is the EFI_LOAD_OPTION structure for the disk boot entry.
347+
// The device path encodes PciRoot(0x0)/Pci(0x1,0x0)/End.
348+
// Digest = SHA384(EFI_LOAD_OPTION variable data).
349+
//
350+
// EFI_LOAD_OPTION layout:
351+
// Attributes(4): 0x00000001 (LOAD_OPTION_ACTIVE)
352+
// FilePathListLength(2): 0x0016 (22 bytes)
353+
// Description(UTF-16LE+null): "UEFI Misc Device\0"
354+
// FilePathList: PciRoot(0x0)/Pci(0x1,0x0)/End
355+
// OptionalData: VenMedia GUID (virtio device signature)
356+
let boot0001_data: Vec<u8> = {
357+
let mut d = Vec::new();
358+
// Attributes: LOAD_OPTION_ACTIVE
359+
d.extend_from_slice(&0x00000001u32.to_le_bytes());
360+
// FilePathListLength
361+
d.extend_from_slice(&0x0016u16.to_le_bytes());
362+
// Description: "UEFI Misc Device" in UTF-16LE + null terminator
363+
for c in "UEFI Misc Device".encode_utf16() {
364+
d.extend_from_slice(&c.to_le_bytes());
365+
}
366+
d.extend_from_slice(&[0x00, 0x00]); // null terminator
367+
// FilePathList: PciRoot(0x0)/Pci(0x1,0x0)/End
368+
// ACPI device path: type=0x02, subtype=0x01, length=0x0c, HID=0x0a0341d0(PNP0A03), UID=0
369+
d.extend_from_slice(&[0x02, 0x01, 0x0c, 0x00]);
370+
d.extend_from_slice(&0x0a0341d0u32.to_le_bytes()); // PNP0A03 (PCI root)
371+
d.extend_from_slice(&0x00000000u32.to_le_bytes()); // UID=0
372+
// PCI device path: type=0x01, subtype=0x01, length=0x06, Function=0, Device=1
373+
d.extend_from_slice(&[0x01, 0x01, 0x06, 0x00, 0x00, 0x01]);
374+
// End device path: type=0x7f, subtype=0xff, length=0x04
375+
d.extend_from_slice(&[0x7f, 0xff, 0x04, 0x00]);
376+
// OptionalData: VenMedia GUID for virtio device signature
377+
// 4e ac 08 81 11 9f 59 4d 85 0e e2 1a 52 2c 59 b2
378+
d.extend_from_slice(&[
379+
0x4e, 0xac, 0x08, 0x81, 0x11, 0x9f, 0x59, 0x4d, 0x85, 0x0e, 0xe2, 0x1a, 0x52, 0x2c,
380+
0x59, 0xb2,
381+
]);
382+
d
383+
};
384+
let boot0001_hash = measure_sha384(&boot0001_data);
385+
386+
Ok((
387+
vec![
388+
td_hob_hash,
389+
cfv_image_hash.to_vec(),
390+
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "SecureBoot")?,
391+
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "PK")?,
392+
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "KEK")?,
393+
measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "db")?,
394+
measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "dbx")?,
395+
measure_sha384(&[0x00, 0x00, 0x00, 0x00]), // Separator
396+
acpi_loader_hash,
397+
acpi_rsdp_hash,
398+
acpi_tables_hash,
399+
boot_order_hash,
400+
boot000_hash.to_vec(),
401+
boot0001_hash,
402+
],
403+
tables,
404+
))
405+
}
406+
307407
fn measure_td_hob(&self, memory_size: u64) -> Result<Vec<u8>> {
308408
let mut memory_acceptor = MemoryAcceptor::new(0, memory_size);
309409
let mut td_hob = Vec::new();

0 commit comments

Comments
 (0)