From 7edd818c65e7a7e3188c8bb0eac4fdd28abd9c7f Mon Sep 17 00:00:00 2001 From: mayank-microsoft Date: Wed, 22 Jul 2026 15:30:54 +0530 Subject: [PATCH] product_policy: enforce UEFI security policy, ephemeral VMGS, and Secure AVIC Wire the measured product policy into runtime and build-time enforcement: - uefi_security_policy: parse the custom UEFI JSON and enforce Replace-mode secure boot signatures, self-contained PK/KEK/db/dbx, and BootConfigurationDataHash presence; add get_validated_uefi_json and enforce_ephemeral_vmgs_required. - underhill_core: enforce the secure-boot policy and ephemeral-VMGS requirement, and prefer the policy's validated UEFI JSON over the host VMGS store. - loader/paravisor: run the same validation at build time. - CwcowPolicy::enforce_secure_avic, sourced from SnpBackedShared (x86_64 only). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9828afc4-c610-4883-bee1-816052ce7328 --- Cargo.lock | 3 + openhcl/product_policy/Cargo.toml | 3 + openhcl/product_policy/src/cwcow.rs | 39 +- openhcl/product_policy/src/sivm.rs | 27 +- openhcl/product_policy/src/tests.rs | 363 ++++++++++++++++++ .../src/uefi_security_policy.rs | 97 +++-- .../src/measured_product_policy.rs | 73 ++++ openhcl/underhill_core/src/worker.rs | 74 ++-- openhcl/virt_mshv_vtl/src/lib.rs | 9 + .../virt_mshv_vtl/src/processor/snp/mod.rs | 3 +- vm/loader/src/paravisor.rs | 6 + 11 files changed, 628 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c5e8458304..abe7bc43e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6715,11 +6715,14 @@ version = "0.0.0" dependencies = [ "anyhow", "base64 0.22.1", + "firmware_uefi_custom_vars", + "hyperv_uefi_custom_vars_json", "inspect", "mesh_protobuf", "paste", "serde", "serde_json", + "uefi_specs", ] [[package]] diff --git a/openhcl/product_policy/Cargo.toml b/openhcl/product_policy/Cargo.toml index fcca471e50..cca58d68f0 100644 --- a/openhcl/product_policy/Cargo.toml +++ b/openhcl/product_policy/Cargo.toml @@ -15,6 +15,9 @@ mesh_protobuf.workspace = true paste.workspace = true anyhow.workspace = true +hyperv_uefi_custom_vars_json.workspace = true +firmware_uefi_custom_vars.workspace = true +uefi_specs.workspace = true base64 = { workspace = true, optional = true, features = ["alloc"] } inspect = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["alloc", "derive"] } diff --git a/openhcl/product_policy/src/cwcow.rs b/openhcl/product_policy/src/cwcow.rs index 2345a35b74..7782377791 100644 --- a/openhcl/product_policy/src/cwcow.rs +++ b/openhcl/product_policy/src/cwcow.rs @@ -15,7 +15,7 @@ use alloc::vec::Vec; #[mesh(package = "openhcl.product_policy")] /// Cwcow policy pub struct CwcowPolicy { - /// Reserved: require an ephemeral VMGS. Not enforced at runtime yet. + /// Require an ephemeral VMGS guest state lifetime. #[mesh(1)] pub require_ephemeral_vmgs: bool, @@ -23,17 +23,16 @@ pub struct CwcowPolicy { #[mesh(2)] pub require_secure_boot: bool, - /// Reserved: require PK/KEK/db/dbx variables. Not enforced at runtime yet. + /// Require PK/KEK/db/dbx variables to be self-contained. #[mesh(3)] pub require_secure_boot_vars: bool, - /// Reserved: require `BootConfigurationDataHash`. Not enforced at runtime yet. + /// Require `BootConfigurationDataHash`. #[mesh(4)] pub require_bcd_integrity: bool, - /// Custom UEFI JSON bytes (base64 in manifest JSON). Required in - /// manifests and asserted non-empty at build time when secure boot - /// plus secure-boot-vars or BCD-integrity are set; + /// Custom UEFI JSON bytes (base64 in manifest JSON); mandatory when + /// secure boot plus secure-boot-vars or BCD-integrity are set. #[mesh(5)] #[cfg_attr( feature = "manifest", @@ -42,7 +41,7 @@ pub struct CwcowPolicy { #[cfg_attr(feature = "inspect", inspect(with = "Vec::::len"))] pub custom_uefi_json: Vec, - /// Reserved: require Secure AVIC. Not enforced at runtime yet. + /// Require Secure AVIC to be enabled. #[mesh(6)] pub require_secure_avic: bool, } @@ -51,6 +50,32 @@ impl crate::uefi_security_policy::UefiSecurityPolicyParams for CwcowPolicy { fn require_secure_boot(&self) -> bool { self.require_secure_boot } + + fn require_secure_boot_vars(&self) -> bool { + self.require_secure_boot_vars + } + + fn require_bcd_integrity(&self) -> bool { + self.require_bcd_integrity + } + + fn custom_uefi_json(&self) -> &[u8] { + &self.custom_uefi_json + } + + fn require_ephemeral_vmgs(&self) -> bool { + self.require_ephemeral_vmgs + } } impl crate::uefi_security_policy::UefiSecurityPolicy for CwcowPolicy {} + +impl CwcowPolicy { + /// Enforce that Secure AVIC is enabled if required by the policy. + pub fn enforce_secure_avic(&self, on: bool) -> anyhow::Result<()> { + if self.require_secure_avic && !on { + anyhow::bail!("product policy requires Secure AVIC to be enabled"); + } + Ok(()) + } +} diff --git a/openhcl/product_policy/src/sivm.rs b/openhcl/product_policy/src/sivm.rs index 218aa4d67f..a216972c8c 100644 --- a/openhcl/product_policy/src/sivm.rs +++ b/openhcl/product_policy/src/sivm.rs @@ -21,7 +21,7 @@ use crate::uefi_security_policy::UefiSecurityPolicy; #[cfg_attr(feature = "inspect", derive(inspect::Inspect))] #[mesh(package = "openhcl.product_policy")] pub struct SivmPolicy { - /// Reserved: require an ephemeral VMGS. Not enforced at runtime yet. + /// Require an ephemeral VMGS guest state lifetime. #[mesh(1)] pub require_ephemeral_vmgs: bool, @@ -29,17 +29,16 @@ pub struct SivmPolicy { #[mesh(2)] pub require_secure_boot: bool, - /// Reserved: require PK/KEK/db/dbx variables. Not enforced at runtime yet. + /// Require PK/KEK/db/dbx variables to be self-contained. #[mesh(3)] pub require_secure_boot_vars: bool, - /// Reserved: require `BootConfigurationDataHash`. Not enforced at runtime yet. + /// Require `BootConfigurationDataHash`. #[mesh(4)] pub require_bcd_integrity: bool, - /// Custom UEFI JSON bytes (base64 in manifest JSON). Required in - /// manifests and asserted non-empty at build time when secure boot - /// plus secure-boot-vars or BCD-integrity are set; + /// Custom UEFI JSON bytes (base64 in manifest JSON); mandatory when + /// secure boot plus secure-boot-vars or BCD-integrity are set. #[mesh(5)] #[cfg_attr( feature = "manifest", @@ -53,6 +52,22 @@ impl crate::uefi_security_policy::UefiSecurityPolicyParams for SivmPolicy { fn require_secure_boot(&self) -> bool { self.require_secure_boot } + + fn require_secure_boot_vars(&self) -> bool { + self.require_secure_boot_vars + } + + fn require_bcd_integrity(&self) -> bool { + self.require_bcd_integrity + } + + fn custom_uefi_json(&self) -> &[u8] { + &self.custom_uefi_json + } + + fn require_ephemeral_vmgs(&self) -> bool { + self.require_ephemeral_vmgs + } } impl UefiSecurityPolicy for SivmPolicy {} diff --git a/openhcl/product_policy/src/tests.rs b/openhcl/product_policy/src/tests.rs index d96d9a0703..cc39476af5 100644 --- a/openhcl/product_policy/src/tests.rs +++ b/openhcl/product_policy/src/tests.rs @@ -253,3 +253,366 @@ mod measured_policy_tests { assert_eq!(json.as_deref(), Some(&b"hi"[..])); } } + +mod uefi_security_policy_tests { + use super::*; + use alloc::string::ToString; + + #[test] + fn secure_boot_flag_off_passes_either_way() { + let p = SivmPolicy::default(); + assert!(p.validate_secure_boot_enabled(false).is_ok()); + assert!(p.validate_secure_boot_enabled(true).is_ok()); + } + + #[test] + fn secure_boot_flag_on_passes_when_enabled() { + let p = SivmPolicy { + require_secure_boot: true, + ..Default::default() + }; + assert!(p.validate_secure_boot_enabled(true).is_ok()); + } + + #[test] + fn secure_boot_flag_on_fails_when_disabled() { + let p = SivmPolicy { + require_secure_boot: true, + ..Default::default() + }; + let err = p.validate_secure_boot_enabled(false).unwrap_err(); + assert!(err.to_string().contains("secure boot")); + } + + #[test] + fn get_validated_uefi_json_fails_on_empty() { + let p = SivmPolicy { + custom_uefi_json: vec![], + ..Default::default() + }; + let err = p.get_validated_uefi_json().unwrap_err(); + assert!(err.to_string().contains("custom UEFI JSON")); + } + + #[test] + fn enforcement_rejects_unparseable_json() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: vec![0xFF, 0xFE], + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("failed to parse")); + } + + /// Valid Replace-mode JSON with explicit PK/KEK/db/dbx. + const REPLACE_JSON: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }, + "KEK": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "db": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "dbx": [{ + "type": "sha256", + "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] + }] + } + } + } +}"#; + + /// Valid Replace-mode JSON with BCD hash custom variable. + const REPLACE_JSON_WITH_BCD: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }, + "KEK": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "db": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "dbx": [{ + "type": "sha256", + "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] + }] + }, + "BootConfigurationDataHash": { + "guid": "Yd/ki8qT0hGqDQDgmAMrjA==", + "attributes": "BwAAAA==", + "value": "aGFzaHZhbHVl" + } + } + } +}"#; + + /// Replace-mode JSON with a BootConfigurationDataHash under a wrong namespace GUID. + const REPLACE_JSON_WITH_BCD_WRONG_GUID: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }, + "KEK": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "db": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }], + "dbx": [{ + "type": "sha256", + "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] + }] + }, + "BootConfigurationDataHash": { + "guid": "vZr6d1kDTTK9YCj05494Sw==", + "attributes": "BwAAAA==", + "value": "aGFzaHZhbHVl" + } + } + } +}"#; + + /// Append-mode JSON. + const APPEND_JSON: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Append", + "signatures": { + "KEK": [{ + "type": "x509", + "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] + }] + } + } + } +}"#; + + #[test] + fn enforcement_rejects_append_mode() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: APPEND_JSON.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("Replace mode")); + } + + #[test] + fn enforcement_passes_valid_replace_json() { + let p = SivmPolicy { + require_secure_boot_vars: true, + require_bcd_integrity: false, + custom_uefi_json: REPLACE_JSON.to_vec(), + ..Default::default() + }; + assert!(p.validate_secure_boot_policy_enforcement().is_ok()); + } + + #[test] + fn bcd_integrity_fails_when_hash_missing() { + let p = SivmPolicy { + require_bcd_integrity: true, + custom_uefi_json: REPLACE_JSON.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("BootConfigurationDataHash")); + } + + #[test] + fn bcd_integrity_passes_when_hash_present() { + let p = SivmPolicy { + require_bcd_integrity: true, + custom_uefi_json: REPLACE_JSON_WITH_BCD.to_vec(), + ..Default::default() + }; + assert!(p.validate_secure_boot_policy_enforcement().is_ok()); + } + + #[test] + fn bcd_integrity_fails_when_hash_has_wrong_guid() { + let p = SivmPolicy { + require_bcd_integrity: true, + custom_uefi_json: REPLACE_JSON_WITH_BCD_WRONG_GUID.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("BootConfigurationDataHash")); + } + + /// Replace-mode JSON where PK relies on the template (Default). + const REPLACE_JSON_PK_DEFAULT: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { "type": "Default" }, + "KEK": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "db": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "dbx": [{ "type": "sha256", "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] }] + } + } + } +}"#; + + /// Replace-mode JSON where KEK relies on the template (Default). + const REPLACE_JSON_KEK_DEFAULT: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }, + "KEK": [{ "type": "Default" }], + "db": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "dbx": [{ "type": "sha256", "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] }] + } + } + } +}"#; + + /// Replace-mode JSON where db relies on the template (Default). + const REPLACE_JSON_DB_DEFAULT: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }, + "KEK": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "db": [{ "type": "Default" }], + "dbx": [{ "type": "sha256", "value": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="] }] + } + } + } +}"#; + + /// Replace-mode JSON where dbx relies on the template (Default). + const REPLACE_JSON_DBX_DEFAULT: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": { + "PK": { "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }, + "KEK": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "db": [{ "type": "x509", "value": ["ZmFrZV9jZXJ0X2RhdGFfZm9yX3Rlc3Q="] }], + "dbx": [{ "type": "Default" }] + } + } + } +}"#; + + #[test] + fn enforcement_rejects_pk_default_when_secure_boot_vars_required() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: REPLACE_JSON_PK_DEFAULT.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("PK uses Default")); + } + + #[test] + fn enforcement_rejects_kek_default_when_secure_boot_vars_required() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: REPLACE_JSON_KEK_DEFAULT.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("KEK uses Default")); + } + + #[test] + fn enforcement_rejects_db_default_when_secure_boot_vars_required() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: REPLACE_JSON_DB_DEFAULT.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("db uses Default")); + } + + #[test] + fn enforcement_rejects_dbx_default_when_secure_boot_vars_required() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: REPLACE_JSON_DBX_DEFAULT.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("dbx uses Default")); + } + + /// JSON with no uefiSettings section. + const JSON_MISSING_UEFI_SETTINGS: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": {} +}"#; + + /// Replace-mode JSON with an empty signatures object. + const JSON_EMPTY_SIGNATURES: &[u8] = br#"{ + "type": "Microsoft.Compute/disks", + "properties": { + "uefiSettings": { + "signatureMode": "Replace", + "signatures": {} + } + } +}"#; + + #[test] + fn enforcement_rejects_missing_uefi_settings() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: JSON_MISSING_UEFI_SETTINGS.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("failed to parse")); + } + + #[test] + fn enforcement_rejects_empty_signatures() { + let p = SivmPolicy { + require_secure_boot_vars: true, + custom_uefi_json: JSON_EMPTY_SIGNATURES.to_vec(), + ..Default::default() + }; + let err = p.validate_secure_boot_policy_enforcement().unwrap_err(); + assert!(err.to_string().contains("failed to parse")); + } +} diff --git a/openhcl/product_policy/src/uefi_security_policy.rs b/openhcl/product_policy/src/uefi_security_policy.rs index e6790dff2b..e77fe3369b 100644 --- a/openhcl/product_policy/src/uefi_security_policy.rs +++ b/openhcl/product_policy/src/uefi_security_policy.rs @@ -10,6 +10,10 @@ /// exposed outside the crate. pub(crate) trait UefiSecurityPolicyParams { fn require_secure_boot(&self) -> bool; + fn require_secure_boot_vars(&self) -> bool; + fn require_bcd_integrity(&self) -> bool; + fn custom_uefi_json(&self) -> &[u8]; + fn require_ephemeral_vmgs(&self) -> bool; } /// A trait for validating UEFI security settings. Implementors only @@ -30,38 +34,77 @@ where } Ok(()) } -} -#[cfg(test)] -mod tests { - extern crate alloc; - use super::*; - use crate::sivm::SivmPolicy; - use alloc::string::ToString; + /// Validate the secure boot policy enforcement from the custom UEFI JSON. + fn validate_secure_boot_policy_enforcement(&self) -> anyhow::Result<()> { + validate_secure_boot_policy_enforcement_common(self) + } + + /// Return the custom UEFI JSON after validating it, or an error. + fn get_validated_uefi_json(&self) -> anyhow::Result<&[u8]> { + if self.custom_uefi_json().is_empty() { + anyhow::bail!("product policy requires custom UEFI JSON"); + } + self.validate_secure_boot_policy_enforcement()?; + Ok(self.custom_uefi_json()) + } - #[test] - fn secure_boot_flag_off_passes_either_way() { - let p = SivmPolicy::default(); - assert!(p.validate_secure_boot_enabled(false).is_ok()); - assert!(p.validate_secure_boot_enabled(true).is_ok()); + /// Enforce that the guest uses an ephemeral VMGS if required. + fn enforce_ephemeral_vmgs_required(&self, vmgs_is_ephemeral: bool) -> anyhow::Result<()> { + if self.require_ephemeral_vmgs() && !vmgs_is_ephemeral { + anyhow::bail!("product policy requires an ephemeral VMGS guest state lifetime"); + } + Ok(()) } +} + +/// Validate the secure boot policy from the parsed custom UEFI JSON. +fn validate_secure_boot_policy_enforcement_common( + params: &T, +) -> anyhow::Result<()> { + use firmware_uefi_custom_vars::delta::SignaturesDelta; + + let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(params.custom_uefi_json()) + .map_err(|e| anyhow::anyhow!("failed to parse custom UEFI JSON: {e}"))?; - #[test] - fn secure_boot_flag_on_passes_when_enabled() { - let p = SivmPolicy { - require_secure_boot: true, - ..Default::default() - }; - assert!(p.validate_secure_boot_enabled(true).is_ok()); + let sigs = match delta.signatures { + SignaturesDelta::Replace(r) => r, + SignaturesDelta::Append(_) => { + anyhow::bail!("product policy requires Replace mode for secure boot signatures"); + } + }; + + if params.require_secure_boot_vars() { + use firmware_uefi_custom_vars::delta::SignatureDelta; + use firmware_uefi_custom_vars::delta::SignatureDeltaVec; + + // All vars must carry explicit signatures; Default relies on a base template. + if matches!(sigs.pk, SignatureDelta::Default) { + anyhow::bail!("product policy: PK uses Default (not self-contained)"); + } + if matches!(sigs.kek, SignatureDeltaVec::Default) { + anyhow::bail!("product policy: KEK uses Default (not self-contained)"); + } + if matches!(sigs.db, SignatureDeltaVec::Default) { + anyhow::bail!("product policy: db uses Default (not self-contained)"); + } + if matches!(sigs.dbx, SignatureDeltaVec::Default) { + anyhow::bail!("product policy: dbx uses Default (not self-contained)"); + } } - #[test] - fn secure_boot_flag_on_fails_when_disabled() { - let p = SivmPolicy { - require_secure_boot: true, - ..Default::default() - }; - let err = p.validate_secure_boot_enabled(false).unwrap_err(); - assert!(err.to_string().contains("secure boot")); + if params.require_bcd_integrity() { + use uefi_specs::uefi::nvram::vars::EFI_GLOBAL_VARIABLE; + + let has_bcd_hash = delta.custom_vars.iter().any(|(name, value)| { + name == "BootConfigurationDataHash" && value.guid == EFI_GLOBAL_VARIABLE + }); + if !has_bcd_hash { + anyhow::bail!( + "product policy: require_bcd_integrity is set but BootConfigurationDataHash variable is missing from custom UEFI JSON" + ); + } } + + Ok(()) } diff --git a/openhcl/underhill_core/src/measured_product_policy.rs b/openhcl/underhill_core/src/measured_product_policy.rs index d8f08773c8..3234106fda 100644 --- a/openhcl/underhill_core/src/measured_product_policy.rs +++ b/openhcl/underhill_core/src/measured_product_policy.rs @@ -7,6 +7,8 @@ use crate::dispatch::LoadedVm; use anyhow::Context as _; +use firmware_uefi_custom_vars::CustomVars; +use get_protocol::dps_json::GuestStateLifetime; use product_policy::MeasuredProductPolicy; use product_policy::UefiSecurityPolicy; @@ -32,9 +34,46 @@ fn validate_uefi_security_policy( vm: &LoadedVm, ) -> anyhow::Result<()> { policy.validate_secure_boot_enabled(vm.device_platform_settings.general.secure_boot_enabled)?; + policy.validate_secure_boot_policy_enforcement()?; Ok(()) } +/// Enforce the policy's ephemeral-VMGS requirement, called before the VMGS is +/// opened so a policy requiring ephemeral can't trigger a host-VMGS read. +pub fn enforce_ephemeral_vmgs( + policy: &MeasuredProductPolicy, + guest_state_lifetime: GuestStateLifetime, +) -> anyhow::Result<()> { + let vmgs_is_ephemeral = matches!(guest_state_lifetime, GuestStateLifetime::Ephemeral); + policy.sivm(|p| p.enforce_ephemeral_vmgs_required(vmgs_is_ephemeral))?; + policy.cwcow(|p| p.enforce_ephemeral_vmgs_required(vmgs_is_ephemeral))?; + Ok(()) +} + +fn validated_uefi_json_fn(p: &T) -> anyhow::Result> +where + T: UefiSecurityPolicy, +{ + Ok(p.get_validated_uefi_json()?.to_vec()) +} + +/// Return the measured, validated UEFI nvram state from the policy applied onto +/// `base_vars`, or `None` when the policy is not present. +pub fn measured_uefi_nvram_state( + policy: &MeasuredProductPolicy, + base_vars: &CustomVars, +) -> anyhow::Result> { + let uefi_state_json = policy + .sivm(validated_uefi_json_fn)? + .or(policy.cwcow(validated_uefi_json_fn)?); + if let Some(uefi_state) = uefi_state_json { + let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&uefi_state)?; + let measured = base_vars.clone().apply_delta(delta)?; + return Ok(Some(measured)); + } + Ok(None) +} + /// Post-load validation of the measured product policy. pub fn validate(loaded_vm: &LoadedVm) -> anyhow::Result<()> { loaded_vm @@ -43,5 +82,39 @@ pub fn validate(loaded_vm: &LoadedVm) -> anyhow::Result<()> { loaded_vm .measured_product_policy .cwcow(|p| validate_uefi_security_policy(p, loaded_vm))?; + + #[cfg(guest_arch = "x86_64")] + { + let hardware_secure_avic_enabled = loaded_vm.partition.secure_avic_enabled(); + loaded_vm + .measured_product_policy + .cwcow(|p| p.enforce_secure_avic(hardware_secure_avic_enabled))?; + } Ok(()) } + +#[cfg(test)] +mod tests { + use product_policy::ProductPolicy; + use product_policy::sivm::SivmPolicy; + use test_with_tracing::test; + + use super::decode; + + #[test] + fn decode_checks_measured_policy_size() { + let expected = ProductPolicy::Sivm(SivmPolicy { + require_ephemeral_vmgs: true, + require_secure_boot: true, + ..Default::default() + }); + let encoded = product_policy::encode_product_policy(&expected); + + let measured = decode(&encoded, encoded.len()).expect("policy should decode"); + assert_eq!(measured.raw(), Some(&expected)); + + let error = decode(&encoded, encoded.len() + 1) + .expect_err("a declared size mismatch should be rejected"); + assert!(error.to_string().contains("product policy size mismatch")); + } +} diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 192f0bbf40..71b6873029 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -1801,6 +1801,12 @@ async fn new_underhill_vm( .context("failed to construct the processor topology")? }; + #[cfg(feature = "product_policy")] + crate::measured_product_policy::enforce_ephemeral_vmgs( + measured_vtl2_info.measured_product_policy(), + dps.general.guest_state_lifetime, + )?; + // also construct the VMGS nice and early, as much like the GET, it also // plays an important role during initial bringup let mut vmgs = match (dps.general.guest_state_lifetime, servicing_state.vmgs) { @@ -2563,39 +2569,51 @@ async fn new_underhill_vm( } }; - // check if vmgs includes custom UEFI JSON - let custom_uefi_json_data = if let Some(vmgs_client) = vmgs_client.as_ref() { - vmgs_client - .as_non_volatile_store(vmgs::FileId::CUSTOM_UEFI, false) - .context("failed to instantiate custom UEFI JSON store")? - .restore() - .await - .context("failed to get custom UEFI JSON data")? + #[cfg(feature = "product_policy")] + let policy_uefi_vars = crate::measured_product_policy::measured_uefi_nvram_state( + measured_vtl2_info.measured_product_policy(), + &base_vars, + )?; + #[cfg(not(feature = "product_policy"))] + let policy_uefi_vars: Option = None; + + let custom_uefi_vars = if let Some(vars) = policy_uefi_vars { + vars } else { - None - }; + // check if vmgs includes custom UEFI JSON + let custom_uefi_json_data = if let Some(vmgs_client) = vmgs_client.as_ref() { + vmgs_client + .as_non_volatile_store(vmgs::FileId::CUSTOM_UEFI, false) + .context("failed to instantiate custom UEFI JSON store")? + .restore() + .await + .context("failed to get custom UEFI JSON data")? + } else { + None + }; - // obtain the final custom uefi vars by applying the delta onto - // the base vars - let custom_uefi_vars = match custom_uefi_json_data { - Some(data) => { - let res = (|| -> Result { - let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?; - Ok(base_vars.apply_delta(delta)?) - })(); - - match res { - Ok(vars) => vars, - Err(e) => { - tracing::error!(CVM_ALLOWED, "Failed to load custom UEFI vars"); - get_client - .event_log_fatal(EventLogId::BOOT_FAILURE_SECURE_BOOT_FAILED) - .await; - return Err(e).context("failed to load custom UEFI variables"); + // obtain the final custom uefi vars by applying the delta onto + // the base vars + match custom_uefi_json_data { + Some(data) => { + let res = (|| -> Result { + let delta = hyperv_uefi_custom_vars_json::load_delta_from_json(&data)?; + Ok(base_vars.apply_delta(delta)?) + })(); + + match res { + Ok(vars) => vars, + Err(e) => { + tracing::error!(CVM_ALLOWED, "Failed to load custom UEFI vars"); + get_client + .event_log_fatal(EventLogId::BOOT_FAILURE_SECURE_BOOT_FAILED) + .await; + return Err(e).context("failed to load custom UEFI variables"); + } } } + None => base_vars, } - None => base_vars, }; let config = firmware_uefi_resources::UefiConfig { diff --git a/openhcl/virt_mshv_vtl/src/lib.rs b/openhcl/virt_mshv_vtl/src/lib.rs index 919e14656b..e4146057e0 100755 --- a/openhcl/virt_mshv_vtl/src/lib.rs +++ b/openhcl/virt_mshv_vtl/src/lib.rs @@ -2043,6 +2043,15 @@ impl<'a> UhProtoPartition<'a> { } impl UhPartition { + /// Whether Secure AVIC is enabled; only ever true on SNP-isolated x86_64. + #[cfg(guest_arch = "x86_64")] + pub fn secure_avic_enabled(&self) -> bool { + if let BackingShared::Snp(snp) = &self.inner.backing_shared { + return snp.secure_avic; + } + false + } + /// Gets the guest OS ID for VTL0. pub fn vtl0_guest_os_id(&self) -> Result { // If Underhill is emulating the hypervisor interfaces, get this value diff --git a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs index 011d2950c4..b02f32a7d3 100755 --- a/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs +++ b/openhcl/virt_mshv_vtl/src/processor/snp/mod.rs @@ -505,7 +505,8 @@ pub struct SnpBackedShared { /// Accessor for managing lower VTL timer deadlines. #[inspect(skip)] guest_timer: hardware_cvm::VmTimeGuestTimer, - secure_avic: bool, + /// Whether Secure AVIC is enabled for VTL2's VMSA. + pub(crate) secure_avic: bool, /// Whether virtual NMI (V_NMI) is supported by the host CPU. pub(crate) vnmi: bool, } diff --git a/vm/loader/src/paravisor.rs b/vm/loader/src/paravisor.rs index 864588af86..5fde2e5bc7 100644 --- a/vm/loader/src/paravisor.rs +++ b/vm/loader/src/paravisor.rs @@ -48,6 +48,7 @@ use page_table::x64::align_up_to_large_page_size; use page_table::x64::align_up_to_page_size; use page_table::x64::calculate_pde_table_count; use product_policy::ProductPolicy; +use product_policy::UefiSecurityPolicy; use product_policy::encode_product_policy; use std::io::Read; use std::io::Seek; @@ -137,6 +138,8 @@ fn validate_product_policy_for_build(policy: &ProductPolicy) { !sivm.custom_uefi_json.is_empty(), "product policy requires non-empty custom_uefi_json" ); + sivm.validate_secure_boot_policy_enforcement() + .expect("product policy validations must pass"); } } ProductPolicy::Cwcow(cwcow) => { @@ -147,6 +150,9 @@ fn validate_product_policy_for_build(policy: &ProductPolicy) { !cwcow.custom_uefi_json.is_empty(), "product policy requires non-empty custom_uefi_json" ); + cwcow + .validate_secure_boot_policy_enforcement() + .expect("product policy validations must pass"); } } }