diff --git a/Cargo.lock b/Cargo.lock index baebb9c73f..1c573acf72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,6 +2084,7 @@ dependencies = [ "guestmem", "guid", "hcl_compat_uefi_nvram_storage", + "hyperv_secure_boot_templates", "inspect", "jiff", "local_clock", @@ -3259,7 +3260,9 @@ dependencies = [ "thiserror 2.0.16", "tracing", "ucs2 0.0.0", + "uefi_nvram_specvars", "uefi_nvram_storage", + "uefi_specs", "vmcore", "wchar", "zerocopy", diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 192f0bbf40..726f9c6046 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -2562,6 +2562,7 @@ async fn new_underhill_vm( } } }; + let base_secure_boot_template_vars = base_vars.clone(); // check if vmgs includes custom UEFI JSON let custom_uefi_json_data = if let Some(vmgs_client) = vmgs_client.as_ref() { @@ -2574,6 +2575,7 @@ async fn new_underhill_vm( } else { None }; + let custom_uefi_config_present = custom_uefi_json_data.is_some(); // obtain the final custom uefi vars by applying the delta onto // the base vars @@ -2599,7 +2601,9 @@ async fn new_underhill_vm( }; let config = firmware_uefi_resources::UefiConfig { + base_secure_boot_template_vars, custom_uefi_vars, + custom_uefi_config_present, secure_boot: dps.general.secure_boot_enabled, initial_generation_id, use_mmio: cfg!(not(guest_arch = "x86_64")), diff --git a/openvmm/openvmm_entry/src/lib.rs b/openvmm/openvmm_entry/src/lib.rs index 0a07f1d74f..b9c17d3d72 100644 --- a/openvmm/openvmm_entry/src/lib.rs +++ b/openvmm/openvmm_entry/src/lib.rs @@ -1177,7 +1177,7 @@ async fn vm_config_from_command_line( ); } - let custom_uefi_vars = { + let (base_secure_boot_template_vars, custom_uefi_vars, custom_uefi_config_present) = { use firmware_uefi_custom_vars::CustomVars; // load base vars from specified template, or use an empty set of base @@ -1199,6 +1199,7 @@ async fn vm_config_from_command_line( }, None => CustomVars::default(), }; + let base_secure_boot_template_vars = base_vars.clone(); // TODO: fallback to VMGS read if no command line flag was given @@ -1206,15 +1207,22 @@ async fn vm_config_from_command_line( Some(file) => Some(fs_err::read(file).context("opening custom uefi json file")?), None => None, }; + let custom_uefi_config_present = custom_uefi_json_data.is_some(); // obtain the final custom uefi vars by applying the delta onto the base vars - match custom_uefi_json_data { + let custom_uefi_vars = match custom_uefi_json_data { Some(data) => { let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?; base_vars.apply_delta(delta)? } None => base_vars, - } + }; + + ( + base_secure_boot_template_vars, + custom_uefi_vars, + custom_uefi_config_present, + ) }; if opt.uefi { @@ -1230,7 +1238,9 @@ async fn vm_config_from_command_line( }; chipset = chipset.with_uefi(vm_manifest_builder::UefiManifest::new( arch, + base_secure_boot_template_vars, custom_uefi_vars, + custom_uefi_config_present, opt.secure_boot, log_level, None, diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 67c37d84ee..a515b47673 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -393,7 +393,7 @@ impl PetriVmConfigOpenVmm { // OpenhclUefi uses BaseChipsetType::HclHost, so it does not need this. if matches!(firmware, Firmware::Uefi { .. }) { let uefi_cfg = firmware.uefi_config(); - let custom_uefi_vars = + let base_secure_boot_template_vars = uefi_cfg.map_or_else(Default::default, |c| match (arch, c.secure_boot_template) { (MachineArch::X86_64, Some(SecureBootTemplate::MicrosoftWindows)) => { hyperv_secure_boot_templates::x64::microsoft_windows() @@ -411,6 +411,7 @@ impl PetriVmConfigOpenVmm { ) => hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca(), (_, None) => Default::default(), }); + let custom_uefi_vars = base_secure_boot_template_vars.clone(); let secure_boot = uefi_cfg.is_some_and(|c| c.secure_boot_enabled); let log_level = match uefi_cfg .map(|c| c.efi_diagnostics_log_level) @@ -433,7 +434,9 @@ impl PetriVmConfigOpenVmm { MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64, MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64, }, + base_secure_boot_template_vars, custom_uefi_vars, + false, secure_boot, log_level, diagnostics_rate_limit, diff --git a/vm/devices/firmware/firmware_uefi/Cargo.toml b/vm/devices/firmware/firmware_uefi/Cargo.toml index ada0e567e0..fa749fe804 100644 --- a/vm/devices/firmware/firmware_uefi/Cargo.toml +++ b/vm/devices/firmware/firmware_uefi/Cargo.toml @@ -14,6 +14,7 @@ fuzzing = [] firmware_uefi_custom_vars.workspace = true firmware_uefi_resources.workspace = true hcl_compat_uefi_nvram_storage = { workspace = true, features = ["inspect", "save_restore"] } +hyperv_secure_boot_templates.workspace = true uefi_nvram_storage = { workspace = true, features = ["inspect", "save_restore"] } uefi_specs.workspace = true uefi_nvram_specvars.workspace = true diff --git a/vm/devices/firmware/firmware_uefi/src/resolver.rs b/vm/devices/firmware/firmware_uefi/src/resolver.rs index 65044dd2e6..f465769273 100644 --- a/vm/devices/firmware/firmware_uefi/src/resolver.rs +++ b/vm/devices/firmware/firmware_uefi/src/resolver.rs @@ -17,8 +17,12 @@ use firmware_uefi_resources::UefiDeviceHandle; use firmware_uefi_resources::UefiLoggerHandleKind; use firmware_uefi_resources::UefiVsmConfigHandleKind; use firmware_uefi_resources::UefiWatchdogPlatformHandleKind; +use hcl_compat_uefi_nvram_storage::BaseSecureBootTemplateVariables; use hcl_compat_uefi_nvram_storage::HclCompatNvram; +use std::borrow::Cow; use thiserror::Error; +use uefi_nvram_specvars::signature_list::SignatureData; +use uefi_nvram_specvars::signature_list::SignatureList; use vm_resource::AsyncResolveResource; use vm_resource::ResolveError; use vm_resource::ResourceResolver; @@ -57,6 +61,62 @@ pub enum ResolveUefiDeviceError { Init(#[from] crate::UefiInitError), } +fn base_secure_boot_template_variables( + custom_vars: &firmware_uefi_custom_vars::CustomVars, +) -> Option { + let signatures = custom_vars.signatures.as_ref()?; + + let mut pk = Vec::new(); + extend_signature_var(std::iter::once(&signatures.pk), &mut pk); + + let mut kek = Vec::new(); + extend_signature_var(&signatures.kek, &mut kek); + + let mut db = Vec::new(); + extend_signature_var(&signatures.db, &mut db); + + let mut dbx = Vec::new(); + extend_signature_var(&signatures.dbx, &mut dbx); + + Some(BaseSecureBootTemplateVariables::new(pk, kek, db, dbx)) +} + +fn extend_signature_var<'a>( + signatures: impl IntoIterator, + data: &mut Vec, +) { + use firmware_uefi_custom_vars::Signature; + use uefi_specs::hyperv::nvram::vars::MSFT_SECURE_BOOT_PRODUCTION_GUID; + + for signature in signatures { + match signature { + Signature::X509(certs) => { + for cert in certs { + SignatureList::X509(SignatureData::new_x509( + MSFT_SECURE_BOOT_PRODUCTION_GUID, + Cow::Borrowed(cert.0.as_slice()), + )) + .extend_as_spec_signature_list(data); + } + } + Signature::Sha256(digests) => { + SignatureList::Sha256( + digests + .iter() + .map(|digest| { + SignatureData::new_sha256( + MSFT_SECURE_BOOT_PRODUCTION_GUID, + Cow::Borrowed(&digest.0), + ) + }) + .collect(), + ) + .extend_as_spec_signature_list(data); + } + } + } +} + // The ACPI GPE0 line to use for generation ID. This must match the value in // the DSDT. const GPE0_LINE_GENERATION_ID: u32 = 0; @@ -133,12 +193,33 @@ impl AsyncResolveResource for UefiDev } }; - let nvram_storage = Box::new(HclCompatNvram::new( + let nvram_storage = HclCompatNvram::new( vmm_core::emuplat::hcl_compat_uefi_nvram_storage::VmgsStorageBackendAdapter( nvram_storage, ), storage_quirks, - )); + ); + let nvram_storage = if config.secure_boot { + let baseline_configured = config.base_secure_boot_template_vars.signatures.is_some(); + tracing::info!( + baseline_configured, + baseline_revision = if baseline_configured { + hyperv_secure_boot_templates::BASELINE_REVISION + } else { + "none" + }, + custom_uefi_config_present = config.custom_uefi_config_present, + "secure boot configuration" + ); + + match base_secure_boot_template_variables(&config.base_secure_boot_template_vars) { + Some(template) => nvram_storage.with_base_secure_boot_template_variables(template), + None => nvram_storage, + } + } else { + nvram_storage + }; + let nvram_storage = Box::new(nvram_storage); let gm = input.encrypted_guest_memory.clone(); let runtime_deps = UefiRuntimeDeps { diff --git a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs index f088d4e10b..279a675397 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs @@ -84,7 +84,9 @@ impl NvramServices { }; if !is_restoring { - nvram.inject_vars_on_first_boot(custom_vars).await?; + nvram + .inject_vars_on_first_boot(custom_vars, secure_boot_enabled) + .await?; nvram.inject_hyperv_vars().await?; nvram.setup_secure_boot(secure_boot_enabled).await?; } @@ -104,6 +106,7 @@ impl NvramServices { async fn inject_vars_on_first_boot( &mut self, custom_vars: CustomVars, + secure_boot_enabled: bool, ) -> Result<(), NvramSetupError> { // "First boot" is marked by having no variables in nvram storage if !self @@ -160,6 +163,9 @@ impl NvramServices { } self.inject_custom_vars(custom_vars).await?; + if secure_boot_enabled { + self.services.after_custom_vars_injected(); + } Ok(()) } diff --git a/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs index 2fe5df92b6..f9a5a413b7 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/nvram/spec_services/mod.rs @@ -242,6 +242,12 @@ impl NvramSpecServices { self.storage.is_empty().await } + /// Called after custom UEFI variables are injected on first boot, + /// when secure boot is enabled. + pub fn after_custom_vars_injected(&self) { + self.storage.after_custom_vars_injected() + } + /// Update "SetupMode" based on the current value of "PK" /// /// From UEFI spec section 32.3 diff --git a/vm/devices/firmware/firmware_uefi_resources/src/lib.rs b/vm/devices/firmware/firmware_uefi_resources/src/lib.rs index 077b862372..7eae16c4a9 100644 --- a/vm/devices/firmware/firmware_uefi_resources/src/lib.rs +++ b/vm/devices/firmware/firmware_uefi_resources/src/lib.rs @@ -160,6 +160,8 @@ pub struct UefiConfig { pub command_set: UefiCommandSet, pub diagnostics_log_level: LogLevel, pub diagnostics_rate_limit: Option, + pub base_secure_boot_template_vars: CustomVars, + pub custom_uefi_config_present: bool, } /// Resource kind for the platform-provided UEFI logger. diff --git a/vm/devices/firmware/hcl_compat_uefi_nvram_storage/Cargo.toml b/vm/devices/firmware/hcl_compat_uefi_nvram_storage/Cargo.toml index 1c6caf7d00..963b80ebc6 100644 --- a/vm/devices/firmware/hcl_compat_uefi_nvram_storage/Cargo.toml +++ b/vm/devices/firmware/hcl_compat_uefi_nvram_storage/Cargo.toml @@ -14,7 +14,9 @@ save_restore = [ "dep:vmcore", "uefi_nvram_storage/save_restore"] [dependencies] hcl_compat_uefi_nvram_resources.workspace = true +uefi_nvram_specvars.workspace = true uefi_nvram_storage.workspace = true +uefi_specs.workspace = true vmcore = { workspace = true, optional = true } cvm_tracing.workspace = true diff --git a/vm/devices/firmware/hcl_compat_uefi_nvram_storage/src/lib.rs b/vm/devices/firmware/hcl_compat_uefi_nvram_storage/src/lib.rs index d2d5d4f004..fd7020e1da 100644 --- a/vm/devices/firmware/hcl_compat_uefi_nvram_storage/src/lib.rs +++ b/vm/devices/firmware/hcl_compat_uefi_nvram_storage/src/lib.rs @@ -18,18 +18,29 @@ pub mod storage_backend; +use core::mem::size_of; +use core::mem::size_of_val; use cvm_tracing::CVM_ALLOWED; use cvm_tracing::CVM_CONFIDENTIAL; use guid::Guid; use hcl_compat_uefi_nvram_resources::HclCompatNvramQuirks; -use std::fmt::Debug; +use std::collections::BTreeSet; use storage_backend::StorageBackend; use ucs2::Ucs2LeSlice; +use uefi_nvram_specvars::signature_list::ParseError as SignatureListParseError; +use uefi_nvram_specvars::signature_list::ParseSignatureList; +use uefi_nvram_specvars::signature_list::ParseSignatureLists; use uefi_nvram_storage::EFI_TIME; use uefi_nvram_storage::NextVariable; use uefi_nvram_storage::NvramStorage; use uefi_nvram_storage::NvramStorageError; use uefi_nvram_storage::in_memory; +use uefi_specs::uefi::nvram::EFI_VARIABLE_AUTHENTICATION_2; +use uefi_specs::uefi::nvram::signature_list::EFI_SIGNATURE_DATA; +use uefi_specs::uefi::nvram::vars; +use uefi_specs::uefi::signing::EFI_CERT_TYPE_PKCS7_GUID; +use uefi_specs::uefi::signing::WIN_CERT_TYPE_EFI_GUID; +use uefi_specs::uefi::signing::WIN_CERTIFICATE_UEFI_GUID; use zerocopy::FromBytes; use zerocopy::Immutable; use zerocopy::IntoBytes; @@ -42,6 +53,37 @@ const EFI_MAX_VARIABLE_DATA_SIZE: usize = 32 * 1024; // TODO: how big required for secure boot with db/dbx? const INITIAL_NVRAM_SIZE: usize = 32768; const MAXIMUM_NVRAM_SIZE: usize = INITIAL_NVRAM_SIZE * 4; +const WIN_CERT_REVISION_2_0: u16 = 0x0200; + +/// Secure Boot signature entry tracked by baseline telemetry. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum SignatureValue { + X509(EFI_SIGNATURE_DATA, Vec), + Sha256(EFI_SIGNATURE_DATA, Vec), +} + +type SignatureSet = BTreeSet; + +/// Base Secure Boot template variable contents expected to be present in loaded NVRAM. +#[derive(Clone, Debug)] +pub struct BaseSecureBootTemplateVariables { + pk: Vec, + kek: Vec, + db: Vec, + dbx: Vec, +} + +impl BaseSecureBootTemplateVariables { + /// Create a new base Secure Boot template variable set. + pub fn new(pk: Vec, kek: Vec, db: Vec, dbx: Vec) -> Self { + Self { pk, kek, db, dbx } + } + + /// Return whether there are no base Secure Boot template variables to track. + fn is_empty(&self) -> bool { + self.pk.is_empty() && self.kek.is_empty() && self.db.is_empty() && self.dbx.is_empty() + } +} mod format { use super::*; @@ -98,6 +140,9 @@ pub struct HclCompatNvram { // whether the NVRAM has been loaded, either from storage or saved state loaded: bool, + + #[cfg_attr(feature = "inspect", inspect(skip))] + base_secure_boot_template_variables: Option, } impl HclCompatNvram { @@ -115,9 +160,22 @@ impl HclCompatNvram { nvram_buf: Vec::new(), loaded: false, + + base_secure_boot_template_variables: None, } } + /// Store the base Secure Boot template variables for later observation. + pub fn with_base_secure_boot_template_variables( + mut self, + template: BaseSecureBootTemplateVariables, + ) -> Self { + if !template.is_empty() { + self.base_secure_boot_template_variables = Some(template); + } + self + } + async fn lazy_load_from_storage(&mut self) -> Result<(), NvramStorageError> { let res = self.lazy_load_from_storage_inner().await; if let Err(e) = &res { @@ -142,6 +200,7 @@ impl HclCompatNvram { .await .map_err(|e| NvramStorageError::Load(e.into()))? .unwrap_or_default(); + let loaded_existing_state = !nvram_buf.is_empty(); if nvram_buf.len() > MAXIMUM_NVRAM_SIZE { return Err(NvramStorageError::Load( @@ -275,9 +334,117 @@ impl HclCompatNvram { } self.loaded = true; + if loaded_existing_state { + self.report_secure_boot_base_template_presence(); + } Ok(()) } + /// Report whether each base Secure Boot template variable is present in loaded NVRAM. + fn report_secure_boot_base_template_presence(&self) { + let Some(template) = &self.base_secure_boot_template_variables else { + return; + }; + + // TODO: Determine whether custom keys can be appended to PK before + // requiring an exact PK match instead of baseline set membership. + for (variable, (vendor, name), base_template_variable) in [ + ("PK", vars::PK(), template.pk.as_slice()), + ("KEK", vars::KEK(), template.kek.as_slice()), + ("db", vars::DB(), template.db.as_slice()), + ("dbx", vars::DBX(), template.dbx.as_slice()), + ] { + // Load the variable from NVRAM. + let loaded_variable = match self + .in_memory + .iter() + .find(|entry| entry.vendor == vendor && entry.name.as_bytes() == name.as_bytes()) + .map(|entry| entry.data) + { + Some(loaded_variable) if !loaded_variable.is_empty() => loaded_variable, + loaded_variable => { + tracing::warn!( + CVM_ALLOWED, + variable, + loaded_variable_bytes = loaded_variable.map_or(0, |data| data.len()), + "base secure boot template variable is missing in NVRAM" + ); + continue; + } + }; + + // Parse the loaded variable into a set of signatures. + let loaded_variable_bytes = loaded_variable.len(); + let loaded_signatures = match collect_signature_set(loaded_variable) { + Ok(signatures) => signatures, + Err(error) => { + tracing::warn!( + CVM_CONFIDENTIAL, + variable, + error = &error as &dyn std::error::Error, + "failed to parse loaded secure boot variable" + ); + continue; + } + }; + let loaded_entries = loaded_signatures.len(); + + // Parse the base template variable into a set of signatures. + let base_template_bytes = base_template_variable.len(); + let base_template_signatures = match collect_signature_set(base_template_variable) { + Ok(signatures) if !signatures.is_empty() => signatures, + Ok(_) => { + tracing::warn!( + CVM_ALLOWED, + variable, + base_template_bytes, + "base secure boot template variable contains no signatures" + ); + continue; + } + Err(error) => { + tracing::warn!( + CVM_CONFIDENTIAL, + variable, + error = &error as &dyn std::error::Error, + "failed to parse base secure boot template variable" + ); + continue; + } + }; + let base_template_entries = base_template_signatures.len(); + + // Count how many base template signatures are missing from the loaded variable. + let missing_entries = base_template_signatures + .difference(&loaded_signatures) + .count(); + + if missing_entries == 0 { + tracing::info!( + CVM_ALLOWED, + variable, + base_template_entries, + loaded_entries, + missing_entries, + base_template_bytes, + loaded_variable_bytes, + "base secure boot template variable is present" + ); + } else { + tracing::warn!( + CVM_ALLOWED, + variable, + base_template_entries, + loaded_entries, + missing_entries, + base_template_bytes, + loaded_variable_bytes, + "base secure boot template variable is missing" + ); + } + } + } + /// Dump in-memory nvram to the underlying storage device. async fn flush_storage(&mut self) -> Result<(), NvramStorageError> { self.nvram_buf.clear(); @@ -458,6 +625,64 @@ impl NvramStorage for HclCompatNvram { self.in_memory.next_variable(name_vendor).await } + + fn after_custom_vars_injected(&self) { + HclCompatNvram::report_secure_boot_base_template_presence(self) + } +} + +/// Parse a serialized Secure Boot variable into a comparable set of signatures. +fn collect_signature_set(data: &[u8]) -> Result { + let mut signatures = BTreeSet::new(); + + for list in ParseSignatureLists::new(signature_list_payload(data)) { + match list? { + ParseSignatureList::X509(certs) => { + for cert in certs { + let cert = cert?; + signatures.insert(SignatureValue::X509( + cert.header, + cert.data.0.as_ref().to_vec(), + )); + } + } + ParseSignatureList::Sha256(digests) => { + for digest in digests { + let digest = digest?; + signatures.insert(SignatureValue::Sha256( + digest.header, + digest.data.0.as_ref().to_vec(), + )); + } + } + } + } + + Ok(signatures) +} + +/// Return the `EFI_SIGNATURE_LIST` payload, skipping a valid auth header if present. +fn signature_list_payload(data: &[u8]) -> &[u8] { + let Ok((auth, _)) = EFI_VARIABLE_AUTHENTICATION_2::read_from_prefix(data) else { + return data; + }; + + if auth.auth_info.header.revision != WIN_CERT_REVISION_2_0 + || auth.auth_info.header.certificate_type != WIN_CERT_TYPE_EFI_GUID + || auth.auth_info.cert_type != EFI_CERT_TYPE_PKCS7_GUID + { + return data; + } + + let cert_len = auth.auth_info.header.length as usize; + if cert_len < size_of::() { + return data; + } + + let Some(auth_len) = size_of_val(&auth.timestamp).checked_add(cert_len) else { + return data; + }; + data.get(auth_len..).unwrap_or(data) } #[cfg(feature = "save_restore")] @@ -490,10 +715,78 @@ mod test { use super::storage_backend::StorageBackendError; use super::*; use pal_async::async_test; + use std::borrow::Cow; use ucs2::Ucs2LeVec; + use uefi_nvram_specvars::signature_list::SignatureData; + use uefi_nvram_specvars::signature_list::SignatureList; use uefi_nvram_storage::in_memory::impl_agnostic_tests; use wchar::wchz; + const TEST_OWNER: Guid = Guid { + data1: 1, + data2: 0, + data3: 0, + data4: [0; 8], + }; + + fn x509_variable(owner: Guid, certs: &[&'static [u8]]) -> Vec { + let mut data = Vec::new(); + for cert in certs { + SignatureList::X509(SignatureData::new_x509(owner, Cow::Borrowed(*cert))) + .extend_as_spec_signature_list(&mut data); + } + data + } + + fn signature_set(data: &[u8]) -> SignatureSet { + collect_signature_set(data).unwrap() + } + + #[test] + fn empty_secure_boot_variable_contains_no_signatures() { + assert!(signature_set(&[]).is_empty()); + } + + #[test] + fn base_secure_boot_template_variable_counts_missing_entries() { + let base_data = x509_variable(TEST_OWNER, &[b"cert1", b"cert2"]); + let loaded_data = x509_variable(TEST_OWNER, &[b"cert1", b"cert3"]); + let base = signature_set(&base_data); + let loaded = signature_set(&loaded_data); + let missing_entries = base.difference(&loaded).count(); + + assert_eq!(base.len(), 2); + assert_eq!(loaded.len(), 2); + assert_eq!(missing_entries, 1); + } + + #[test] + fn secure_boot_template_comparison_includes_signature_owner() { + let baseline = signature_set(&x509_variable(TEST_OWNER, &[b"cert1"])); + let loaded = signature_set(&x509_variable(Guid::new_random(), &[b"cert1"])); + + assert_ne!(baseline, loaded); + } + + #[test] + fn secure_boot_template_variable_skips_auth_header() { + let data = x509_variable(TEST_OWNER, &[b"cert1"]); + let mut authenticated_data = EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes().to_vec(); + authenticated_data.extend_from_slice(&data); + + assert_eq!(signature_set(&authenticated_data), signature_set(&data)); + } + + #[test] + fn secure_boot_template_variable_rejects_invalid_auth_header_length() { + let mut data = EFI_VARIABLE_AUTHENTICATION_2::DUMMY.as_bytes().to_vec(); + let length_offset = size_of_val(&EFI_VARIABLE_AUTHENTICATION_2::DUMMY.timestamp); + data[length_offset..length_offset + size_of::()] + .copy_from_slice(&u32::MAX.to_ne_bytes()); + + assert_eq!(signature_list_payload(&data), data); + } + /// An ephemeral implementation of [`StorageBackend`] backed by an in-memory /// buffer. Useful for tests, stateless VM scenarios. #[derive(Default)] diff --git a/vm/devices/firmware/hyperv_secure_boot_templates/src/lib.rs b/vm/devices/firmware/hyperv_secure_boot_templates/src/lib.rs index 3926eaec0c..0cce71edbf 100644 --- a/vm/devices/firmware/hyperv_secure_boot_templates/src/lib.rs +++ b/vm/devices/firmware/hyperv_secure_boot_templates/src/lib.rs @@ -11,6 +11,11 @@ //! gates! Unused templates should be stripped from the final binary by the //! linker. +/// Revision of the Secure Boot baseline represented by the built-in templates. +/// Update this value when the built-in templates are updated to a new baseline. +/// TODO: Should we change our baseline identifier? +pub const BASELINE_REVISION: &str = "July 2026"; + macro_rules! include_templates { ( $(($fn_name:ident, $path:literal),)* @@ -27,7 +32,8 @@ macro_rules! include_templates { // in the final bin (given that much of the parsing + validation // code is shared between both templates and user custom uefi // JSON files), it may result in a nice .rodata size decrease. - hyperv_uefi_custom_vars_json::load_template_from_json(include_bytes!(concat!(env!("OUT_DIR"), "/", $path))).unwrap() + hyperv_uefi_custom_vars_json::load_template_from_json(include_bytes!(concat!(env!("OUT_DIR"), "/", $path))) + .unwrap() } )* @@ -36,7 +42,7 @@ macro_rules! include_templates { $( #[test] fn $fn_name() { - super::$fn_name(); + assert!(super::$fn_name().signatures.is_some()); } )* } diff --git a/vm/devices/firmware/uefi_nvram_storage/src/lib.rs b/vm/devices/firmware/uefi_nvram_storage/src/lib.rs index 36926d3020..69cdde79de 100644 --- a/vm/devices/firmware/uefi_nvram_storage/src/lib.rs +++ b/vm/devices/firmware/uefi_nvram_storage/src/lib.rs @@ -116,6 +116,10 @@ pub trait NvramStorage: Send + Sync { NextVariable::EndOfList )) } + + /// Called after custom UEFI variables are injected on first boot, + /// when secure boot is enabled. + fn after_custom_vars_injected(&self) {} } #[async_trait::async_trait] @@ -167,6 +171,10 @@ impl NvramStorage for Box { ) -> Result { (**self).next_variable(name_vendor).await } + + fn after_custom_vars_injected(&self) { + (**self).after_custom_vars_injected() + } } /// Defines a trait that combines NvramStorage, Inspect, and SaveRestore @@ -236,6 +244,10 @@ mod save_restore { ) -> Result { (**self).next_variable(name_vendor).await } + + fn after_custom_vars_injected(&self) { + (**self).after_custom_vars_injected() + } } impl SaveRestore for Box> { diff --git a/vmm_core/vm_manifest_builder/src/lib.rs b/vmm_core/vm_manifest_builder/src/lib.rs index 9fcf7dff34..2f14d4876f 100644 --- a/vmm_core/vm_manifest_builder/src/lib.rs +++ b/vmm_core/vm_manifest_builder/src/lib.rs @@ -113,7 +113,9 @@ impl UefiManifest { /// [`SystemTimeClockHandle`]: chipset_resources::cmos_rtc_time_source::SystemTimeClockHandle pub fn new( arch: MachineArch, + base_secure_boot_template_vars: CustomVars, custom_uefi_vars: CustomVars, + custom_uefi_config_present: bool, secure_boot: bool, diagnostics_log_level: LogLevel, diagnostics_rate_limit: Option, @@ -124,7 +126,9 @@ impl UefiManifest { getrandom::fill(&mut initial_generation_id).expect("rng failure"); Self { config: UefiConfig { + base_secure_boot_template_vars, custom_uefi_vars, + custom_uefi_config_present, secure_boot, initial_generation_id, use_mmio: !matches!(arch, MachineArch::X86_64),