Skip to content

Commit 5aac58f

Browse files
committed
dstack-mr: derive BootOrder / Boot0000 / Boot0001 digests from first principles
The Stable202505 RTMR[0] code used to carry six hardcoded sha384 digests captured from a single 0.5.10 CVM. For the three EV_EFI_VARIABLE_BOOT2 events that's needlessly opaque — the TCG digest is taken over the on-the-wire variable data, which is fully derivable from a handful of OVMF-internal constants (FV/file GUIDs, descriptions, attributes). New uefi_var module builds: * BootOrder via boot_order_bytes(&[u16]) * Boot#### via boot_option_bytes(attrs, desc, &[device_path_node]) Stable202505 arm now hashes synthesised bytes for those three events; GUIDs/descriptions/attributes for OVMF's BootManagerMenuApp and "EFI Firmware Setup" entries are at the top of tdvf.rs with a comment naming the upstream source. The remaining two digests (bootorder fw_cfg + EV_EFI_VARIABLE_AUTHORITY) stay captured because their payloads depend on QEMU's device topology and OVMF-internal logic that isn't worth shadowing here. Comments now spell out what each measures, the dstack/QEMU config they're constants for, and the failure mode if the config changes. Verified: synth digests match the captured values; RTMR[0] still equals the running 0.5.10 CVM's quote exactly.
1 parent 3ec3f53 commit 5aac58f

3 files changed

Lines changed: 221 additions & 14 deletions

File tree

dstack-mr/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod kernel;
1919
mod machine;
2020
mod num;
2121
mod tdvf;
22+
mod uefi_var;
2223
mod util;
2324

2425
/// Pick the OVMF variant for a given dstack OS version string ("MAJOR.MINOR.PATCH").

dstack-mr/src/tdvf.rs

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,35 @@ use sha2::{Digest, Sha384};
99

1010
use crate::acpi::Tables;
1111
use crate::num::read_le;
12+
use crate::uefi_var::{
13+
boot_option_bytes, boot_order_bytes, fv_file_node, fv_node, END_OF_DEVICE_PATH,
14+
};
1215
use crate::{measure_log, measure_sha384, utf16_encode, Machine, OvmfVariant, RtmrLog};
1316

1417
const PAGE_SIZE: u64 = 0x1000;
1518
const MR_EXTEND_GRANULARITY: usize = 0x100;
1619

20+
// OVMF firmware-volume identifiers used by edk2-stable202505. These are baked
21+
// into the OVMF binary at build time; if the firmware is regenerated against a
22+
// different EDK2 source these constants may need refreshing.
23+
//
24+
// Each GUID is stored in the on-the-wire little-endian byte form OVMF puts in
25+
// the EFI_DEVICE_PATH MEDIA_FV / MEDIA_FV_FILE nodes — the first three GUID
26+
// fields are byte-swapped relative to the canonical string form.
27+
//
28+
// canonical: 7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1
29+
const OVMF_FV_GUID_LE: [u8; 16] = [
30+
0xc9, 0xbd, 0xb8, 0x7c, 0xeb, 0xf8, 0x34, 0x4f, 0xaa, 0xea, 0x3e, 0xe4, 0xaf, 0x65, 0x16, 0xa1,
31+
];
32+
// canonical: eec25bdc-67f2-4d95-b1d5-f81b2039d11d (MdeModulePkg UiApp)
33+
const OVMF_UIAPP_FILE_GUID_LE: [u8; 16] = [
34+
0xdc, 0x5b, 0xc2, 0xee, 0xf2, 0x67, 0x95, 0x4d, 0xb1, 0xd5, 0xf8, 0x1b, 0x20, 0x39, 0xd1, 0x1d,
35+
];
36+
// canonical: 462caa21-7614-4503-836e-8ab6f4662331 (MdeModulePkg BootMaintenance / FrontPage)
37+
const OVMF_FRONTPAGE_FILE_GUID_LE: [u8; 16] = [
38+
0x21, 0xaa, 0x2c, 0x46, 0x14, 0x76, 0x03, 0x45, 0x83, 0x6e, 0x8a, 0xb6, 0xf4, 0x66, 0x23, 0x31,
39+
];
40+
1741
const ATTRIBUTE_MR_EXTEND: u32 = 0x00000001;
1842
const ATTRIBUTE_PAGE_AUG: u32 = 0x00000002;
1943

@@ -310,21 +334,68 @@ impl<'a> Tdvf<'a> {
310334
]
311335
}
312336
OvmfVariant::Stable202505 => {
313-
// edk2-stable202505 adds 4 new events and re-formats BootOrder/Boot0000
314-
// in RTMR[0]. Digests captured from a 0.5.10 dstack CVM running the
315-
// standard -kernel boot config (9p host_share_mode); they are firmware
316-
// constants for this OVMF build.
317-
let bootmenu_fwcfg_hash = measure_sha384(&[0x00, 0x00]); // BootMenu fw_cfg = 0x0000
337+
// edk2-stable202505 emits 17 RTMR[0] events instead of 13. The
338+
// boot-option set is fully derivable from OVMF-internal
339+
// constants (FV and file GUIDs, descriptions, attributes); the
340+
// remaining two — the bootorder fw_cfg measurement and
341+
// EV_EFI_VARIABLE_AUTHORITY — stay as captured digests because
342+
// their content depends on QEMU's emitted device list and on
343+
// OVMF-internal logic that's not worth shadowing here.
344+
345+
// fw_cfg `BootMenu` is a u16; dstack doesn't pass `-boot
346+
// menu=on`, so it defaults to 0x0000.
347+
let bootmenu_fwcfg_hash = measure_sha384(&[0x00, 0x00]);
348+
349+
// fw_cfg `bootorder` is QEMU's generated bootorder string for
350+
// the current device topology. dstack-vmm's QEMU command (9p +
351+
// virtio-blk + vhost-vsock, no `-boot order=` / `bootindex=`)
352+
// is fixed, so this is a constant. If the QEMU command line is
353+
// ever changed (different host_share_mode, GPU passthrough,
354+
// extra bootable devices) this digest will need re-capturing.
318355
let bootorder_fwcfg_hash =
319356
hex!("53D3DBC00691328371E74C1B1C77BDF1BF4FF79DD1B55222B694FE8341CD8B0ADB3A9AB9286552C681272883B9B0FC1D");
357+
358+
// EV_EFI_VARIABLE_AUTHORITY: OVMF emits this once during BDS
359+
// even when Secure Boot is disabled. The 32-byte event blob in
360+
// the log is a sentinel; the actual measured payload is
361+
// OVMF-internal. Captured digest is a constant for the
362+
// edk2-stable202505 build dstack ships.
320363
let variable_authority_hash =
321364
hex!("FB66919801F1DFC9C4C273B6A739380790CB0FD3CB706A42F6AC050510EBC8618E7FBA53A1564522F5C6F0DC9E1F41A6");
322-
let boot_order_var_hash =
323-
hex!("52B9A02DE946B947364B57D8210C63113B9058996E2A3BA7CEAD54AF11AE0873B085D1E52BC01E4FEBE57CA05CA1332B");
324-
let boot0000_hash =
325-
hex!("5068E6A9DED2A1C3A8EBB5D26004410EA8670742D8F444C5C3D161B76C66FA23A7B1D2FB3F9840570B675384B5818F2D");
326-
let boot0001_hash =
327-
hex!("DD424F2EEB35F3E8A2C2F50F6CC87FF90B7577E92CE63E13A22869D07D104FD5EA9800E6E4F12C5058FC4EAA78374F20");
365+
366+
// BootOrder UEFI variable holds [0x0000, 0x0001] — the two
367+
// boot options OVMF's BDS publishes (UiApp and FrontPage).
368+
// The TCG digest for `EV_EFI_VARIABLE_BOOT2` is over the raw
369+
// variable data, NOT a UEFI_VARIABLE_DATA wrapper.
370+
let boot_order_var_hash = measure_sha384(&boot_order_bytes(&[0x0000, 0x0001]));
371+
372+
// Boot0000 = OVMF's BootManagerMenuApp; Boot0001 = "EFI
373+
// Firmware Setup" (FrontPage). Both live in the OVMF FV and
374+
// are baked into the firmware at build time. The attribute
375+
// bits and descriptions come from MdeModulePkg's
376+
// BdsBootManagerLib in edk2-stable202505.
377+
// 0x101 = LOAD_OPTION_ACTIVE | LOAD_OPTION_CATEGORY_APP
378+
// 0x109 = + LOAD_OPTION_HIDDEN
379+
let boot0000_hash = measure_sha384(&boot_option_bytes(
380+
0x0000_0109,
381+
"BootManagerMenuApp",
382+
&[
383+
fv_node(&OVMF_FV_GUID_LE),
384+
fv_file_node(&OVMF_UIAPP_FILE_GUID_LE),
385+
END_OF_DEVICE_PATH,
386+
],
387+
&[],
388+
));
389+
let boot0001_hash = measure_sha384(&boot_option_bytes(
390+
0x0000_0101,
391+
"EFI Firmware Setup",
392+
&[
393+
fv_node(&OVMF_FV_GUID_LE),
394+
fv_file_node(&OVMF_FRONTPAGE_FILE_GUID_LE),
395+
END_OF_DEVICE_PATH,
396+
],
397+
&[],
398+
));
328399
vec![
329400
td_hob_hash,
330401
cfv_image_hash.to_vec(),
@@ -340,9 +411,9 @@ impl<'a> Tdvf<'a> {
340411
acpi_rsdp_hash,
341412
acpi_tables_hash,
342413
variable_authority_hash.to_vec(),
343-
boot_order_var_hash.to_vec(),
344-
boot0000_hash.to_vec(),
345-
boot0001_hash.to_vec(),
414+
boot_order_var_hash,
415+
boot0000_hash,
416+
boot0001_hash,
346417
]
347418
}
348419
};

dstack-mr/src/uefi_var.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
//! Helpers for synthesising the UEFI variable byte blobs that OVMF measures
6+
//! into RTMR[0] as `EV_EFI_VARIABLE_BOOT2` events.
7+
//!
8+
//! For the BootOrder / Boot#### variables the TCG PFP spec digest is taken
9+
//! over the *variable data* portion only (not the full `UEFI_VARIABLE_DATA`
10+
//! struct), so we just build the on-the-wire variable contents here.
11+
12+
use crate::utf16_encode;
13+
14+
/// Build the raw bytes of a `BootOrder` UEFI variable from a sequence of boot
15+
/// option numbers — each entry is a little-endian `u16` referring to a
16+
/// `Boot####` variable.
17+
pub fn boot_order_bytes(entries: &[u16]) -> Vec<u8> {
18+
let mut out = Vec::with_capacity(entries.len() * 2);
19+
for &entry in entries {
20+
out.extend_from_slice(&entry.to_le_bytes());
21+
}
22+
out
23+
}
24+
25+
/// An `EFI_DEVICE_PATH_PROTOCOL` node.
26+
#[derive(Clone, Copy)]
27+
pub struct DevicePathNode<'a> {
28+
pub r#type: u8,
29+
pub subtype: u8,
30+
pub data: &'a [u8],
31+
}
32+
33+
impl DevicePathNode<'_> {
34+
fn write_to(self, buf: &mut Vec<u8>) {
35+
let len = 4 + self.data.len();
36+
buf.push(self.r#type);
37+
buf.push(self.subtype);
38+
buf.extend_from_slice(&(len as u16).to_le_bytes());
39+
buf.extend_from_slice(self.data);
40+
}
41+
}
42+
43+
/// `END_ENTIRE_DEVICE_PATH` terminator.
44+
pub const END_OF_DEVICE_PATH: DevicePathNode<'static> = DevicePathNode {
45+
r#type: 0x7f,
46+
subtype: 0xff,
47+
data: &[],
48+
};
49+
50+
/// `MEDIA_DEVICE_PATH / Firmware Volume` node (`type=4, subtype=7`).
51+
pub fn fv_node(guid_le: &[u8; 16]) -> DevicePathNode<'_> {
52+
DevicePathNode {
53+
r#type: 0x04,
54+
subtype: 0x07,
55+
data: guid_le,
56+
}
57+
}
58+
59+
/// `MEDIA_DEVICE_PATH / Firmware File` node (`type=4, subtype=6`).
60+
pub fn fv_file_node(guid_le: &[u8; 16]) -> DevicePathNode<'_> {
61+
DevicePathNode {
62+
r#type: 0x04,
63+
subtype: 0x06,
64+
data: guid_le,
65+
}
66+
}
67+
68+
/// Build the raw bytes of a `Boot####` UEFI variable — the on-the-wire form of
69+
/// `EFI_LOAD_OPTION { Attributes, FilePathListLength, Description, FilePathList,
70+
/// OptionalData }`.
71+
///
72+
/// The description is automatically NUL-terminated in UTF-16LE.
73+
pub fn boot_option_bytes(
74+
attributes: u32,
75+
description: &str,
76+
file_path_nodes: &[DevicePathNode<'_>],
77+
optional_data: &[u8],
78+
) -> Vec<u8> {
79+
// Serialise the device-path list first so we know its length.
80+
let mut file_path = Vec::new();
81+
for node in file_path_nodes {
82+
node.write_to(&mut file_path);
83+
}
84+
85+
let mut desc = utf16_encode(description);
86+
desc.extend_from_slice(&[0x00, 0x00]); // NUL terminator
87+
88+
let mut out = Vec::with_capacity(4 + 2 + desc.len() + file_path.len() + optional_data.len());
89+
out.extend_from_slice(&attributes.to_le_bytes());
90+
out.extend_from_slice(&(file_path.len() as u16).to_le_bytes());
91+
out.extend_from_slice(&desc);
92+
out.extend_from_slice(&file_path);
93+
out.extend_from_slice(optional_data);
94+
out
95+
}
96+
97+
#[cfg(test)]
98+
mod tests {
99+
use super::*;
100+
use sha2::{Digest, Sha384};
101+
102+
fn sha384(bytes: &[u8]) -> String {
103+
hex::encode(Sha384::new_with_prefix(bytes).finalize())
104+
}
105+
106+
#[test]
107+
fn boot_option_round_trip_sample() {
108+
// Trivial sanity check: a load option with one MEDIA_FV_FILE node and
109+
// an empty description should serialise to a 4 (Attrs) + 2 (FpLen) +
110+
// 2 (NUL) + (4 + 16) (FV_FILE) + 4 (END) = 32 byte blob, and
111+
// round-tripping the descriptive string survives.
112+
let blob = boot_option_bytes(1, "", &[fv_file_node(&[0; 16]), END_OF_DEVICE_PATH], &[]);
113+
assert_eq!(blob.len(), 4 + 2 + 2 + 20 + 4);
114+
assert_eq!(&blob[0..4], &[0x01, 0x00, 0x00, 0x00]);
115+
assert_eq!(&blob[4..6], &[0x18, 0x00]); // FilePathListLength = 24
116+
// Description is just a NUL terminator (two bytes of 0).
117+
assert_eq!(&blob[6..8], &[0x00, 0x00]);
118+
}
119+
120+
#[test]
121+
fn boot_order_encodes_u16_le_entries() {
122+
assert_eq!(
123+
boot_order_bytes(&[0x0000, 0x0001]),
124+
vec![0x00, 0x00, 0x01, 0x00]
125+
);
126+
assert_eq!(
127+
boot_order_bytes(&[0x1234, 0xabcd]),
128+
vec![0x34, 0x12, 0xcd, 0xab]
129+
);
130+
assert_eq!(
131+
sha384(&boot_order_bytes(&[0x0000, 0x0001])),
132+
"52b9a02de946b947364b57d8210c63113b9058996e2a3ba7cead54af11ae0873b085d1e52bc01e4febe57ca05ca1332b"
133+
);
134+
}
135+
}

0 commit comments

Comments
 (0)