From a8c01ba89c0887caf3f3083996474639ec94232d Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Sat, 7 Mar 2026 00:05:23 +0000 Subject: [PATCH 01/16] init implementation Signed-off-by: Ming-Wei Shih --- .../src/igvm_attest/get.rs | 8 +- .../src/igvm_attest/mod.rs | 62 ++++++++- openhcl/underhill_attestation/src/lib.rs | 125 +++++++++++++++++- .../src/secure_key_release.rs | 3 + vm/devices/get/get_protocol/src/lib.rs | 1 + vm/devices/get/get_resources/src/lib.rs | 2 + vm/devices/get/test_igvm_agent_lib/src/lib.rs | 71 ++++++++++ .../test_igvm_agent_rpc_server/src/main.rs | 5 + 8 files changed, 265 insertions(+), 12 deletions(-) diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 09aff9b890..7d65d65970 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -167,12 +167,14 @@ impl IgvmAttestRequestHeader { /// Bitmap of additional Igvm request attributes. /// 0 - error_code: Requesting IGVM Agent Error code /// 1 - retry: Retry preference +/// 2 - skip_hw_unsealing: Skip hardware unsealing in case key release request fails #[bitfield(u32)] #[derive(IntoBytes, FromBytes, Immutable, KnownLayout)] pub struct IgvmCapabilityBitMap { pub error_code: bool, pub retry: bool, - #[bits(30)] + pub skip_hw_unsealing: bool, + #[bits(29)] _reserved: u32, } @@ -229,11 +231,13 @@ impl IgvmAttestRequestDataExt { /// Bitmap indicates a signal to requestor /// 0 - IGVM_SIGNAL_RETRY_RCOMMENDED_BIT: Retry recommendation +/// 1 - IGVM_SIGNAL_SKIP_HW_UNSEALING_RECOMMENDED_BIT: Skip hardware unsealing #[bitfield(u32)] #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] pub struct IgvmSignal { pub retry: bool, - #[bits(31)] + pub skip_hw_unsealing: bool, + #[bits(30)] _reserved: u32, } diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index 5ccc6ac1e4..132a84b53f 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -54,12 +54,13 @@ pub enum Error { latest_version: IgvmAttestResponseVersion, }, #[error( - "attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal})" + "attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal}), skip hw unsealing ({skip_hw_unsealing})" )] Attestation { igvm_error_code: u32, http_status_code: u32, retry_signal: bool, + skip_hw_unsealing: bool, }, } @@ -247,6 +248,7 @@ pub fn parse_response_header(response: &[u8]) -> Result false, - Err((_, r)) => r, + Err((_, r)) => *r, }; + let skip_hw_unsealing = matches!( + &skr_response, + Err(( + secure_key_release::RequestVmgsEncryptionKeysError::ParseIgvmAttestKeyReleaseResponse( + igvm_attest::key_release::KeyReleaseError::ParseHeader( + igvm_attest::Error::Attestation { + skip_hw_unsealing: true, + .. + }, + ), + ), + _, + )) + ); + let VmgsEncryptionKeys { ingress_rsa_kek, wrapped_des_key, @@ -342,6 +359,7 @@ async fn try_unlock_vmgs( tcb_version, guest_state_encryption_policy, strict_encryption_policy, + skip_hw_unsealing, ) .await .map_err(|e| { @@ -683,6 +701,7 @@ async fn get_derived_keys( tcb_version: Option, guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, + skip_hw_unsealing: bool, ) -> Result { tracing::info!( CVM_ALLOWED, @@ -866,7 +885,17 @@ async fn get_derived_keys( // If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key if (no_kek && found_dek) || (no_gsp && requires_gsp) || (no_gsp_by_id && requires_gsp_by_id) { // If possible, get ingressKey from hardware sealed data - let (hardware_key_protector, hardware_derived_keys) = if let Some(tee_call) = tee_call { + let (hardware_key_protector, hardware_derived_keys) = if skip_hw_unsealing { + tracing::warn!( + CVM_ALLOWED, + "Skipping hardware unsealing of VMGS DEK as signaled by IGVM agent" + ); + get.event_log_fatal( + guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED, + ); + + return Err(GetDerivedKeysError::HardwareUnsealingSkipped); + } else if let Some(tee_call) = tee_call { let hardware_key_protector = match vmgs::read_hardware_key_protector(vmgs).await { Ok(hardware_key_protector) => Some(hardware_key_protector), Err(e) => { @@ -932,12 +961,12 @@ async fn get_derived_keys( }); } else { if no_kek && found_dek { - Err(GetDerivedKeysError::GetIngressKeyFromKpFailed)? + return Err(GetDerivedKeysError::GetIngressKeyFromKpFailed); } else if no_gsp && requires_gsp { - Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed)? + return Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed); } else { // no_gsp_by_id && requires_gsp_by_id - Err(GetDerivedKeysError::GetIngressKeyFromKGspByIdFailed)? + return Err(GetDerivedKeysError::GetIngressKeyFromKGspByIdFailed); } } } @@ -2563,6 +2592,90 @@ mod tests { assert!(vmgs.encrypted()); } + #[async_test] + async fn init_sec_secure_key_release_skip_hw_unsealing(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // IGVM attest is required + // KEY_RELEASE succeeds on first boot, fails with skip_hw_unsealing on second boot. + // WRAPPED_KEY is not in the plan, so it falls back to default (success) every time. + let mut plan = IgvmAgentTestPlan::default(); + plan.insert( + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondFailureSkipHwUnsealing, + ]), + ); + + let get_pair = new_test_get(driver, true, Some(plan)).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let tee = MockTeeCall::new(0x1234); + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted and HWKP is updated. + assert!(vmgs.encrypted()); + assert!(!hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. + let key_reference = serde_json::json!({ + "key_info": { + "host": "name" + }, + "attestation_info": { + "host": "attestation_name" + } + }); + let key_reference = serde_json::to_string(&key_reference).unwrap(); + let key_reference = key_reference.as_bytes(); + let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; + expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); + assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: KEY_RELEASE fails with skip_hw_unsealing signal. + // The skip_hw_unsealing signal causes the hardware unsealing fallback to be + // skipped, so VMGS unlock should fail. + // NOTE: The test relies on the test GED to return failing KEY_RELEASE response + // with retry recommendation as false so the retry loop terminates immediately. + // Otherwise, the test will get stuck on timer.sleep() as the driver is not + // progressed. + let result = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await; + + assert!(result.is_err()); + } + #[async_test] async fn init_sec_secure_key_release_no_hw_sealing_backup(driver: DefaultDriver) { let mut vmgs = new_formatted_vmgs().await; diff --git a/openhcl/underhill_attestation/src/secure_key_release.rs b/openhcl/underhill_attestation/src/secure_key_release.rs index e3dcac2b24..df8618b71f 100644 --- a/openhcl/underhill_attestation/src/secure_key_release.rs +++ b/openhcl/underhill_attestation/src/secure_key_release.rs @@ -155,6 +155,7 @@ pub async fn request_vmgs_encryption_keys( igvm_error_code, http_status_code, retry_signal, + .. }, ), ), @@ -176,6 +177,7 @@ pub async fn request_vmgs_encryption_keys( igvm_error_code, http_status_code, retry_signal, + skip_hw_unsealing, }, ), ), @@ -185,6 +187,7 @@ pub async fn request_vmgs_encryption_keys( igvm_error_code = &igvm_error_code, igvm_http_status_code = &http_status_code, retry_signal = &retry_signal, + skip_hw_unsealing = &skip_hw_unsealing, error = &key_release_attest_error as &dyn std::error::Error, "VMGS key-encryption failed due to igvm attest error" ); diff --git a/vm/devices/get/get_protocol/src/lib.rs b/vm/devices/get/get_protocol/src/lib.rs index 89e5c0d3cf..86f7dcd267 100644 --- a/vm/devices/get/get_protocol/src/lib.rs +++ b/vm/devices/get/get_protocol/src/lib.rs @@ -350,6 +350,7 @@ open_enum! { TPM_INVALID_STATE = 17, TPM_IDENTITY_CHANGE_FAILED = 18, WRAPPED_KEY_REQUIRED_BUT_INVALID = 19, + DEK_HARDWARE_UNSEALING_SKIPPED = 23, } } diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index 1c66d12b3a..588da4b69f 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -252,5 +252,7 @@ pub mod ged { AkCertRequestFailureAndRetry, /// Config for testing AK cert persistency across boots. AkCertPersistentAcrossBoot, + /// Config for testing skip hardware unsealing signal from IGVMAgent. + KeyReleaseFailureSkipHwUnsealing, } } diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 4b45e6d546..3f0dd09b53 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -115,6 +115,8 @@ pub enum IgvmAgentAction { RespondSuccess, /// Emit a response that indicates a protocol error. RespondFailure, + /// Emit a response that indicates a protocol error with skip_hw_unsealing signal. + RespondFailureSkipHwUnsealing, /// Skip responding to simulate a timeout. NoResponse, } @@ -166,6 +168,15 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan VecDeque::from([IgvmAgentAction::RespondSuccess, IgvmAgentAction::NoResponse]), ); } + IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { + plan.insert( + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondFailureSkipHwUnsealing, + ]), + ); + } } plan @@ -366,6 +377,66 @@ impl TestIgvmAgent { ty => return Err(Error::UnsupportedIgvmAttestRequestType(ty.0)), } } + IgvmAgentAction::RespondFailureSkipHwUnsealing => { + tracing::info!(?request.header.request_type, "Test plan: RespondFailureSkipHwUnsealing"); + match request.header.request_type { + IgvmAttestRequestType::WRAPPED_KEY_REQUEST => { + let header = IgvmAttestWrappedKeyResponseHeader { + data_size: size_of::() as u32, + version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION, + error_info: IgvmErrorInfo { + error_code: 0x5678, + http_status_code: 400, + igvm_signal: IgvmSignal::default() + .with_retry(false) + .with_skip_hw_unsealing(true), + reserved: [0; 3], + }, + }; + let payload = header.as_bytes().to_vec(); + let payload_len = payload.len() as u32; + + (payload, payload_len) + } + IgvmAttestRequestType::KEY_RELEASE_REQUEST => { + let header = IgvmAttestKeyReleaseResponseHeader { + data_size: size_of::() as u32, + version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION, + error_info: IgvmErrorInfo { + error_code: 0x5678, + http_status_code: 400, + igvm_signal: IgvmSignal::default() + .with_retry(false) + .with_skip_hw_unsealing(true), + reserved: [0; 3], + }, + }; + let payload = header.as_bytes().to_vec(); + let payload_len = payload.len() as u32; + + (payload, payload_len) + } + IgvmAttestRequestType::AK_CERT_REQUEST => { + let header = IgvmAttestAkCertResponseHeader { + data_size: size_of::() as u32, + version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION, + error_info: IgvmErrorInfo { + error_code: 0x5678, + http_status_code: 400, + igvm_signal: IgvmSignal::default() + .with_retry(false) + .with_skip_hw_unsealing(true), + reserved: [0; 3], + }, + }; + let payload = header.as_bytes().to_vec(); + let payload_len = payload.len() as u32; + + (payload, payload_len) + } + ty => return Err(Error::UnsupportedIgvmAttestRequestType(ty.0)), + } + } } } else { // If no plan is provided, fall back to the default behavior that diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index f4f98cf153..03d15f4a17 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -28,6 +28,8 @@ enum TestConfig { AkCertRequestFailureAndRetry, /// Test AK cert persistency across boots AkCertPersistentAcrossBoot, + /// Test skip hardware unsealing signal from IGVM Agent + KeyReleaseFailureSkipHwUnsealing, } impl From for IgvmAttestTestConfig { @@ -39,6 +41,9 @@ impl From for IgvmAttestTestConfig { TestConfig::AkCertPersistentAcrossBoot => { IgvmAttestTestConfig::AkCertPersistentAcrossBoot } + TestConfig::KeyReleaseFailureSkipHwUnsealing => { + IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing + } } } } From 2520c65e41484366f0e61545ddd3aa904a2b0d35 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Sat, 7 Mar 2026 05:39:08 +0000 Subject: [PATCH 02/16] vmm tests Signed-off-by: Ming-Wei Shih --- .../test_igvm_agent_rpc_server/src/main.rs | 4 +- .../src/rpc/handlers.rs | 3 +- .../src/rpc/igvm_agent.rs | 162 ++++++++++++++++-- .../vmm_tests/tests/tests/multiarch/tpm.rs | 84 ++++++++- 4 files changed, 235 insertions(+), 18 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index 03d15f4a17..f951f826ac 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -71,8 +71,8 @@ fn main() -> ExitCode { if let Some(test_config) = args.test_config { let igvm_config: IgvmAttestTestConfig = test_config.into(); let setting = IgvmAgentTestSetting::TestConfig(igvm_config); - rpc::igvm_agent::install_plan(&setting); - tracing::info!(?test_config, "installed test configuration"); + rpc::igvm_agent::install_default_plan(&setting); + tracing::info!(?test_config, "installed default test configuration"); } else { tracing::info!("no test configuration provided, using default behavior"); } diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/handlers.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/handlers.rs index cae47b764f..c4feaa1ca2 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/handlers.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/handlers.rs @@ -218,7 +218,8 @@ pub extern "system" fn rpc_igvm_attest( "invoking attest igvm_agent" ); - let payload = match igvm_agent::process_igvm_attest(report_slice) { + let vm_name_ref = vm_name_str.as_deref().unwrap_or(""); + let payload = match igvm_agent::process_igvm_attest(vm_name_ref, report_slice) { Ok(payload) => payload, Err(err) => { tracing::error!(?err, "igvm_agent::process_igvm_attest failed"); diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index 0c7ad429c5..bab479bad2 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -1,9 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Shared façade that exposes the test IGVM agent through a singleton instance. +//! Per-VM test IGVM agent façade. +//! +//! Each VM gets its own [`TestIgvmAgent`] keyed by VM name. The test plan +//! for a given VM is resolved by matching its name against a hardcoded +//! mapping (see [`config_for_vm_name`]). A default plan can also be +//! installed via the CLI `--test_config` flag; it applies to VMs whose +//! names do not match any known pattern. +//! +//! **Naming convention** – Hyper-V VM names are capped at 100 characters +//! (see `petri::vm::make_vm_safe_name`). The prefix added by the test +//! macro can consume ~85 characters on the worst-case image name, leaving +//! only ~15 characters for the test function name. Keep test function +//! names short (≤ 15 chars) so the distinctive part is never truncated. +use get_resources::ged::IgvmAttestTestConfig; use parking_lot::Mutex; +use std::collections::HashMap; use std::sync::OnceLock; use test_igvm_agent_lib::Error; use test_igvm_agent_lib::IgvmAgentTestSetting; @@ -21,25 +35,93 @@ pub enum TestAgentFacadeError { /// Convenience result type for façade invocations. pub type TestAgentResult = Result; -static TEST_AGENT: OnceLock> = OnceLock::new(); +/// Per-VM agent registry. +struct AgentRegistry { + /// Default setting (from CLI `--test_config`), applied to VMs that + /// don't match any hardcoded pattern. + default_setting: Option, + /// Live per-VM agents, lazily created on first request. + agents: HashMap, +} + +static REGISTRY: OnceLock> = OnceLock::new(); -fn global_agent() -> &'static Mutex { - TEST_AGENT.get_or_init(|| Mutex::new(TestIgvmAgent::new())) +fn registry() -> &'static Mutex { + REGISTRY.get_or_init(|| { + Mutex::new(AgentRegistry { + default_setting: None, + agents: HashMap::new(), + }) + }) } -fn guard_agent() -> parking_lot::MutexGuard<'static, TestIgvmAgent> { - global_agent().lock() +/// Hardcoded mapping from VM name substrings to test configurations. +/// +/// The substring must be short enough to survive Hyper-V's 100-char name +/// limit even on the longest image prefixes (~85 chars). Keep patterns +/// ≤ 15 characters. +/// +/// Hyper-V VM names are built as: +/// `{module}::{vmm}_{firmware}_{arch}_{image}_{isolation}_{test_fn}` +/// We match by `contains` on the test function name portion. +fn config_for_vm_name(vm_name: &str) -> Option { + /// (substring, config) pairs – order does not matter since each + /// pattern is unique. + const KNOWN_TEST_CONFIGS: &[(&str, IgvmAttestTestConfig)] = &[ + ( + "ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "akcert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "skip_hw_unseal", + IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, + ), + ]; + + for &(pattern, config) in KNOWN_TEST_CONFIGS { + if vm_name.contains(pattern) { + tracing::info!(vm_name, pattern, "matched test config for VM"); + return Some(IgvmAgentTestSetting::TestConfig(config)); + } + } + + None } -/// Install a scripted test plan for the shared test agent instance. -pub fn install_plan(setting: &IgvmAgentTestSetting) { - let mut agent = guard_agent(); - agent.install_plan_from_setting(setting); +/// Install a default test plan used as a fallback for VMs that don't +/// match any hardcoded pattern. +pub fn install_default_plan(setting: &IgvmAgentTestSetting) { + let mut reg = registry().lock(); + reg.default_setting = Some(setting.clone()); } -/// Process an attestation request payload using the shared test agent. -pub fn process_igvm_attest(report: &[u8]) -> TestAgentResult> { - let mut agent = guard_agent(); +/// Process an attestation request payload for the given VM. +/// +/// On first contact the VM's agent is created and configured: +/// 1. If the VM name matches a hardcoded pattern, that config is used. +/// 2. Otherwise the default plan (if any) is installed. +pub fn process_igvm_attest(vm_name: &str, report: &[u8]) -> TestAgentResult> { + let mut reg = registry().lock(); + + // Clone the default setting before entering the entry API so the + // borrow checker is happy. + let default_setting = reg.default_setting.clone(); + + let agent = reg.agents.entry(vm_name.to_owned()).or_insert_with(|| { + let mut agent = TestIgvmAgent::new(); + if let Some(setting) = config_for_vm_name(vm_name) { + agent.install_plan_from_setting(&setting); + } else if let Some(ref default) = default_setting { + agent.install_plan_from_setting(default); + } + tracing::info!(vm_name, "created per-VM test agent"); + agent + }); + let (payload, expected_len) = agent.handle_request(report).map_err(|err| match err { Error::InvalidIgvmAttestRequest => TestAgentFacadeError::InvalidRequest, _ => TestAgentFacadeError::AgentFailure, @@ -49,3 +131,57 @@ pub fn process_igvm_attest(report: &[u8]) -> TestAgentResult> { } Ok(payload) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn match_akcert_retry() { + let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_akcert_retry"; + assert!(vm_name.len() <= 100, "name must fit in 100 chars"); + match config_for_vm_name(vm_name) { + Some(IgvmAgentTestSetting::TestConfig(c)) => assert!(matches!( + c, + IgvmAttestTestConfig::AkCertRequestFailureAndRetry + )), + other => panic!("expected AkCertRequestFailureAndRetry, got {:?}", other), + } + } + + #[test] + fn match_akcert_cache() { + let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_akcert_cache"; + assert!(vm_name.len() <= 100, "name must fit in 100 chars"); + match config_for_vm_name(vm_name) { + Some(IgvmAgentTestSetting::TestConfig(c)) => assert!(matches!( + c, + IgvmAttestTestConfig::AkCertPersistentAcrossBoot + )), + other => panic!("expected AkCertPersistentAcrossBoot, got {:?}", other), + } + } + + #[test] + fn match_skip_hwunseal() { + let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_skip_hwunseal"; + assert!(vm_name.len() <= 100, "name must fit in 100 chars"); + match config_for_vm_name(vm_name) { + Some(IgvmAgentTestSetting::TestConfig(c)) => assert!(matches!( + c, + IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing + )), + other => panic!("expected KeyReleaseFailureSkipHwUnsealing, got {:?}", other), + } + } + + #[test] + fn no_match_unknown_vm() { + assert!(config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test").is_none()); + } + + #[test] + fn no_match_empty() { + assert!(config_for_vm_name("").is_none()); + } +} \ No newline at end of file diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 56a779ced0..cdfb28bbbd 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -4,6 +4,7 @@ use anyhow::Context; use anyhow::ensure; use petri::PetriGuestStateLifetime; +use petri::PetriHaltReason; use petri::PetriVmBuilder; use petri::PetriVmmBackend; use petri::ResolvedArtifact; @@ -231,7 +232,7 @@ async fn boot_with_tpm(config: PetriVmBuilder) -> anyhow: openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64], openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64] )] -async fn tpm_ak_cert_persisted( +async fn ak_cert_cache( config: PetriVmBuilder, extra_deps: (ResolvedArtifact,), ) -> anyhow::Result<()> { @@ -293,7 +294,7 @@ async fn tpm_ak_cert_persisted( openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64], openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64] )] -async fn tpm_ak_cert_retry( +async fn ak_cert_retry( config: PetriVmBuilder, extra_deps: (ResolvedArtifact,), ) -> anyhow::Result<()> { @@ -547,6 +548,85 @@ async fn cvm_tpm_guest_tests( Ok(()) } +/// Test that skip_hw_unsealing signal from IGVM agent causes VMGS +/// unlock to fail on second boot. +/// +/// First boot: KEY_RELEASE succeeds, VMGS is encrypted with hardware +/// key protector, TPM state is sealed. +/// Second boot: KEY_RELEASE fails with skip_hw_unsealing signal, hardware +/// unsealing fallback is skipped, VMGS cannot be unlocked. +/// `initialize_platform_security` returns an error, underhill reports the +/// failure to the host via `complete_start_vtl0`, and the host terminates +/// the VM. +/// +/// The test function name contains `skip_hwunseal` so the per-VM agent +/// registry in test_igvm_agent_rpc_server matches it to the +/// `KeyReleaseFailureSkipHwUnsealing` configuration. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn skip_hwun_seal( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(true) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // First boot: KEY_RELEASE succeeds. Verify AK cert is present. + let expected_hex = expected_ak_cert_hex(); + let ak_cert_output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + ak_cert_output.contains("AK certificate matches expected value"), + format!("{ak_cert_output}") + ); + + // Reboot: triggers second KEY_RELEASE which fails with skip_hw_unsealing. + // VMGS unlock will fail because hardware unsealing fallback is skipped. + // initialize_platform_security returns an error, underhill reports the + // failure to the host via complete_start_vtl0, and the host terminates + // the VM. + agent.reboot().await?; + + // Phase 1: Consume the Reset halt event. For isolated CVMs, this also + // confirms the VM reaches Running state after the reset. + let halt_reason = vm.wait_for_halt().await?; + assert_eq!( + halt_reason, + PetriHaltReason::Reset, + "Expected reset from reboot" + ); + + // Phase 2: Wait for the VM to be terminated by the host after underhill + // fails to start on the second boot. + let halt_reason = vm.wait_for_teardown().await?; + tracing::info!("Second boot halt reason: {halt_reason:?}"); + + Ok(()) +} + /// Test that TPM NVRAM size persists across servicing. #[vmm_test( openvmm_openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[LATEST_STANDARD_X64, VMGS_WITH_16K_TPM], From 7bdcc4db0fd601fe82005eabdb5d3ae9b790bcfd Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 05:12:12 +0000 Subject: [PATCH 03/16] support statless + hw sealing Signed-off-by: Ming-Wei Shih --- .../src/igvm_attest/get.rs | 13 + .../openhcl_attestation_protocol/src/vmgs.rs | 42 +- openhcl/tee_call/Cargo.toml | 1 + openhcl/tee_call/src/lib.rs | 34 +- .../src/hardware_key_sealing.rs | 347 ++++++++- .../src/igvm_attest/mod.rs | 7 +- openhcl/underhill_attestation/src/lib.rs | 669 ++++++++++++++---- openhcl/underhill_attestation/src/vmgs.rs | 8 +- openhcl/underhill_core/src/worker.rs | 44 +- petri/src/vm/hyperv/hyperv.psm1 | 15 + petri/src/vm/hyperv/mod.rs | 15 + petri/src/vm/hyperv/powershell.rs | 56 ++ petri/src/vm/hyperv/vm.rs | 8 + petri/src/vm/mod.rs | 28 + petri/src/vm/openvmm/construct.rs | 1 + vm/devices/get/get_protocol/src/dps_json.rs | 28 +- vm/devices/get/get_protocol/src/lib.rs | 2 + vm/devices/get/get_resources/src/lib.rs | 3 + .../get/guest_emulation_device/src/lib.rs | 5 + .../guest_emulation_device/src/resolver.rs | 1 + .../src/test_utilities.rs | 1 + .../get/guest_emulation_transport/src/api.rs | 3 + .../guest_emulation_transport/src/client.rs | 1 + vm/devices/get/test_igvm_agent_lib/src/lib.rs | 9 + .../test_igvm_agent_rpc_server/src/main.rs | 3 + .../src/rpc/igvm_agent.rs | 20 +- vm/devices/tpm/tpm_guest_tests/src/main.rs | 169 +++++ .../vmm_tests/tests/tests/multiarch/tpm.rs | 357 ++++++++++ 28 files changed, 1706 insertions(+), 184 deletions(-) diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 7d65d65970..73cc486963 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -422,6 +422,17 @@ pub mod runtime_claims { } } + /// Supported hardware sealing policy + #[derive(Clone, Copy, Debug, Deserialize, Serialize, MeshPayload)] + pub enum HardwareSealingPolicy { + #[serde(rename = "none")] + None, + #[serde(rename = "hash")] + Hash, + #[serde(rename = "signer")] + Signer, + } + /// VM configuration to be included in the `RuntimeClaims`. #[derive(Clone, Debug, Deserialize, Serialize, MeshPayload)] #[serde(rename_all = "kebab-case")] @@ -441,6 +452,8 @@ pub mod runtime_claims { pub tpm_enabled: bool, /// Whether the TPM states is persisted pub tpm_persisted: bool, + /// Hardware sealing policy + pub hardware_sealing_policy: HardwareSealingPolicy, /// Whether certain vPCI devices are allowed through the device filter pub filtered_vpci_devices_allowed: bool, /// VM id diff --git a/openhcl/openhcl_attestation_protocol/src/vmgs.rs b/openhcl/openhcl_attestation_protocol/src/vmgs.rs index 0a72629705..af58e93eff 100644 --- a/openhcl/openhcl_attestation_protocol/src/vmgs.rs +++ b/openhcl/openhcl_attestation_protocol/src/vmgs.rs @@ -74,11 +74,14 @@ pub struct SecurityProfile { pub agent_data: [u8; AGENT_DATA_MAX_SIZE], } -/// The header, IV, and last 256 bits of HMAC are fixed for this version. -/// The ciphertext is allowed to grow, though secrets should stay -/// in the same position to allow downlevel versions to continue to understand -/// that portion of the data. -pub const HW_KEY_VERSION: u32 = 1; // using AES-CBC-HMAC-SHA256 +/// VMGS hardware key protector entry that includes the metadata of +/// local hardware sealing with AES-CBC-HMAC-SHA256. +/// +/// Version 1 is incompatible with newer versions. +/// Version 2 or newer is forward-compatible if header.mix_measurement is not set. +pub const HW_KEY_PROTECTOR_VERSION_1: u32 = 1; +pub const HW_KEY_PROTECTOR_VERSION_2: u32 = 2; +pub const HW_KEY_PROTECTOR_CURRENT_VERSION: u32 = HW_KEY_PROTECTOR_VERSION_2; /// The size of the `FileId::HW_KEY_PROTECTOR` entry in the VMGS file. pub const HW_KEY_PROTECTOR_SIZE: usize = size_of::(); @@ -105,18 +108,22 @@ pub struct HardwareKeyProtectorHeader { pub length: u32, /// TCB version obtained from the hardware pub tcb_version: u64, - /// Reserved - pub _reserved: [u8; 8], + /// Whether to mix the measurement in hardware key derivation + /// Only supported in version 2 and above + pub mix_measurement: u8, + /// Reserved bytes for future use + pub _reserved: [u8; 7], } impl HardwareKeyProtectorHeader { /// Create a `HardwareKeyProtectorHeader` instance. - pub fn new(version: u32, length: u32, tcb_version: u64) -> Self { + pub fn new(version: u32, length: u32, tcb_version: u64, mix_measurement: u8) -> Self { Self { version, length, tcb_version, - _reserved: [0u8; 8], + mix_measurement, + _reserved: [0; 7], } } } @@ -131,7 +138,7 @@ pub struct HardwareKeyProtector { pub iv: [u8; AES_CBC_IV_LENGTH], /// Encrypted key pub ciphertext: [u8; AES_GCM_KEY_LENGTH], - /// HMAC-SHA-256 of [`header`, `iv`, `ciphertext`] + /// HMAC-SHA-256 of [header, iv, ciphertext] pub hmac: [u8; HMAC_SHA_256_KEY_LENGTH], } @@ -145,3 +152,18 @@ pub struct GuestSecretKey { /// the guest secret key to be provisioned to vTPM pub guest_secret_key: [u8; GUEST_SECRET_KEY_MAX_SIZE], } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hardware_key_protector_header_new() { + let h = HardwareKeyProtectorHeader::new(2, 104, 0x1234, 1); + assert_eq!(h.version, 2); + assert_eq!(h.length, 104); + assert_eq!(h.tcb_version, 0x1234); + assert_eq!(h.mix_measurement, 1); + assert_eq!(h._reserved, [0; 7]); + } +} diff --git a/openhcl/tee_call/Cargo.toml b/openhcl/tee_call/Cargo.toml index b406d6db30..f62dff480f 100644 --- a/openhcl/tee_call/Cargo.toml +++ b/openhcl/tee_call/Cargo.toml @@ -20,5 +20,6 @@ x86defs.workspace = true static_assertions.workspace = true thiserror.workspace = true zerocopy.workspace = true + [lints] workspace = true diff --git a/openhcl/tee_call/src/lib.rs b/openhcl/tee_call/src/lib.rs index 61a272fa95..00a1115e3f 100644 --- a/openhcl/tee_call/src/lib.rs +++ b/openhcl/tee_call/src/lib.rs @@ -65,10 +65,23 @@ pub struct GetAttestationReportResult { pub tcb_version: Option, } +/// Key derivation policy +#[derive(Debug, Clone, Copy)] +pub struct KeyDerivationPolicy { + /// The TCB version to use for key derivation. + pub tcb_version: u64, + /// Whether to mix measurement into the key derivation. + pub mix_measurement: bool, +} + /// Trait that defines the get attestation report interface for TEE. -// TODO VBS: Implement the trait for VBS pub trait TeeCall: Send + Sync { /// Get the hardware-backed attestation report. + /// + /// # Arguments + /// * `report_data` - The report data to include in the attestation report. + /// + /// Returns the attestation report result. fn get_attestation_report( &self, report_data: &[u8; REPORT_DATA_SIZE], @@ -80,10 +93,18 @@ pub trait TeeCall: Send + Sync { } /// Optional sub-trait that defines get derived key interface for TEE. +/// +/// # Arguments +/// * `policy` - The key derivation policy to use. +/// +/// Returns the derived key. pub trait TeeCallGetDerivedKey: TeeCall { /// Get the derived key that should be deterministic based on the hardware and software /// configurations. - fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error>; + fn get_derived_key( + &self, + policy: KeyDerivationPolicy, + ) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error>; } /// Implementation of [`TeeCall`] for SNP @@ -119,7 +140,10 @@ impl TeeCall for SnpCall { impl TeeCallGetDerivedKey for SnpCall { /// Get the derived key from /dev/sev-guest. - fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error> { + fn get_derived_key( + &self, + policy: KeyDerivationPolicy, + ) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error> { let dev = sev_guest_device::SevGuestDevice::open().map_err(Error::OpenDevSevGuest)?; // Derive a key mixing in following data: @@ -128,7 +152,7 @@ impl TeeCallGetDerivedKey for SnpCall { // - TcbVersion (do not derive same key on older TCB that might have a bug) let guest_field_select = x86defs::snp::GuestFieldSelect::default() .with_guest_policy(true) - .with_measurement(true) + .with_measurement(policy.mix_measurement) .with_tcb_version(true); let derived_key = dev @@ -137,7 +161,7 @@ impl TeeCallGetDerivedKey for SnpCall { guest_field_select.into(), 0, // VMPL 0 0, // default guest svn to 0 - tcb_version, + policy.tcb_version, ) .map_err(Error::GetSnpDerivedKey)?; diff --git a/openhcl/underhill_attestation/src/hardware_key_sealing.rs b/openhcl/underhill_attestation/src/hardware_key_sealing.rs index 94937fe2c5..f013165821 100644 --- a/openhcl/underhill_attestation/src/hardware_key_sealing.rs +++ b/openhcl/underhill_attestation/src/hardware_key_sealing.rs @@ -16,6 +16,8 @@ use zerocopy::IntoBytes; #[derive(Debug, Error)] pub(crate) enum HardwareDerivedKeysError { + #[error("key derivation policy does not match VM configuration")] + KeyDerivationPolicyMismatch, #[error("failed to initialize hardware secret")] InitializeHardwareSecret(#[source] tee_call::Error), #[error("KDF derivation with hardware secret failed")] @@ -41,32 +43,47 @@ pub(crate) enum HardwareKeySealingError { } /// Hold the hardware-derived keys. +#[derive(Debug)] pub struct HardwareDerivedKeys { - tcb_version: u64, + policy: tee_call::KeyDerivationPolicy, aes_key: [u8; vmgs::AES_CBC_KEY_LENGTH], hmac_key: [u8; vmgs::HMAC_SHA_256_KEY_LENGTH], } impl HardwareDerivedKeys { - /// Derive an AES and HMAC keys based on the hardware secret for key sealing. + /// Derive an AES and HMAC keys based on the hardware secret, VM configuration, and policy for key sealing. pub fn derive_key( tee_call: &dyn tee_call::TeeCallGetDerivedKey, vm_config: &igvm_attest::get::runtime_claims::AttestationVmConfig, - tcb_version: u64, + policy: tee_call::KeyDerivationPolicy, ) -> Result { + let mix_measurement_from_vm_config = matches!( + vm_config.hardware_sealing_policy, + igvm_attest::get::runtime_claims::HardwareSealingPolicy::Hash + ); + + // Policy is based on the VM configuration (`hardware_sealing_policy`) on the + // sealing path and on VMGS file (`HardwareKeyProtector`) on the unsealing path. + // On both paths, the policy must be consistent with the VM configuration. + // An inconsistency will cause mismatch in the key derivation function that takes + // VM configuration as input. + if policy.mix_measurement != mix_measurement_from_vm_config { + return Err(HardwareDerivedKeysError::KeyDerivationPolicyMismatch); + } + let hardware_secret = tee_call - .get_derived_key(tcb_version) + .get_derived_key(policy) .map_err(HardwareDerivedKeysError::InitializeHardwareSecret)?; let label = b"ISOHWKEY"; - let vm_config = serde_json::to_string(vm_config).expect("JSON serialization failed"); + let vm_config_json = serde_json::to_string(vm_config).expect("JSON serialization failed"); let mut kdf = Kbkdf::new( openssl::hash::MessageDigest::sha256(), label.to_vec(), hardware_secret.to_vec(), ); - kdf.set_context(vm_config.as_bytes().to_vec()); + kdf.set_context(vm_config_json.as_bytes().to_vec()); let mut output = [0u8; vmgs::AES_CBC_KEY_LENGTH + vmgs::HMAC_SHA_256_KEY_LENGTH]; openssl_kdf::kdf::derive(kdf, &mut output) @@ -79,7 +96,7 @@ impl HardwareDerivedKeys { hmac_key.copy_from_slice(&output[vmgs::AES_CBC_KEY_LENGTH..]); Ok(Self { - tcb_version, + policy, aes_key, hmac_key, }) @@ -107,9 +124,10 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { egress_key: &[u8], ) -> Result { let header = vmgs::HardwareKeyProtectorHeader::new( - vmgs::HW_KEY_VERSION, + vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION, vmgs::HW_KEY_PROTECTOR_SIZE as u32, - hardware_derived_keys.tcb_version, + hardware_derived_keys.policy.tcb_version, + hardware_derived_keys.policy.mix_measurement as u8, ); let mut iv = [0u8; vmgs::AES_CBC_IV_LENGTH]; @@ -182,17 +200,16 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { mod tests { use super::*; use crate::test_utils::MockTeeCall; + use igvm_attest::get::runtime_claims::AttestationVmConfig; + use igvm_attest::get::runtime_claims::HardwareSealingPolicy; use zerocopy::FromBytes; - #[test] - fn hardware_derived_keys() { - const PLAINTEXT: [u8; 32] = [ - 0x5e, 0xd7, 0xf3, 0xd4, 0x9e, 0xcf, 0xb5, 0x6c, 0x05, 0x54, 0x7c, 0x87, 0xe7, 0x30, - 0x59, 0xb1, 0x91, 0xcb, 0xa6, 0xc4, 0x0e, 0x4e, 0x30, 0x77, 0x65, 0x19, 0x71, 0xf5, - 0x20, 0x83, 0x2a, 0xc0, - ]; - - let vm_config = igvm_attest::get::runtime_claims::AttestationVmConfig { + const PLAINTEXT: [u8; 32] = [0xAB; 32]; + + fn create_test_vm_config( + hardware_sealing_policy: HardwareSealingPolicy, + ) -> AttestationVmConfig { + AttestationVmConfig { current_time: None, root_cert_thumbprint: "".to_string(), console_enabled: false, @@ -200,30 +217,292 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, + hardware_sealing_policy, filtered_vpci_devices_allowed: true, vm_unique_id: "".to_string(), + } + } + + #[test] + fn hardware_derived_keys_hash_policy() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let hardware_derived_keys = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: true, + }, + ) + .unwrap(); + + let output = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT).unwrap(); + let hardware_key_protector = HardwareKeyProtector::read_from_prefix(output.as_bytes()) + .unwrap() + .0; + let plaintext = hardware_key_protector + .unseal_key(&hardware_derived_keys) + .unwrap(); + assert_eq!(plaintext, PLAINTEXT); + } + + #[test] + fn hardware_derived_keys_signer_policy() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let k1 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: false, + }, + ) + .unwrap(); + let output = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); + let hardware_key_protector = HardwareKeyProtector::read_from_prefix(output.as_bytes()) + .unwrap() + .0; + + // Unseal should succeed with different measurements when using signer policy + let mock_tee_call = Box::new(MockTeeCall::new([0x8bu8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let k2 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: false, + }, + ) + .unwrap(); + let plaintext = hardware_key_protector.unseal_key(&k2).unwrap(); + assert_eq!(plaintext, PLAINTEXT); + } + + #[test] + fn hardware_derived_keys_policy_mismatch() { + { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = + Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + + let result = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: false, + }, + ); + assert!(result.is_err()); + let err = result.unwrap_err(); + matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); + } + + { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); + let mock_tee_call = + Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + + let result = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: true, + }, + ); + assert!(result.is_err()); + let err = result.unwrap_err(); + matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); + } + } + + #[test] + fn hardware_key_protector_header_fields_set() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let policy = tee_call::KeyDerivationPolicy { + tcb_version: 0xDEAD_BEEF, + mix_measurement: false, }; - let mock_call = Box::new(MockTeeCall::new(0x1234)) as Box; - let mock_get_derived_key_call = mock_call.supports_get_derived_key().unwrap(); - let result = HardwareDerivedKeys::derive_key( + let k = + HardwareDerivedKeys::derive_key(mock_get_derived_key_call, &vm_config, policy).unwrap(); + let hwkp = HardwareKeyProtector::seal_key(&k, &PLAINTEXT).unwrap(); + + assert_eq!(hwkp.header.tcb_version, policy.tcb_version); + assert_eq!(hwkp.header.mix_measurement, policy.mix_measurement as u8); + assert_eq!(hwkp.header.length as usize, vmgs::HW_KEY_PROTECTOR_SIZE); + assert_eq!(hwkp.header.version, vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION); + } + + #[test] + fn unseal_key_fails_when_original_plaintext_not_32() { + // With CBC and no padding enabled, sealing must fail for non-16-aligned sizes. + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let k = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 2, + mix_measurement: true, + }, + ) + .unwrap(); + + let plaintext = [0x7Au8; 20]; + let err = HardwareKeyProtector::seal_key(&k, &plaintext) + .expect_err("expected seal to fail for non-block-multiple length"); + matches!(err, HardwareKeySealingError::EncryptEgressKey(_)); + } + + #[test] + fn hardware_key_protector_hmac_mismatch_detected() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let hardware_derived_keys = HardwareDerivedKeys::derive_key( mock_get_derived_key_call, &vm_config, - 0x7308000000000003, + tee_call::KeyDerivationPolicy { + tcb_version: 0x7308000000000003, + mix_measurement: true, + }, + ) + .unwrap(); + + let mut hwkp = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT).unwrap(); + + // Corrupt the HMAC to force verification failure + hwkp.hmac[0] ^= 0xFF; + + let err = hwkp + .unseal_key(&hardware_derived_keys) + .expect_err("expected HMAC verification to fail"); + + matches!( + err, + HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed ); - assert!(result.is_ok()); - let hardware_derived_keys = result.unwrap(); + } - let result = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT); - assert!(result.is_ok()); - let output = result.unwrap(); + #[test] + fn unseal_fails_with_different_policy_mix_measurement() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let result = HardwareKeyProtector::read_from_prefix(output.as_bytes()); - assert!(result.is_ok()); - let hardware_key_protector = result.unwrap().0; + let k1: HardwareDerivedKeys = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x1, + mix_measurement: true, + }, + ) + .unwrap(); + let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); - let result = hardware_key_protector.unseal_key(&hardware_derived_keys); - assert!(result.is_ok()); - let plaintext = result.unwrap(); - assert_eq!(plaintext, PLAINTEXT); + let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); + let k2 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0x1, + mix_measurement: false, + }, + ) + .unwrap(); + + let err = hwkp + .unseal_key(&k2) + .expect_err("mix_measurement policy change should break unseal"); + matches!( + err, + HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed + ); + } + + #[test] + fn unseal_fails_with_different_tcb_version() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + + let k1 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0xAAAAAAAAAAAAAAAA, + mix_measurement: true, + }, + ) + .unwrap(); + let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); + + let k2 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0xBBBBBBBBBBBBBBBB, + mix_measurement: true, + }, + ) + .unwrap(); + + let err = hwkp + .unseal_key(&k2) + .expect_err("TCB change should break unseal"); + matches!( + err, + HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed + ); + } + + #[test] + fn unseal_fails_with_different_measurements() { + let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); + let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + + let k1 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0xAAAAAAAAAAAAAAAA, + mix_measurement: true, + }, + ) + .unwrap(); + let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); + + let mock_tee_call = Box::new(MockTeeCall::new([0x8bu8; 32])) as Box; + let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let k2 = HardwareDerivedKeys::derive_key( + mock_get_derived_key_call, + &vm_config, + tee_call::KeyDerivationPolicy { + tcb_version: 0xAAAAAAAAAAAAAAAA, + mix_measurement: true, + }, + ) + .unwrap(); + + let err = hwkp + .unseal_key(&k2) + .expect_err("measurement change should break unseal"); + matches!( + err, + HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed + ); } } diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index 132a84b53f..d86a11afcc 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -354,6 +354,7 @@ fn runtime_claims_to_bytes( #[cfg(test)] mod tests { use super::*; + use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; #[test] fn test_create_request() { @@ -491,7 +492,7 @@ mod tests { #[test] fn test_vm_configuration_no_time() { - const EXPECTED_JWK: &str = r#"{"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; + const EXPECTED_JWK: &str = r#"{"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"hardware-sealing-policy":"signer","filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; let attestation_vm_config = AttestationVmConfig { current_time: None, @@ -501,6 +502,7 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, + hardware_sealing_policy: HardwareSealingPolicy::Signer, filtered_vpci_devices_allowed: true, vm_unique_id: String::new(), }; @@ -513,7 +515,7 @@ mod tests { #[test] fn test_vm_configuration_with_time() { - const EXPECTED_JWK: &str = r#"{"current-time":1691103220,"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; + const EXPECTED_JWK: &str = r#"{"current-time":1691103220,"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"hardware-sealing-policy":"hash","filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; let attestation_vm_config = AttestationVmConfig { current_time: None, @@ -523,6 +525,7 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, + hardware_sealing_policy: HardwareSealingPolicy::Hash, filtered_vpci_devices_allowed: true, vm_unique_id: String::new(), }; diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index a277076e02..6fe981c9f2 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -24,6 +24,7 @@ pub use igvm_attest::Error as IgvmAttestError; pub use igvm_attest::IgvmAttestRequestHelper; pub use igvm_attest::ak_cert::parse_response as parse_ak_cert_response; +use crate::hardware_key_sealing::HardwareKeySealingError; use ::vmgs::EncryptionAlgorithm; use ::vmgs::GspType; use ::vmgs::Vmgs; @@ -40,8 +41,13 @@ use key_protector::GetKeysFromKeyProtectorError; use key_protector::KeyProtectorExt as _; use mesh::MeshPayload; use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; +use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; +use openhcl_attestation_protocol::vmgs::AES_CBC_KEY_LENGTH; use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH; use openhcl_attestation_protocol::vmgs::AGENT_DATA_MAX_SIZE; +use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION; +use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_VERSION_1; +use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_VERSION_2; use openhcl_attestation_protocol::vmgs::HardwareKeyProtector; use openhcl_attestation_protocol::vmgs::KeyProtector; use openhcl_attestation_protocol::vmgs::SecurityProfile; @@ -51,6 +57,8 @@ use pal_async::local::LocalDriver; use secure_key_release::VmgsEncryptionKeys; use static_assertions::const_assert_eq; use std::fmt::Debug; +use tee_call::KeyDerivationPolicy; +use tee_call::REPORT_DATA_SIZE; use tee_call::TeeCall; use thiserror::Error; use zerocopy::FromZeros; @@ -81,6 +89,8 @@ enum AttestationErrorInner { UnlockVmgsDataStore(#[source] UnlockVmgsDataStoreError), #[error("failed to read guest secret key from vmgs")] ReadGuestSecretKey(#[source] vmgs::ReadFromVmgsError), + #[error("failed to get an attestation report")] + GetAttestationReport(#[source] tee_call::Error), } #[derive(Debug, Error)] @@ -94,9 +104,9 @@ enum GetDerivedKeysError { #[error("GSP By Id required, but no GSP By Id found")] GspByIdRequiredButNotFound, #[error("failed to unseal the ingress key using hardware derived keys")] - UnsealIngressKeyUsingHardwareDerivedKeys( - #[source] hardware_key_sealing::HardwareKeySealingError, - ), + UnsealIngressKeyUsingHardwareDerivedKeys(#[source] HardwareKeySealingError), + #[error("failed to get an ingress key from hardware key protector")] + GetIngressKeyFromHardwareKeyProtectorFailed, #[error("failed to get an ingress key from key protector")] GetIngressKeyFromKpFailed, #[error("failed to get an ingress key from guest state protection")] @@ -108,7 +118,7 @@ enum GetDerivedKeysError { #[error("VMGS encryption is required, but no encryption sources were found")] EncryptionRequiredButNotFound, #[error("failed to seal the egress key using hardware derived keys")] - SealEgressKeyUsingHardwareDerivedKeys(#[source] hardware_key_sealing::HardwareKeySealingError), + SealEgressKeyUsingHardwareDerivedKeys(#[source] HardwareKeySealingError), #[error("failed to write to `FileId::HW_KEY_PROTECTOR` in vmgs")] VmgsWriteHardwareKeyProtector(#[source] vmgs::WriteToVmgsError), #[error("failed to get derived key by id")] @@ -119,6 +129,8 @@ enum GetDerivedKeysError { DeriveEgressKey(#[source] crypto::KbkdfError), #[error("skipped hardware unsealing for VMGS DEK as signaled by IGVM agent")] HardwareUnsealingSkipped, + #[error("Hardware sealing is required, but not supported")] + HardwareSealingRequiredButNotSupported, } #[derive(Debug, Error)] @@ -158,9 +170,11 @@ enum UnlockVmgsDataStoreError { #[derive(Debug, Error)] enum PersistAllKeyProtectorsError { #[error("failed to write key protector to vmgs")] - WriteKeyProtector(#[source] vmgs::WriteToVmgsError), + KeyProtector(#[source] vmgs::WriteToVmgsError), #[error("failed to read key protector by id to vmgs")] - WriteKeyProtectorById(#[source] vmgs::WriteToVmgsError), + KeyProtectorById(#[source] vmgs::WriteToVmgsError), + #[error("failed to write hardware key protector to vmgs")] + HardwareKeyProtector(#[source] vmgs::WriteToVmgsError), } // Operation types for provisioning telemetry. @@ -218,6 +232,8 @@ struct DerivedKeyResult { key_protector_settings: KeyProtectorSettings, /// The instance of [`GspExtendedStatusFlags`] returned by GSP. gsp_extended_status_flags: GspExtendedStatusFlags, + /// Optional hardware key protector. + hardware_key_protector: Option, } /// The return values of [`initialize_platform_security`]. @@ -231,7 +247,6 @@ pub struct PlatformAttestationData { } /// The attestation type to use. -// TODO: Support VBS #[derive(Debug, MeshPayload, Copy, Clone, PartialEq, Eq)] pub enum AttestationType { /// Use the SEV-SNP TEE for attestation. @@ -256,21 +271,38 @@ async fn try_unlock_vmgs( tee_call: Option<&dyn TeeCall>, guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, + require_hardware_sealing: bool, agent_data: &mut [u8; AGENT_DATA_MAX_SIZE], key_protector_by_id: &mut KeyProtectorById, ) -> Result { let skr_response = if let Some(tee_call) = tee_call { - tracing::info!(CVM_ALLOWED, "Retrieving key-encryption key"); + if !require_hardware_sealing { + tracing::info!(CVM_ALLOWED, "Retrieving key-encryption key"); + // Retrieve the tenant key via attestation + secure_key_release::request_vmgs_encryption_keys( + get, + tee_call, + vmgs, + attestation_vm_config, + agent_data, + ) + .await + } else { + tracing::info!( + CVM_ALLOWED, + "Getting attestation report only for hardware sealing" + ); - // Retrieve the tenant key via attestation - secure_key_release::request_vmgs_encryption_keys( - get, - tee_call, - vmgs, - attestation_vm_config, - agent_data, - ) - .await + let report = tee_call + .get_attestation_report(&[0; REPORT_DATA_SIZE]) + .map_err(|e| (AttestationErrorInner::GetAttestationReport(e), false))?; + + Ok(VmgsEncryptionKeys { + ingress_rsa_kek: None, + wrapped_des_key: None, + tcb_version: report.tcb_version, + }) + } } else { tracing::info!(CVM_ALLOWED, "Key-encryption key retrieval not required"); @@ -319,27 +351,53 @@ async fn try_unlock_vmgs( } }; - // Determine the minimal size of a DEK entry based on whether `wrapped_des_key` presents - let dek_minimal_size = if wrapped_des_key.is_some() { - key_protector::AES_WRAPPED_AES_KEY_LENGTH + let mut key_protector = if !require_hardware_sealing { + // Determine the minimal size of a DEK entry based on whether `wrapped_des_key` presents + let dek_minimal_size = if wrapped_des_key.is_some() { + key_protector::AES_WRAPPED_AES_KEY_LENGTH + } else { + key_protector::RSA_WRAPPED_AES_KEY_LENGTH + }; + + // Read Key Protector blob from VMGS + tracing::info!( + CVM_ALLOWED, + dek_minimal_size = dek_minimal_size, + "Reading key protector from VMGS" + ); + + vmgs::read_key_protector(vmgs, dek_minimal_size) + .await + .map_err(|e| (AttestationErrorInner::ReadKeyProtector(e), false))? } else { - key_protector::RSA_WRAPPED_AES_KEY_LENGTH + tracing::info!( + CVM_ALLOWED, + "Hardware sealing is required, skip reading key protector from VMGS" + ); + KeyProtector::new_zeroed() }; - // Read Key Protector blob from VMGS - tracing::info!( - CVM_ALLOWED, - dek_minimal_size = dek_minimal_size, - "Reading key protector from VMGS" - ); - let mut key_protector = vmgs::read_key_protector(vmgs, dek_minimal_size) - .await - .map_err(|e| (AttestationErrorInner::ReadKeyProtector(e), false))?; - let start_time = std::time::SystemTime::now(); let vmgs_encrypted = vmgs.encrypted(); + + // Determine mix_measurement based on hardware sealing policy: + // - None: false (no hardware sealing) + // - Hash: true (mix measurement for strong binding) + // - Signer: false (use signer-based policy only) + let mix_measurement = match attestation_vm_config.hardware_sealing_policy { + HardwareSealingPolicy::None => false, + HardwareSealingPolicy::Hash => true, + HardwareSealingPolicy::Signer => false, + }; + + let key_derivation_policy = tcb_version.map(|tcb_version| KeyDerivationPolicy { + tcb_version, + mix_measurement, + }); + tracing::info!( - ?tcb_version, + CVM_ALLOWED, + key_derivation_policy=?key_derivation_policy, vmgs_encrypted, op_type = ?LogOpType::BeginDecryptVmgs, "Deriving keys" @@ -356,9 +414,10 @@ async fn try_unlock_vmgs( vmgs_encrypted, ingress_rsa_kek.as_ref(), wrapped_des_key.as_deref(), - tcb_version, + key_derivation_policy, guest_state_encryption_policy, strict_encryption_policy, + require_hardware_sealing, skip_hw_unsealing, ) .await @@ -376,13 +435,14 @@ async fn try_unlock_vmgs( (AttestationErrorInner::GetDerivedKeys(e), retry) })?; - // All Underhill VMs use VMGS encryption tracing::info!("Unlocking VMGS"); + if let Err(e) = unlock_vmgs_data_store( vmgs, vmgs_encrypted, &mut key_protector, key_protector_by_id, + derived_keys_result.hardware_key_protector, derived_keys_result.derived_keys, derived_keys_result.key_protector_settings, bios_guid, @@ -457,9 +517,20 @@ pub async fn initialize_platform_security( .await .map_err(AttestationErrorInner::ReadSecurityProfile)?; - // If attestation is suppressed, return the `agent_data` that is required by - // TPM AK cert request. - if suppress_attestation { + let require_hardware_sealing = tee_call.is_some() + && matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::HardwareSealing + ) + && !matches!( + attestation_vm_config.hardware_sealing_policy, + HardwareSealingPolicy::None + ); + + // Attestation is suppressed and `guest_state_encryption_policy` is not + // `HardwareSealing` indicates that VMGS encryption is bypassed. Skip the attestation flow + // and return the `agent_data` that is required by TPM AK cert request. + if suppress_attestation && !require_hardware_sealing { tracing::info!(CVM_ALLOWED, "Suppressing attestation"); return Ok(PlatformAttestationData { @@ -471,31 +542,44 @@ pub async fn initialize_platform_security( }); } - // Read VM id from VMGS - tracing::info!(CVM_ALLOWED, "Reading VM ID from VMGS"); - let mut key_protector_by_id = match vmgs::read_key_protector_by_id(vmgs).await { - Ok(key_protector_by_id) => KeyProtectorById { - inner: key_protector_by_id, - found_id: true, - }, - Err(vmgs::ReadFromVmgsError::EntryNotFound(_)) => KeyProtectorById { - inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), - found_id: false, - }, - Err(e) => { Err(AttestationErrorInner::ReadKeyProtectorById(e)) }?, - }; + let (mut key_protector_by_id, vm_id_changed) = if !require_hardware_sealing { + // Read VM id from VMGS + tracing::info!(CVM_ALLOWED, "Reading VM ID from VMGS"); + let key_protector_by_id = match vmgs::read_key_protector_by_id(vmgs).await { + Ok(key_protector_by_id) => KeyProtectorById { + inner: key_protector_by_id, + found_id: true, + }, + Err(vmgs::ReadFromVmgsError::EntryNotFound(_)) => KeyProtectorById { + inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), + found_id: false, + }, + Err(e) => { Err(AttestationErrorInner::ReadKeyProtectorById(e)) }?, + }; - // Check if the VM id has been changed since last boot with KP write - let vm_id_changed = if key_protector_by_id.found_id { - let changed = key_protector_by_id.inner.id_guid != bios_guid; - if changed { - tracing::info!("VM Id has changed since last boot"); + // Check if the VM id has been changed since last boot with KP write + let vm_id_changed = if key_protector_by_id.found_id { + let changed = key_protector_by_id.inner.id_guid != bios_guid; + if changed { + tracing::info!("VM Id has changed since last boot"); + }; + changed + } else { + // Previous id in KP not found means this is the first boot or the GspById + // is not provisioned, treat id as unchanged for this case. + false }; - changed + + (key_protector_by_id, vm_id_changed) } else { - // Previous id in KP not found means this is the first boot or the GspById - // is not provisioned, treat id as unchanged for this case. - false + // When hardware sealing is required, the key protector by id is not used, and VM id change does not trigger state refresh. + ( + KeyProtectorById { + inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), + found_id: false, + }, + false, + ) }; // Retry attestation call-out if necessary (if VMGS encrypted). @@ -522,6 +606,7 @@ pub async fn initialize_platform_security( tee_call, guest_state_encryption_policy, strict_encryption_policy, + require_hardware_sealing, &mut agent_data, &mut key_protector_by_id, ) @@ -578,6 +663,7 @@ async fn unlock_vmgs_data_store( vmgs_encrypted: bool, key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, + hardware_key_protector: Option, derived_keys: Option, key_protector_settings: KeyProtectorSettings, bios_guid: Guid, @@ -664,6 +750,7 @@ async fn unlock_vmgs_data_store( vmgs, key_protector, key_protector_by_id, + hardware_key_protector.as_ref(), bios_guid, key_protector_settings, ) @@ -698,9 +785,10 @@ async fn get_derived_keys( is_encrypted: bool, ingress_rsa_kek: Option<&Rsa>, wrapped_des_key: Option<&[u8]>, - tcb_version: Option, + key_derivation_policy: Option, guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, + require_hardware_sealing: bool, skip_hw_unsealing: bool, ) -> Result { tracing::info!( @@ -710,14 +798,6 @@ async fn get_derived_keys( "encryption policy" ); - // TODO: implement hardware sealing only - if matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::HardwareSealing - ) { - todo!("hardware sealing") - } - let mut key_protector_settings = KeyProtectorSettings { should_write_kp: true, use_gsp_by_id: false, @@ -883,7 +963,11 @@ async fn get_derived_keys( }; // If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key - if (no_kek && found_dek) || (no_gsp && requires_gsp) || (no_gsp_by_id && requires_gsp_by_id) { + if (no_kek && found_dek) + || (no_gsp && requires_gsp) + || (no_gsp_by_id && requires_gsp_by_id) + || (require_hardware_sealing && is_encrypted) + { // If possible, get ingressKey from hardware sealed data let (hardware_key_protector, hardware_derived_keys) = if skip_hw_unsealing { tracing::warn!( @@ -892,7 +976,8 @@ async fn get_derived_keys( ); get.event_log_fatal( guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED, - ); + ) + .await; return Err(GetDerivedKeysError::HardwareUnsealingSkipped); } else if let Some(tee_call) = tee_call { @@ -911,17 +996,48 @@ async fn get_derived_keys( let hardware_derived_keys = tee_call.supports_get_derived_key().and_then(|tee_call| { if let Some(hardware_key_protector) = &hardware_key_protector { - match HardwareDerivedKeys::derive_key( - tee_call, - attestation_vm_config, - hardware_key_protector.header.tcb_version, - ) { + let policy = match hardware_key_protector.header.version { + HW_KEY_PROTECTOR_VERSION_1 => { + // Version 1 is not forward compatible with other versions because it always mixes the OpenHCL + // measurement into the hardware key derivation function (KDF). This means that any version + // change implying an OpenHCL measurement change will result in a different hardware sealing key, + // causing the unsealing process to fail. To prevent this issue, we return None here and log + // the appropriate information. + // + // NOTE: In future implementations, we should handle version 2 and above differently. + // These versions support forward compatibility when using signer-based sealing policy that + // does not mix the OpenHCL measurement into the hardware KDF. + tracing::error!( + CVM_ALLOWED, + current_version = HW_KEY_PROTECTOR_CURRENT_VERSION, + "HW_KEY_PROTECTOR version 1 is incompatible with newer versions. Skip VMGS DEK unsealing with hardware key protector." + ); + return None; + } + HW_KEY_PROTECTOR_VERSION_2 => KeyDerivationPolicy { + tcb_version: hardware_key_protector.header.tcb_version, + mix_measurement: hardware_key_protector.header.mix_measurement == 1, + }, + unsupported_version => { + // unsupported version + tracing::warn!( + CVM_ALLOWED, + unsupported_version, + "unsupported HW_KEY_PROTECTOR version", + ); + return None; + } + }; + + match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, policy) { Ok(hardware_derived_key) => Some(hardware_derived_key), Err(e) => { // non-fatal tracing::warn!( CVM_ALLOWED, error = &e as &dyn std::error::Error, + tcb_version = hardware_key_protector.header.tcb_version, + mix_measurement = hardware_key_protector.header.mix_measurement, "failed to derive hardware keys using HW_KEY_PROTECTOR", ); None @@ -940,27 +1056,78 @@ async fn get_derived_keys( if let (Some(hardware_key_protector), Some(hardware_derived_keys)) = (hardware_key_protector, hardware_derived_keys) { - derived_keys.ingress = hardware_key_protector - .unseal_key(&hardware_derived_keys) - .map_err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys)?; + let dek = match hardware_key_protector.unseal_key(&hardware_derived_keys) { + Ok(dek) => dek, + Err(e @ HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed) + if require_hardware_sealing => + { + tracing::error!( + CVM_ALLOWED, + "hardware unsealing failed due to inconsistent hardware-derived keys" + ); + + get.event_log_fatal( + guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_INVALID_KEY, + ) + .await; + + return Err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys(e)); + } + Err(e) => { + return Err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys(e)); + } + }; + + derived_keys.ingress = dek; derived_keys.decrypt_egress = None; - derived_keys.encrypt_egress = derived_keys.ingress; - key_protector_settings.should_write_kp = false; - key_protector_settings.use_hardware_unlock = true; + let hardware_key_protector = if require_hardware_sealing && is_encrypted { + // Generate a new key on every boot for key rotation + let mut new_dek = [0u8; AES_CBC_KEY_LENGTH]; + getrandom::fill(&mut new_dek).expect("rng failure"); - tracing::warn!( - CVM_ALLOWED, - "Using hardware-derived key to recover VMGS DEK" - ); + let updated_hardware_key_protector = + HardwareKeyProtector::seal_key(&hardware_derived_keys, &new_dek) + .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; + + derived_keys.encrypt_egress = new_dek; + + tracing::info!( + CVM_ALLOWED, + "Non-first boot with VMGS hardware sealing mode. Generate a new random key for VMGS DEK rotation." + ); + + // Use the updated key protector in the exclusive hardware sealing scenario + // to support per-boot key rotation + updated_hardware_key_protector + } else { + derived_keys.encrypt_egress = derived_keys.ingress; + + tracing::warn!( + CVM_ALLOWED, + "Using hardware-derived key to recover VMGS DEK" + ); + + // Use the same key protector in the VMGS DEK backup scenario + hardware_key_protector + }; + + key_protector_settings.should_write_kp = false; return Ok(DerivedKeyResult { derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: Some(hardware_key_protector), }); } else { - if no_kek && found_dek { + if require_hardware_sealing && is_encrypted { + get.event_log_fatal( + guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, + ) + .await; + return Err(GetDerivedKeysError::GetIngressKeyFromHardwareKeyProtectorFailed); + } else if no_kek && found_dek { return Err(GetDerivedKeysError::GetIngressKeyFromKpFailed); } else if no_gsp && requires_gsp { return Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed); @@ -978,9 +1145,76 @@ async fn get_derived_keys( gsp = !no_gsp, gsp_by_id_available = ?gsp_by_id_available, gsp_by_id = !no_gsp_by_id, + hw_sealing = require_hardware_sealing, "Encryption sources" ); + // Attempt to get hardware derived keys + let hardware_derived_keys = tee_call + .and_then(|tee_call| tee_call.supports_get_derived_key()) + .and_then(|tee_call| { + if let Some(policy) = key_derivation_policy { + match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, policy) { + Ok(keys) => Some(keys), + Err(e) => { + // non-fatal + tracing::warn!( + CVM_ALLOWED, + error = &e as &dyn std::error::Error, + "failed to derive hardware keys" + ); + None + } + } + } else { + None + } + }); + + // Let hardware sealing take precedence over other sources if it's required + if require_hardware_sealing && !is_encrypted { + let Some(hardware_derived_keys) = hardware_derived_keys else { + get.event_log_fatal( + guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, + ) + .await; + return Err(GetDerivedKeysError::HardwareSealingRequiredButNotSupported); + }; + + let mut new_dek = [0u8; AES_CBC_KEY_LENGTH]; + getrandom::fill(&mut new_dek).expect("rng failure"); + + let hardware_key_protector = + match HardwareKeyProtector::seal_key(&hardware_derived_keys, &new_dek) { + Ok(hardware_key_protector) => hardware_key_protector, + Err(e) => { + get.event_log_fatal( + guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, + ) + .await; + return Err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys( + e, + )); + } + }; + + derived_keys.ingress = [0u8; AES_GCM_KEY_LENGTH]; + derived_keys.decrypt_egress = None; + derived_keys.encrypt_egress = new_dek; + + tracing::info!( + CVM_ALLOWED, + "First boot with VMGS hardware sealing mode. Generate a new random key for VMGS encryption." + ); + + return Ok(DerivedKeyResult { + derived_keys: Some(derived_keys), + key_protector_settings, + gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: Some(hardware_key_protector), + }); + } + // Check if sources of encryption are available if no_kek && no_gsp && no_gsp_by_id { if is_encrypted { @@ -1000,34 +1234,12 @@ async fn get_derived_keys( derived_keys: None, key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: None, }); } } } - // Attempt to get hardware derived keys - let hardware_derived_keys = tee_call - .and_then(|tee_call| tee_call.supports_get_derived_key()) - .and_then(|tee_call| { - if let Some(tcb_version) = tcb_version { - match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, tcb_version) - { - Ok(keys) => Some(keys), - Err(e) => { - // non-fatal - tracing::warn!( - CVM_ALLOWED, - error = &e as &dyn std::error::Error, - "failed to derive hardware keys" - ); - None - } - } - } else { - None - } - }); - // Use tenant key (KEK only) if no_gsp && no_gsp_by_id { tracing::info!(CVM_ALLOWED, "No GSP used with SKR"); @@ -1053,6 +1265,7 @@ async fn get_derived_keys( derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: None, }); } @@ -1092,6 +1305,7 @@ async fn get_derived_keys( derived_keys: Some(derived_keys_by_id), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: None, }); } @@ -1247,6 +1461,7 @@ async fn get_derived_keys( derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, + hardware_key_protector: None, }) } @@ -1343,6 +1558,7 @@ async fn persist_all_key_protectors( vmgs: &mut Vmgs, key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, + hardware_key_protector: Option<&HardwareKeyProtector>, bios_guid: Guid, key_protector_settings: KeyProtectorSettings, ) -> Result<(), PersistAllKeyProtectorsError> { @@ -1351,10 +1567,14 @@ async fn persist_all_key_protectors( if key_protector_settings.use_gsp_by_id && !key_protector_settings.should_write_kp { vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, false, bios_guid) .await - .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; + .map_err(PersistAllKeyProtectorsError::KeyProtectorById)?; } else { // If HW Key unlocked VMGS, do not alter KP - if !key_protector_settings.use_hardware_unlock { + if let Some(hardware_key_protector) = hardware_key_protector { + vmgs::write_hardware_key_protector(hardware_key_protector, vmgs) + .await + .map_err(PersistAllKeyProtectorsError::HardwareKeyProtector)?; + } else { // Remove ingress KP & DEK, no longer applies to data store key_protector.dek[key_protector.active_kp as usize % NUMBER_KP] .dek_buffer @@ -1364,7 +1584,7 @@ async fn persist_all_key_protectors( vmgs::write_key_protector(key_protector, vmgs) .await - .map_err(PersistAllKeyProtectorsError::WriteKeyProtector)?; + .map_err(PersistAllKeyProtectorsError::KeyProtector)?; } // Update Id data to indicate this scheme is no longer in use @@ -1375,7 +1595,7 @@ async fn persist_all_key_protectors( key_protector_by_id.inner.ported = 1; vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, true, bios_guid) .await - .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; + .map_err(PersistAllKeyProtectorsError::KeyProtectorById)?; } } @@ -1387,6 +1607,7 @@ async fn persist_all_key_protectors( pub mod test_utils { use tee_call::GetAttestationReportResult; use tee_call::HW_DERIVED_KEY_LENGTH; + use tee_call::KeyDerivationPolicy; use tee_call::REPORT_DATA_SIZE; use tee_call::TeeCall; use tee_call::TeeCallGetDerivedKey; @@ -1394,14 +1615,24 @@ pub mod test_utils { /// Mock implementation of [`TeeCall`] with get derived key support for testing purposes pub struct MockTeeCall { - /// Mock TCB version to return from get_attestation_report + /// Mock measurement data + pub measurement: [u8; 32], + /// Mock TCB version returned in attestation reports pub tcb_version: u64, } impl MockTeeCall { /// Create a new instance of [`MockTeeCall`]. - pub fn new(tcb_version: u64) -> Self { - Self { tcb_version } + pub fn new(measurement: [u8; 32]) -> Self { + Self { + measurement, + tcb_version: 0x1234, + } + } + + /// Update the mock measurement data. + pub fn update_measurement(&mut self, measurement: [u8; 32]) { + self.measurement = measurement; } } @@ -1431,14 +1662,21 @@ pub mod test_utils { } impl TeeCallGetDerivedKey for MockTeeCall { - fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; 32], tee_call::Error> { + fn get_derived_key( + &self, + policy: KeyDerivationPolicy, + ) -> Result<[u8; 32], tee_call::Error> { // Base test key; mix in policy so different policies yield different derived secrets let mut key: [u8; HW_DERIVED_KEY_LENGTH] = [0xab; HW_DERIVED_KEY_LENGTH]; // Use mutation to simulate the policy - let tcb = tcb_version.to_le_bytes(); + let tcb = policy.tcb_version.to_le_bytes(); for (i, b) in key.iter_mut().enumerate() { - *b ^= tcb[i % tcb.len()]; + if policy.mix_measurement { + *b ^= self.measurement[i] ^ tcb[i % tcb.len()]; + } else { + *b ^= tcb[i % tcb.len()]; + } } Ok(key) @@ -1643,6 +1881,7 @@ mod tests { secure_boot: false, tpm_enabled: true, tpm_persisted: true, + hardware_sealing_policy: HardwareSealingPolicy::None, filtered_vpci_devices_allowed: false, vm_unique_id: String::new(), } @@ -1671,6 +1910,7 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, + None, key_protector_settings, bios_guid, ) @@ -1696,6 +1936,7 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, + None, key_protector_settings, bios_guid, ) @@ -1738,6 +1979,7 @@ mod tests { false, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1794,6 +2036,7 @@ mod tests { true, &mut new_key_protector, &mut new_key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1862,6 +2105,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1936,6 +2180,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2015,6 +2260,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2069,6 +2315,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2156,6 +2403,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2170,6 +2418,178 @@ mod tests { assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); } + #[async_test] + async fn hardware_sealing_first_boot_creates_hwkp_and_encrypts_vmgs(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + // Start with an empty KP to simulate brand-new VMGS with no DEK/GSP present + let mut key_protector = KeyProtector::new_zeroed(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + let bios_guid = Guid::new_random(); + + // Create a GET client backed by the test host + let get_pair = guest_emulation_transport::test_utilities::new_transport_pair( + driver, + None, + get_protocol::ProtocolVersion::NICKEL_REV2, + None, + None, + ) + .await; + + let mock_tee_call = MockTeeCall::new([0x8a; 32]); + + // No KEK, no GSP. Require HardwareSealing and VMGS is not encrypted. + let derived = get_derived_keys( + &get_pair.client, + Some(&mock_tee_call), + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + &AttestationVmConfig { + current_time: None, + root_cert_thumbprint: String::new(), + console_enabled: false, + interactive_console_enabled: false, + secure_boot: false, + tpm_enabled: false, + tpm_persisted: false, + hardware_sealing_policy: HardwareSealingPolicy::Hash, + filtered_vpci_devices_allowed: true, + vm_unique_id: String::new(), + }, + false, + None, + None, + Some(KeyDerivationPolicy { + tcb_version: 0x1234, + mix_measurement: true, + }), + GuestStateEncryptionPolicy::HardwareSealing, + true, + true, + false, + ) + .await + .unwrap(); + + // It must produce an egress key and HWKP + assert!(derived.derived_keys.is_some()); + assert!(derived.hardware_key_protector.is_some()); + + // Apply to VMGS and verify encryption using egress key + unlock_vmgs_data_store( + &mut vmgs, + false, + &mut key_protector, + &mut key_protector_by_id, + derived.hardware_key_protector, + derived.derived_keys, + derived.key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + // VMGS should now be unlockable with only the egress key (ingress zeroed) + vmgs.unlock_with_encryption_key(&[0; AES_GCM_KEY_LENGTH]) + .await + .unwrap_err(); + } + + #[async_test] + async fn hardware_sealing_recovery_uses_hwkp_v2_when_encrypted(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // Pre-encrypt VMGS to simulate previous boot + let bootstrap = [0x33; AES_GCM_KEY_LENGTH]; + vmgs.test_add_new_encryption_key(&bootstrap, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + let bios_guid = Guid::new_random(); + + // Create a HWKP V2 by sealing current key and writing to VMGS + let mock_tee_call = MockTeeCall::new([0x8a; 32]); + + let hdk = HardwareDerivedKeys::derive_key( + mock_tee_call.supports_get_derived_key().unwrap(), + &AttestationVmConfig { + current_time: None, + root_cert_thumbprint: String::new(), + console_enabled: false, + interactive_console_enabled: false, + secure_boot: false, + tpm_enabled: false, + tpm_persisted: false, + hardware_sealing_policy: HardwareSealingPolicy::Hash, + filtered_vpci_devices_allowed: true, + vm_unique_id: String::new(), + }, + KeyDerivationPolicy { + tcb_version: 0x1234, + mix_measurement: true, + }, + ) + .unwrap(); + let hwkp = HardwareKeyProtector::seal_key(&hdk, &bootstrap).unwrap(); + vmgs::write_hardware_key_protector(&hwkp, &mut vmgs) + .await + .unwrap(); + + // Now call get_derived_keys with HardwareSealing required and VMGS encrypted + // Create a GET client backed by the test host + let get_pair = guest_emulation_transport::test_utilities::new_transport_pair( + driver, + None, + get_protocol::ProtocolVersion::NICKEL_REV2, + None, + None, + ) + .await; + + let derived = get_derived_keys( + &get_pair.client, + Some(&mock_tee_call), + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + &AttestationVmConfig { + current_time: None, + root_cert_thumbprint: String::new(), + console_enabled: false, + interactive_console_enabled: false, + secure_boot: false, + tpm_enabled: false, + tpm_persisted: false, + hardware_sealing_policy: HardwareSealingPolicy::Hash, + filtered_vpci_devices_allowed: true, + vm_unique_id: String::new(), + }, + true, + None, + None, + Some(KeyDerivationPolicy { + tcb_version: 0x1234, + mix_measurement: true, + }), + GuestStateEncryptionPolicy::HardwareSealing, + true, + true, + false, + ) + .await + .unwrap(); + + // Should have recovered ingress from HWKP and rotated egress + let keys = derived.derived_keys.unwrap(); + assert_eq!(keys.ingress, bootstrap); + assert_ne!(keys.encrypt_egress, keys.ingress); + } + #[async_test] async fn persist_all_key_protectors_write_key_protector_by_id() { let mut vmgs = new_formatted_vmgs().await; @@ -2193,6 +2613,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + None, bios_guid, key_protector_settings, ) @@ -2237,6 +2658,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + None, bios_guid, key_protector_settings, ) @@ -2285,6 +2707,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2361,7 +2784,7 @@ mod tests { let bios_guid = Guid::new_random(); let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new(0x1234); + let tee = MockTeeCall::new([0x12u8; 32]); // Ensure VMGS is not encrypted and agent data is empty before the call assert!(!vmgs.encrypted()); @@ -2447,7 +2870,7 @@ mod tests { let bios_guid = Guid::new_random(); let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new(0x1234); + let tee = MockTeeCall::new([0x12u8; 32]); // Ensure VMGS is not encrypted and agent data is empty before the call assert!(!vmgs.encrypted()); @@ -2532,7 +2955,7 @@ mod tests { assert!(!vmgs.encrypted()); // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new(0x1234); + let tee = MockTeeCall::new([0x12u8; 32]); let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); let res = initialize_platform_security( &get_pair.client, @@ -2617,7 +3040,7 @@ mod tests { assert!(!vmgs.encrypted()); // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new(0x1234); + let tee = MockTeeCall::new([0x12u8; 32]); let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); let res = initialize_platform_security( &get_pair.client, diff --git a/openhcl/underhill_attestation/src/vmgs.rs b/openhcl/underhill_attestation/src/vmgs.rs index 58bc14c02e..67648f0c42 100644 --- a/openhcl/underhill_attestation/src/vmgs.rs +++ b/openhcl/underhill_attestation/src/vmgs.rs @@ -287,6 +287,7 @@ mod tests { use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; use openhcl_attestation_protocol::vmgs::GspKp; use openhcl_attestation_protocol::vmgs::HMAC_SHA_256_KEY_LENGTH; + use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION; use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_SIZE; use openhcl_attestation_protocol::vmgs::HardwareKeyProtectorHeader; use openhcl_attestation_protocol::vmgs::KEY_PROTECTOR_SIZE; @@ -308,7 +309,12 @@ mod tests { } fn new_hardware_key_protector() -> HardwareKeyProtector { - let header = HardwareKeyProtectorHeader::new(1, HW_KEY_PROTECTOR_SIZE as u32, 2); + let header = HardwareKeyProtectorHeader::new( + HW_KEY_PROTECTOR_CURRENT_VERSION, + HW_KEY_PROTECTOR_SIZE as u32, + 2, + 1, + ); let iv = [3; AES_CBC_IV_LENGTH]; let ciphertext = [4; AES_GCM_KEY_LENGTH]; let hmac = [5; HMAC_SHA_256_KEY_LENGTH]; diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 6524877036..2b1a882cab 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -104,6 +104,7 @@ use mesh_worker::WorkerId; use mesh_worker::WorkerRpc; use net_packet_capture::PacketCaptureParams; use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; +use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; use openhcl_dma_manager::AllocationVisibility; use openhcl_dma_manager::DmaClientParameters; use openhcl_dma_manager::DmaClientSpawner; @@ -1953,6 +1954,23 @@ async fn new_underhill_vm( tracing::warn!(CVM_ALLOWED, "confidential debug enabled"); } + let tpm_persisted = !dps.general.suppress_attestation.unwrap_or(false); + let hardware_sealing_policy = if tpm_persisted { + // If TPM is persisted, use the hash policy to match the existing implementation. + // TODO: Support sealing policy for persisted TPM mode. + HardwareSealingPolicy::Hash + } else { + match dps.general.hardware_sealing_policy { + get_protocol::dps_json::HardwareSealingPolicy::NoSealing => HardwareSealingPolicy::None, + get_protocol::dps_json::HardwareSealingPolicy::HashPolicy => { + HardwareSealingPolicy::Hash + } + get_protocol::dps_json::HardwareSealingPolicy::SignerPolicy => { + HardwareSealingPolicy::Signer + } + } + }; + // Create the `AttestationVmConfig` from `dps`, which will be used in // - stateful mode (the attestation is not suppressed) // - stateless mode (isolated VM with attestation suppressed) @@ -1970,7 +1988,8 @@ async fn new_underhill_vm( interactive_console_enabled: interactive_console, secure_boot: dps.general.secure_boot_enabled, tpm_enabled: dps.general.tpm_enabled, - tpm_persisted: !dps.general.suppress_attestation.unwrap_or(false), + tpm_persisted, + hardware_sealing_policy, filtered_vpci_devices_allowed: with_vmbus_relay && dps.general.vpci_boot_enabled && isolation.is_isolated(), @@ -2880,14 +2899,32 @@ async fn new_underhill_vm( }); if dps.general.tpm_enabled { - let no_persistent_secrets = - vmgs_client.is_none() || dps.general.suppress_attestation.unwrap_or(false); + let no_persistent_secrets = vmgs_client.is_none() + || (dps.general.suppress_attestation.unwrap_or(false) + && matches!( + attestation_vm_config.hardware_sealing_policy, + HardwareSealingPolicy::None + )); let (ppi_store, nvram_store) = if no_persistent_secrets { + tracing::info!( + CVM_ALLOWED, + suppress_attestation=?dps.general.suppress_attestation, + hardware_sealing_policy=?attestation_vm_config.hardware_sealing_policy, + "TPM configured without persistent secrets, using ephemeral stores" + ); + ( EphemeralNonVolatileStoreHandle.into_resource(), EphemeralNonVolatileStoreHandle.into_resource(), ) } else { + tracing::info!( + CVM_ALLOWED, + suppress_attestation=?dps.general.suppress_attestation, + hardware_sealing_policy=?attestation_vm_config.hardware_sealing_policy, + "TPM configured with persistent secrets, using VMGS stores" + ); + ( VmgsFileHandle::new(vmgs::FileId::TPM_PPI, true).into_resource(), VmgsFileHandle::new(vmgs::FileId::TPM_NVRAM, true).into_resource(), @@ -3670,6 +3707,7 @@ fn validate_isolated_configuration(dps: &DevicePlatformSettings) -> Result<(), a suppress_attestation: _, bios_guid: _, vpci_boot_enabled: _, + hardware_sealing_policy: _, // Validated below processor_idle_enabled, diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index d0e035d856..05a8bf9188 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -791,6 +791,21 @@ function Set-GuestStateIsolationMode Set-VmSystemSettings $vssd } +function Set-ManagementVtlEncryptionPolicy +{ + [CmdletBinding()] + Param ( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] $Vm, + + [int] $Policy + ) + + $vssd = Get-VmSystemSettings $Vm + $vssd.ManagementVtlEncryptionPolicy = $Policy + Set-VmSystemSettings $vssd +} + # # CIM Helpers # diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index b27cfb8292..0aa21afc50 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -18,6 +18,7 @@ use crate::OpenHclConfig; use crate::OpenHclServicingFlags; use crate::OpenvmmLogConfig; use crate::PetriHaltReason; +use crate::PetriHardwareSealingPolicy; use crate::PetriVmConfig; use crate::PetriVmResources; use crate::PetriVmRuntime; @@ -550,6 +551,7 @@ impl PetriVmmBackend for HyperVPetriBackend { // Configure the TPM if let Some(TpmConfig { no_persistent_secrets, + hardware_sealing_policy, }) = tpm { if generation == powershell::HyperVGeneration::One { @@ -564,6 +566,19 @@ impl PetriVmmBackend for HyperVPetriBackend { powershell::HyperVGuestStateIsolationMode::Default }) .await?; + + if hardware_sealing_policy != PetriHardwareSealingPolicy::Default { + vm.set_management_vtl_encryption_policy(match hardware_sealing_policy { + PetriHardwareSealingPolicy::HashPolicy => { + powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsHashPolicy + } + PetriHardwareSealingPolicy::SignerPolicy => { + powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsSignerPolicy + } + PetriHardwareSealingPolicy::Default => unreachable!(), + }) + .await?; + } } else if no_persistent_secrets { anyhow::bail!("no persistent secrets requires an hcl"); } diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index 54ddae5dfe..e9af5fdc6b 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -1321,3 +1321,59 @@ pub async fn run_disable_vmtpm(vmid: &Guid) -> anyhow::Result<()> { .map(|_| ()) .context("run_disable_vmtpm") } + +/// VTL encryption policies for Hyper-V VMs. +#[derive(Debug)] +pub enum HyperVManagementVtlEncryptionPolicy { + /// Default encryption policy. + Default = 0, + /// Require GSP key encryption policy. + RequireGspKey = 1, + /// Forbid GSP key encryption policy. + ForbidGspKey = 2, + /// No encryption policy. + None = 3, + /// Hardware sealed secrets hash policy. + HardwareSealedSecretsHashPolicy = 4, + /// Hardware sealed secrets signer policy. + HardwareSealedSecretsSignerPolicy = 5, +} + +impl ps::AsVal for HyperVManagementVtlEncryptionPolicy { + fn as_val(&self) -> impl '_ + AsRef { + match self { + HyperVManagementVtlEncryptionPolicy::Default => "0", + HyperVManagementVtlEncryptionPolicy::RequireGspKey => "1", + HyperVManagementVtlEncryptionPolicy::ForbidGspKey => "2", + HyperVManagementVtlEncryptionPolicy::None => "3", + HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsHashPolicy => "4", + HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsSignerPolicy => "5", + } + } +} + +/// Sets the management VTL encryption policy for a VM. +pub async fn run_set_management_vtl_encryption_policy( + vmid: &Guid, + ps_mod: &Path, + policy: HyperVManagementVtlEncryptionPolicy, +) -> anyhow::Result<()> { + tracing::trace!(?policy, ?vmid, "set management vtl encryption policy"); + + run_host_cmd( + PowerShellBuilder::new() + .cmdlet("Import-Module") + .positional(ps_mod) + .next() + .cmdlet("Get-VM") + .arg("Id", vmid) + .pipeline() + .cmdlet("Set-ManagementVtlEncryptionPolicy") + .arg("Policy", policy) + .finish() + .build(), + ) + .await + .map(|_| ()) + .context("set_management_vtl_encryption_policy") +} diff --git a/petri/src/vm/hyperv/vm.rs b/petri/src/vm/hyperv/vm.rs index 7f04eb6bb3..825aaa0582 100644 --- a/petri/src/vm/hyperv/vm.rs +++ b/petri/src/vm/hyperv/vm.rs @@ -648,6 +648,14 @@ impl HyperVVM { pub async fn disable_tpm(&self) -> anyhow::Result<()> { powershell::run_disable_vmtpm(&self.vmid).await } + + /// Set the management VTL encryption policy + pub async fn set_management_vtl_encryption_policy( + &self, + policy: powershell::HyperVManagementVtlEncryptionPolicy, + ) -> anyhow::Result<()> { + powershell::run_set_management_vtl_encryption_policy(&self.vmid, &self.ps_mod, policy).await + } } impl Drop for HyperVVM { diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 35172e1a4c..66d499f107 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1081,6 +1081,16 @@ impl PetriVmBuilder { self } + /// Set the hardware sealing policy for the VM's TPM. + pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self { + self.config + .tpm + .as_mut() + .expect("hardware sealing policy requires a TPM") + .hardware_sealing_policy = policy; + self + } + /// Add custom VTL 2 settings. // TODO: At some point we want to replace uses of this with nicer with_disk, // with_nic, etc. methods. @@ -1882,16 +1892,34 @@ impl Default for OpenHclConfig { pub struct TpmConfig { /// Use ephemeral TPM state (do not persist to VMGS) pub no_persistent_secrets: bool, + /// Hardware sealing policy for sealed secrets + pub hardware_sealing_policy: PetriHardwareSealingPolicy, } impl Default for TpmConfig { fn default() -> Self { Self { no_persistent_secrets: true, + hardware_sealing_policy: PetriHardwareSealingPolicy::Default, } } } +/// Hardware sealing policy used by the test infrastructure. +/// +/// Maps to Hyper-V `Set-ManagementVtlEncryptionPolicy` values and +/// underhill's `HardwareSealingPolicy`. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum PetriHardwareSealingPolicy { + /// No explicit policy — the backend picks its default. + #[default] + Default, + /// Derive the hardware sealing key from measurement hash. + HashPolicy, + /// Derive the hardware sealing key from signer information. + SignerPolicy, +} + /// Firmware to load into the test VM. // TODO: remove the guests from the firmware enum so that we don't pass them // to the VMM backend after we have already used them generically. diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 5f826463a3..b454184443 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -911,6 +911,7 @@ impl PetriVmConfigSetupCore<'_> { if !self.firmware.is_openhcl() && let Some(TpmConfig { no_persistent_secrets, + .. }) = self.tpm_config { let register_layout = match self.arch { diff --git a/vm/devices/get/get_protocol/src/dps_json.rs b/vm/devices/get/get_protocol/src/dps_json.rs index 98008fd5eb..1bf40cd671 100644 --- a/vm/devices/get/get_protocol/src/dps_json.rs +++ b/vm/devices/get/get_protocol/src/dps_json.rs @@ -121,7 +121,7 @@ pub enum GuestStateEncryptionPolicy { /// Prefer (or require, if strict) GspById. /// /// This prevents a VM from being created as or migrated to GspKey even - /// if it is available. Exisiting GspKey encryption will be used unless + /// if it is available. Existing GspKey encryption will be used unless /// strict encryption policy is enabled. Fails if the data cannot be /// encrypted. GspById, @@ -131,8 +131,9 @@ pub enum GuestStateEncryptionPolicy { /// be used if GspKey is unavailable unless strict encryption policy is /// enabled. Fails if the data cannot be encrypted. GspKey, - /// Use hardware sealing - // TODO: update this doc comment once hardware sealing is implemented + /// Use hardware sealing exclusively. + /// + /// Expect to be set only when `no_persistent_secrets` is true on CVMs. HardwareSealing, } @@ -149,6 +150,23 @@ open_enum! { } } +/// Hardware sealing policy +/// +/// Used when `no_persistent_secrets` is true +/// By default, the policy will be applied to hardware-sealing-based +/// VMGS DEK backup on CVMs. If [`GuestStateEncryptionPolicy::HardwareSealing`] +/// is selected, this policy will be applied to the exclusive hardware sealing. +#[derive(Debug, Copy, Clone, Deserialize, Serialize, Default)] +pub enum HardwareSealingPolicy { + /// No hardware sealing + #[default] + NoSealing, + /// Hash-based hardware sealing + HashPolicy, + /// Signer-based hardware sealing + SignerPolicy, +} + /// Management VTL Feature Flags #[bitfield(u64)] #[derive(Deserialize, Serialize)] @@ -166,7 +184,7 @@ pub struct ManagementVtlFeatures { #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct HclDevicePlatformSettingsV2Static { - //UEFI flags + // UEFI flags pub legacy_memory_map: bool, pub pause_after_boot_failure: bool, pub pxe_ip_v6: bool, @@ -221,6 +239,8 @@ pub struct HclDevicePlatformSettingsV2Static { pub management_vtl_features: ManagementVtlFeatures, #[serde(default)] pub hv_sint_enabled: bool, + #[serde(default)] + pub hardware_sealing_policy: HardwareSealingPolicy, } #[derive(Debug, Default, Deserialize, Serialize)] diff --git a/vm/devices/get/get_protocol/src/lib.rs b/vm/devices/get/get_protocol/src/lib.rs index 86f7dcd267..06d08279fd 100644 --- a/vm/devices/get/get_protocol/src/lib.rs +++ b/vm/devices/get/get_protocol/src/lib.rs @@ -350,6 +350,8 @@ open_enum! { TPM_INVALID_STATE = 17, TPM_IDENTITY_CHANGE_FAILED = 18, WRAPPED_KEY_REQUIRED_BUT_INVALID = 19, + DEK_HARDWARE_SEALING_INVALID_KEY = 20, + DEK_HARDWARE_SEALING_FAILED = 21, DEK_HARDWARE_UNSEALING_SKIPPED = 23, } } diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index 588da4b69f..84fe2c2231 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -254,5 +254,8 @@ pub mod ged { AkCertPersistentAcrossBoot, /// Config for testing skip hardware unsealing signal from IGVMAgent. KeyReleaseFailureSkipHwUnsealing, + /// Config for testing key release failure without skip_hw_unsealing + /// signal — hardware unsealing fallback should be attempted. + KeyReleaseFailure, } } diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 4075990cce..bc0849bb05 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -42,6 +42,7 @@ use get_protocol::VmgsIoStatus; use get_protocol::dps_json::EfiDiagnosticsLogLevelType; use get_protocol::dps_json::GuestStateEncryptionPolicy; use get_protocol::dps_json::GuestStateLifetime; +use get_protocol::dps_json::HardwareSealingPolicy; use get_protocol::dps_json::HclSecureBootTemplateId; use get_protocol::dps_json::ManagementVtlFeatures; use get_protocol::dps_json::PcatBootDevice; @@ -158,6 +159,9 @@ pub struct GuestConfig { /// Management VTL feature flags #[inspect(debug)] pub management_vtl_features: ManagementVtlFeatures, + /// Hardware sealing policy + #[inspect(debug)] + pub hardware_sealing_policy: HardwareSealingPolicy, /// EFI diagnostics log level #[inspect(debug)] pub efi_diagnostics_log_level: EfiDiagnosticsLogLevelType, @@ -1354,6 +1358,7 @@ impl GedChannel { guest_state_lifetime: state.config.guest_state_lifetime, guest_state_encryption_policy: state.config.guest_state_encryption_policy, management_vtl_features: state.config.management_vtl_features, + hardware_sealing_policy: state.config.hardware_sealing_policy, efi_diagnostics_log_level: state.config.efi_diagnostics_log_level, hv_sint_enabled: state.config.hv_sint_enabled, }, diff --git a/vm/devices/get/guest_emulation_device/src/resolver.rs b/vm/devices/get/guest_emulation_device/src/resolver.rs index 195727504f..4539034cde 100644 --- a/vm/devices/get/guest_emulation_device/src/resolver.rs +++ b/vm/devices/get/guest_emulation_device/src/resolver.rs @@ -196,6 +196,7 @@ impl AsyncResolveResource guest_state_lifetime, guest_state_encryption_policy, management_vtl_features, + hardware_sealing_policy: get_protocol::dps_json::HardwareSealingPolicy::default(), efi_diagnostics_log_level: match resource.efi_diagnostics_log_level { EfiDiagnosticsLogLevelType::Default => { get_protocol::dps_json::EfiDiagnosticsLogLevelType::DEFAULT diff --git a/vm/devices/get/guest_emulation_device/src/test_utilities.rs b/vm/devices/get/guest_emulation_device/src/test_utilities.rs index 34c7c15276..4026bf5c9e 100644 --- a/vm/devices/get/guest_emulation_device/src/test_utilities.rs +++ b/vm/devices/get/guest_emulation_device/src/test_utilities.rs @@ -261,6 +261,7 @@ pub fn create_host_channel( guest_state_lifetime: Default::default(), guest_state_encryption_policy: Default::default(), management_vtl_features: Default::default(), + hardware_sealing_policy: Default::default(), efi_diagnostics_log_level: Default::default(), hv_sint_enabled: false, }; diff --git a/vm/devices/get/guest_emulation_transport/src/api.rs b/vm/devices/get/guest_emulation_transport/src/api.rs index de9f74e4c7..caf9dc03b9 100644 --- a/vm/devices/get/guest_emulation_transport/src/api.rs +++ b/vm/devices/get/guest_emulation_transport/src/api.rs @@ -31,6 +31,7 @@ pub mod platform_settings { use get_protocol::dps_json::EfiDiagnosticsLogLevelType; use get_protocol::dps_json::GuestStateEncryptionPolicy; use get_protocol::dps_json::GuestStateLifetime; + use get_protocol::dps_json::HardwareSealingPolicy; use get_protocol::dps_json::ManagementVtlFeatures; use guid::Guid; use inspect::Inspect; @@ -135,6 +136,8 @@ pub mod platform_settings { pub guest_state_encryption_policy: GuestStateEncryptionPolicy, #[inspect(debug)] pub management_vtl_features: ManagementVtlFeatures, + #[inspect(debug)] + pub hardware_sealing_policy: HardwareSealingPolicy, pub hv_sint_enabled: bool, } diff --git a/vm/devices/get/guest_emulation_transport/src/client.rs b/vm/devices/get/guest_emulation_transport/src/client.rs index fc361818a6..3007b24469 100644 --- a/vm/devices/get/guest_emulation_transport/src/client.rs +++ b/vm/devices/get/guest_emulation_transport/src/client.rs @@ -344,6 +344,7 @@ impl GuestEmulationTransportClient { guest_state_lifetime: json.v2.r#static.guest_state_lifetime, guest_state_encryption_policy: json.v2.r#static.guest_state_encryption_policy, management_vtl_features: json.v2.r#static.management_vtl_features, + hardware_sealing_policy: json.v2.r#static.hardware_sealing_policy, hv_sint_enabled: json.v2.r#static.hv_sint_enabled, }, acpi_tables: json.v2.dynamic.acpi_tables, diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 3f0dd09b53..5fa253c724 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -177,6 +177,15 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ]), ); } + IgvmAttestTestConfig::KeyReleaseFailure => { + plan.insert( + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondFailure, + ]), + ); + } } plan diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index f951f826ac..b47fae069a 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -30,6 +30,8 @@ enum TestConfig { AkCertPersistentAcrossBoot, /// Test skip hardware unsealing signal from IGVM Agent KeyReleaseFailureSkipHwUnsealing, + /// Test key release failure without skip_hw_unsealing signal + KeyReleaseFailure, } impl From for IgvmAttestTestConfig { @@ -44,6 +46,7 @@ impl From for IgvmAttestTestConfig { TestConfig::KeyReleaseFailureSkipHwUnsealing => { IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing } + TestConfig::KeyReleaseFailure => IgvmAttestTestConfig::KeyReleaseFailure, } } } diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index bab479bad2..569215ba6d 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -80,6 +80,7 @@ fn config_for_vm_name(vm_name: &str) -> Option { "skip_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, ), + ("hw_unseal", IgvmAttestTestConfig::KeyReleaseFailure), ]; for &(pattern, config) in KNOWN_TEST_CONFIGS { @@ -177,11 +178,26 @@ mod tests { #[test] fn no_match_unknown_vm() { - assert!(config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test").is_none()); + assert!( + config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test") + .is_none() + ); + } + + #[test] + fn match_hw_unseal() { + let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_hw_unseal"; + assert!(vm_name.len() <= 100, "name must fit in 100 chars"); + match config_for_vm_name(vm_name) { + Some(IgvmAgentTestSetting::TestConfig(c)) => { + assert!(matches!(c, IgvmAttestTestConfig::KeyReleaseFailure)) + } + other => panic!("expected KeyReleaseFailure, got {:?}", other), + } } #[test] fn no_match_empty() { assert!(config_for_vm_name("").is_none()); } -} \ No newline at end of file +} diff --git a/vm/devices/tpm/tpm_guest_tests/src/main.rs b/vm/devices/tpm/tpm_guest_tests/src/main.rs index 6fb46a1a35..8e6e08e5bd 100644 --- a/vm/devices/tpm/tpm_guest_tests/src/main.rs +++ b/vm/devices/tpm/tpm_guest_tests/src/main.rs @@ -54,6 +54,27 @@ struct Config { report: bool, user_data: Option>, show_runtime_claims: bool, + nv_define: Option, + nv_write: Option, + nv_read: Option, +} + +#[derive(Debug)] +struct NvDefineConfig { + index: u32, + size: u16, +} + +#[derive(Debug)] +struct NvWriteConfig { + index: u32, + data: Vec, +} + +#[derive(Debug)] +struct NvReadConfig { + index: u32, + expected: Option>, } #[derive(Parser, Debug)] @@ -77,6 +98,15 @@ enum Command { /// Write guest input and read the attestation report #[command(name = "report")] Report(ReportArgs), + /// Define an NV index with a given size + #[command(name = "nv_define")] + NvDefine(NvDefineArgs), + /// Write data to an NV index + #[command(name = "nv_write")] + NvWrite(NvWriteArgs), + /// Read data from an NV index + #[command(name = "nv_read")] + NvRead(NvReadArgs), } #[derive(Args, Debug, Default)] @@ -109,6 +139,45 @@ struct ReportArgs { show_runtime_claims: bool, } +#[derive(Args, Debug)] +struct NvDefineArgs { + /// NV index handle (hex, e.g. 0x1500016) + #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] + index: u32, + + /// Size of the NV index in bytes + #[arg(long, value_name = "BYTES")] + size: u16, +} + +#[derive(Args, Debug)] +struct NvWriteArgs { + /// NV index handle (hex, e.g. 0x1500016) + #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] + index: u32, + + /// Data to write (hex) + #[arg(long, value_name = "HEX")] + data_hex: String, +} + +#[derive(Args, Debug)] +struct NvReadArgs { + /// NV index handle (hex, e.g. 0x1500016) + #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] + index: u32, + + /// Expected data contents (hex); if provided, verifies the read result + #[arg(long, value_name = "HEX")] + expected_data_hex: Option, +} + +fn parse_nv_index(s: &str) -> Result { + let trimmed = s.trim(); + let hex = trimmed.strip_prefix("0x").unwrap_or(trimmed); + u32::from_str_radix(hex, 16).map_err(|e| format!("invalid NV index: {e}")) +} + fn main() { let cli = Cli::parse(); let config = match config_from_cli(cli) { @@ -152,6 +221,18 @@ fn run(config: &Config) -> Result<(), Box> { } } + if let Some(nv_def) = &config.nv_define { + handle_nv_define(&mut helper, nv_def.index, nv_def.size)?; + } + + if let Some(nv_wr) = &config.nv_write { + handle_nv_write(&mut helper, nv_wr.index, &nv_wr.data)?; + } + + if let Some(nv_rd) = &config.nv_read { + handle_nv_read(&mut helper, nv_rd.index, nv_rd.expected.as_deref())?; + } + Ok(()) } @@ -249,6 +330,31 @@ fn config_from_cli(cli: Cli) -> Result { config.user_data = Some(bytes); } } + Command::NvDefine(args) => { + config.nv_define = Some(NvDefineConfig { + index: args.index, + size: args.size, + }); + } + Command::NvWrite(args) => { + let data = parse_hex_bytes(&args.data_hex).map_err(|e| format!("--data-hex: {e}"))?; + config.nv_write = Some(NvWriteConfig { + index: args.index, + data, + }); + } + Command::NvRead(args) => { + let expected = args + .expected_data_hex + .as_deref() + .map(parse_hex_bytes) + .transpose() + .map_err(|e| format!("--expected-data-hex: {e}"))?; + config.nv_read = Some(NvReadConfig { + index: args.index, + expected, + }); + } } Ok(config) @@ -286,6 +392,69 @@ fn handle_report( Ok(att_report) } +fn handle_nv_define( + helper: &mut TpmEngineHelper, + nv_index: u32, + size: u16, +) -> Result<(), Box> { + if helper.nv_read_public(nv_index).is_ok() { + println!("NV index {nv_index:#x} already defined, undefining first…"); + helper + .nv_undefine_space(TPM20_RH_OWNER, nv_index) + .map_err(|e| -> Box { Box::new(e) })?; + } + + println!("Defining NV index {nv_index:#x} with {size} bytes…"); + helper + .nv_define_space(TPM20_RH_OWNER, 0, nv_index, size) + .map_err(|e| -> Box { Box::new(e) })?; + + println!("NV index {nv_index:#x} defined successfully ({size} bytes)."); + Ok(()) +} + +fn handle_nv_write( + helper: &mut TpmEngineHelper, + nv_index: u32, + data: &[u8], +) -> Result<(), Box> { + println!("Writing {} bytes to NV index {nv_index:#x}…", data.len()); + helper.nv_write(TPM20_RH_OWNER, None, nv_index, data)?; + println!( + "NV write to {nv_index:#x} succeeded ({} bytes).", + data.len() + ); + Ok(()) +} + +fn handle_nv_read( + helper: &mut TpmEngineHelper, + nv_index: u32, + expected: Option<&[u8]>, +) -> Result<(), Box> { + println!("Reading NV index {nv_index:#x}…"); + let data = read_nv_index(helper, nv_index)?; + print_nv_summary("NV read", &data); + + if let Some(expected) = expected { + if data == expected { + println!( + "NV index {nv_index:#x} matches expected value ({} bytes).", + data.len() + ); + } else { + return Err(format!( + "NV index {nv_index:#x} contents did not match expected value (got {} bytes, expected {} bytes)", + data.len(), + expected.len() + ) + .into()); + } + } + + Ok(()) +} + fn print_runtime_claims(attestation_report: &[u8]) -> Result<(), Box> { match runtime_claims_json(attestation_report)? { Some(json) => { diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index cdfb28bbbd..2200eb1b00 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -5,6 +5,7 @@ use anyhow::Context; use anyhow::ensure; use petri::PetriGuestStateLifetime; use petri::PetriHaltReason; +use petri::PetriHardwareSealingPolicy; use petri::PetriVmBuilder; use petri::PetriVmmBackend; use petri::ResolvedArtifact; @@ -188,6 +189,88 @@ impl<'a> TpmGuestTests<'a> { _ => unreachable!(), } } + + /// Define an NV index with the given size. + async fn nv_define(&self, index: &str, size: &str) -> anyhow::Result { + let guest_binary_path = &self.guest_binary_path; + match self.os_flavor { + OsFlavor::Linux => { + let sh = self.agent.unix_shell(); + cmd!(sh, "{guest_binary_path}") + .args(["nv_define", "--index", index, "--size", size]) + .read() + .await + } + OsFlavor::Windows => { + let sh = self.agent.windows_shell(); + cmd!(sh, "{guest_binary_path}") + .args(["nv_define", "--index", index, "--size", size]) + .read() + .await + } + _ => unreachable!(), + } + } + + /// Write hex data to an NV index. + async fn nv_write(&self, index: &str, data_hex: &str) -> anyhow::Result { + let guest_binary_path = &self.guest_binary_path; + match self.os_flavor { + OsFlavor::Linux => { + let sh = self.agent.unix_shell(); + cmd!(sh, "{guest_binary_path}") + .args(["nv_write", "--index", index, "--data-hex", data_hex]) + .read() + .await + } + OsFlavor::Windows => { + let sh = self.agent.windows_shell(); + cmd!(sh, "{guest_binary_path}") + .args(["nv_write", "--index", index, "--data-hex", data_hex]) + .read() + .await + } + _ => unreachable!(), + } + } + + /// Read an NV index and verify against expected hex data. + async fn nv_read_with_expected_hex( + &self, + index: &str, + expected_hex: &str, + ) -> anyhow::Result { + let guest_binary_path = &self.guest_binary_path; + match self.os_flavor { + OsFlavor::Linux => { + let sh = self.agent.unix_shell(); + cmd!(sh, "{guest_binary_path}") + .args([ + "nv_read", + "--index", + index, + "--expected-data-hex", + expected_hex, + ]) + .read() + .await + } + OsFlavor::Windows => { + let sh = self.agent.windows_shell(); + cmd!(sh, "{guest_binary_path}") + .args([ + "nv_read", + "--index", + index, + "--expected-data-hex", + expected_hex, + ]) + .read() + .await + } + _ => unreachable!(), + } + } } /// Basic boot tests with TPM enabled. @@ -564,6 +647,7 @@ async fn cvm_tpm_guest_tests( /// `KeyReleaseFailureSkipHwUnsealing` configuration. #[cfg(windows)] #[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], )] async fn skip_hwun_seal( @@ -584,6 +668,7 @@ async fn skip_hwun_seal( .await?; let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, _ => unreachable!(), }; @@ -669,3 +754,275 @@ async fn tpm_servicing( vm.wait_for_clean_teardown().await?; Ok(()) } + +/// Test that KEY_RELEASE failure without skip_hw_unsealing signal allows +/// hardware unsealing fallback to succeed. +/// +/// First boot: KEY_RELEASE succeeds, VMGS is encrypted with hardware +/// key protector, TPM state is sealed. AK cert is verified. +/// Second boot: KEY_RELEASE fails (plain failure, no skip_hw_unsealing +/// signal), hardware unsealing fallback is attempted and succeeds because +/// the hardware key protector was saved on first boot. The VM boots +/// normally and the AK cert remains accessible. +/// +/// The test function name contains `hw_unseal` so the per-VM agent +/// registry in test_igvm_agent_rpc_server matches it to the +/// `KeyReleaseFailure` configuration. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn hw_unseal( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(true) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // First boot: KEY_RELEASE succeeds. Verify AK cert is present. + let expected_hex = expected_ak_cert_hex(); + let ak_cert_output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + ak_cert_output.contains("AK certificate matches expected value"), + format!("{ak_cert_output}") + ); + + // Reboot: triggers second KEY_RELEASE which fails (plain failure, + // no skip_hw_unsealing signal). Hardware unsealing fallback kicks + // in and succeeds — the VM boots normally. + agent.reboot().await?; + let agent = vm.wait_for_reset().await?; + + // Verify AK cert is still accessible after the hw unsealing fallback. + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + let ak_cert_output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + ak_cert_output.contains("AK certificate matches expected value"), + "AK cert should still be accessible after hw unsealing fallback: {ak_cert_output}" + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// NV index used by the hardware sealing persistence tests. +#[cfg(windows)] +const TEST_NV_INDEX: &str = "0x1500016"; +/// Size of the test NV index in bytes. +#[cfg(windows)] +const TEST_NV_SIZE: &str = "64"; +/// Test data written to the NV index (hex). +#[cfg(windows)] +const TEST_NV_DATA_HEX: &str = "0xdeadbeefcafebabe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"; + +/// Test that hardware sealing with hash-based key derivation persists +/// TPM NV index data across reboots. +/// +/// Configuration: `no_persistent_secrets=true` (NoPersistentSecrets +/// isolation) with `HardwareSealedSecretsHashPolicy`. The VMGS is +/// encrypted using a hardware-sealed key derived from the measurement +/// hash. +/// +/// First boot: define NV index, write test data, read and verify. +/// Second boot: read the same NV index and verify data persisted. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn hw_seal_hash( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(false) + .with_hardware_sealing_policy(PetriHardwareSealingPolicy::HashPolicy) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // First boot: define NV index, write test data, read and verify. + let define_output = tpm_guest_tests + .nv_define(TEST_NV_INDEX, TEST_NV_SIZE) + .await?; + ensure!( + define_output.contains("defined successfully"), + "NV define should succeed: {define_output}" + ); + + let write_output = tpm_guest_tests + .nv_write(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + write_output.contains("succeeded"), + "NV write should succeed: {write_output}" + ); + + let read_output = tpm_guest_tests + .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + read_output.contains("matches expected value"), + "NV read should match on first boot: {read_output}" + ); + + // Reboot to test persistence. + agent.reboot().await?; + let agent = vm.wait_for_reset().await?; + + // Second boot: re-send the binary and verify NV data persisted. + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + let read_output = tpm_guest_tests + .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + read_output.contains("matches expected value"), + "NV data should persist across reboot with HashPolicy: {read_output}" + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Test that hardware sealing with signer-based key derivation persists +/// TPM NV index data across reboots. +/// +/// Same as `hw_seal_hash` but uses `HardwareSealedSecretsSignerPolicy` +/// instead. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn hw_seal_signer( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(false) + .with_hardware_sealing_policy(PetriHardwareSealingPolicy::SignerPolicy) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // First boot: define NV index, write test data, read and verify. + let define_output = tpm_guest_tests + .nv_define(TEST_NV_INDEX, TEST_NV_SIZE) + .await?; + ensure!( + define_output.contains("defined successfully"), + "NV define should succeed: {define_output}" + ); + + let write_output = tpm_guest_tests + .nv_write(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + write_output.contains("succeeded"), + "NV write should succeed: {write_output}" + ); + + let read_output = tpm_guest_tests + .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + read_output.contains("matches expected value"), + "NV read should match on first boot: {read_output}" + ); + + // Reboot to test persistence. + agent.reboot().await?; + let agent = vm.wait_for_reset().await?; + + // Second boot: re-send the binary and verify NV data persisted. + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + let read_output = tpm_guest_tests + .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) + .await?; + ensure!( + read_output.contains("matches expected value"), + "NV data should persist across reboot with SignerPolicy: {read_output}" + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} From a7b3430298ece5bed3fb8fea2ff91ae33cea6973 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 05:32:42 +0000 Subject: [PATCH 04/16] Revert "support statless + hw sealing" This reverts commit 7bdcc4db0fd601fe82005eabdb5d3ae9b790bcfd. --- .../src/igvm_attest/get.rs | 13 - .../openhcl_attestation_protocol/src/vmgs.rs | 42 +- openhcl/tee_call/Cargo.toml | 1 - openhcl/tee_call/src/lib.rs | 34 +- .../src/hardware_key_sealing.rs | 347 +-------- .../src/igvm_attest/mod.rs | 7 +- openhcl/underhill_attestation/src/lib.rs | 669 ++++-------------- openhcl/underhill_attestation/src/vmgs.rs | 8 +- openhcl/underhill_core/src/worker.rs | 44 +- petri/src/vm/hyperv/hyperv.psm1 | 15 - petri/src/vm/hyperv/mod.rs | 15 - petri/src/vm/hyperv/powershell.rs | 56 -- petri/src/vm/hyperv/vm.rs | 8 - petri/src/vm/mod.rs | 28 - petri/src/vm/openvmm/construct.rs | 1 - vm/devices/get/get_protocol/src/dps_json.rs | 28 +- vm/devices/get/get_protocol/src/lib.rs | 2 - vm/devices/get/get_resources/src/lib.rs | 3 - .../get/guest_emulation_device/src/lib.rs | 5 - .../guest_emulation_device/src/resolver.rs | 1 - .../src/test_utilities.rs | 1 - .../get/guest_emulation_transport/src/api.rs | 3 - .../guest_emulation_transport/src/client.rs | 1 - vm/devices/get/test_igvm_agent_lib/src/lib.rs | 9 - .../test_igvm_agent_rpc_server/src/main.rs | 3 - .../src/rpc/igvm_agent.rs | 20 +- vm/devices/tpm/tpm_guest_tests/src/main.rs | 169 ----- .../vmm_tests/tests/tests/multiarch/tpm.rs | 357 ---------- 28 files changed, 184 insertions(+), 1706 deletions(-) diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 73cc486963..7d65d65970 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -422,17 +422,6 @@ pub mod runtime_claims { } } - /// Supported hardware sealing policy - #[derive(Clone, Copy, Debug, Deserialize, Serialize, MeshPayload)] - pub enum HardwareSealingPolicy { - #[serde(rename = "none")] - None, - #[serde(rename = "hash")] - Hash, - #[serde(rename = "signer")] - Signer, - } - /// VM configuration to be included in the `RuntimeClaims`. #[derive(Clone, Debug, Deserialize, Serialize, MeshPayload)] #[serde(rename_all = "kebab-case")] @@ -452,8 +441,6 @@ pub mod runtime_claims { pub tpm_enabled: bool, /// Whether the TPM states is persisted pub tpm_persisted: bool, - /// Hardware sealing policy - pub hardware_sealing_policy: HardwareSealingPolicy, /// Whether certain vPCI devices are allowed through the device filter pub filtered_vpci_devices_allowed: bool, /// VM id diff --git a/openhcl/openhcl_attestation_protocol/src/vmgs.rs b/openhcl/openhcl_attestation_protocol/src/vmgs.rs index af58e93eff..0a72629705 100644 --- a/openhcl/openhcl_attestation_protocol/src/vmgs.rs +++ b/openhcl/openhcl_attestation_protocol/src/vmgs.rs @@ -74,14 +74,11 @@ pub struct SecurityProfile { pub agent_data: [u8; AGENT_DATA_MAX_SIZE], } -/// VMGS hardware key protector entry that includes the metadata of -/// local hardware sealing with AES-CBC-HMAC-SHA256. -/// -/// Version 1 is incompatible with newer versions. -/// Version 2 or newer is forward-compatible if header.mix_measurement is not set. -pub const HW_KEY_PROTECTOR_VERSION_1: u32 = 1; -pub const HW_KEY_PROTECTOR_VERSION_2: u32 = 2; -pub const HW_KEY_PROTECTOR_CURRENT_VERSION: u32 = HW_KEY_PROTECTOR_VERSION_2; +/// The header, IV, and last 256 bits of HMAC are fixed for this version. +/// The ciphertext is allowed to grow, though secrets should stay +/// in the same position to allow downlevel versions to continue to understand +/// that portion of the data. +pub const HW_KEY_VERSION: u32 = 1; // using AES-CBC-HMAC-SHA256 /// The size of the `FileId::HW_KEY_PROTECTOR` entry in the VMGS file. pub const HW_KEY_PROTECTOR_SIZE: usize = size_of::(); @@ -108,22 +105,18 @@ pub struct HardwareKeyProtectorHeader { pub length: u32, /// TCB version obtained from the hardware pub tcb_version: u64, - /// Whether to mix the measurement in hardware key derivation - /// Only supported in version 2 and above - pub mix_measurement: u8, - /// Reserved bytes for future use - pub _reserved: [u8; 7], + /// Reserved + pub _reserved: [u8; 8], } impl HardwareKeyProtectorHeader { /// Create a `HardwareKeyProtectorHeader` instance. - pub fn new(version: u32, length: u32, tcb_version: u64, mix_measurement: u8) -> Self { + pub fn new(version: u32, length: u32, tcb_version: u64) -> Self { Self { version, length, tcb_version, - mix_measurement, - _reserved: [0; 7], + _reserved: [0u8; 8], } } } @@ -138,7 +131,7 @@ pub struct HardwareKeyProtector { pub iv: [u8; AES_CBC_IV_LENGTH], /// Encrypted key pub ciphertext: [u8; AES_GCM_KEY_LENGTH], - /// HMAC-SHA-256 of [header, iv, ciphertext] + /// HMAC-SHA-256 of [`header`, `iv`, `ciphertext`] pub hmac: [u8; HMAC_SHA_256_KEY_LENGTH], } @@ -152,18 +145,3 @@ pub struct GuestSecretKey { /// the guest secret key to be provisioned to vTPM pub guest_secret_key: [u8; GUEST_SECRET_KEY_MAX_SIZE], } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hardware_key_protector_header_new() { - let h = HardwareKeyProtectorHeader::new(2, 104, 0x1234, 1); - assert_eq!(h.version, 2); - assert_eq!(h.length, 104); - assert_eq!(h.tcb_version, 0x1234); - assert_eq!(h.mix_measurement, 1); - assert_eq!(h._reserved, [0; 7]); - } -} diff --git a/openhcl/tee_call/Cargo.toml b/openhcl/tee_call/Cargo.toml index f62dff480f..b406d6db30 100644 --- a/openhcl/tee_call/Cargo.toml +++ b/openhcl/tee_call/Cargo.toml @@ -20,6 +20,5 @@ x86defs.workspace = true static_assertions.workspace = true thiserror.workspace = true zerocopy.workspace = true - [lints] workspace = true diff --git a/openhcl/tee_call/src/lib.rs b/openhcl/tee_call/src/lib.rs index 00a1115e3f..61a272fa95 100644 --- a/openhcl/tee_call/src/lib.rs +++ b/openhcl/tee_call/src/lib.rs @@ -65,23 +65,10 @@ pub struct GetAttestationReportResult { pub tcb_version: Option, } -/// Key derivation policy -#[derive(Debug, Clone, Copy)] -pub struct KeyDerivationPolicy { - /// The TCB version to use for key derivation. - pub tcb_version: u64, - /// Whether to mix measurement into the key derivation. - pub mix_measurement: bool, -} - /// Trait that defines the get attestation report interface for TEE. +// TODO VBS: Implement the trait for VBS pub trait TeeCall: Send + Sync { /// Get the hardware-backed attestation report. - /// - /// # Arguments - /// * `report_data` - The report data to include in the attestation report. - /// - /// Returns the attestation report result. fn get_attestation_report( &self, report_data: &[u8; REPORT_DATA_SIZE], @@ -93,18 +80,10 @@ pub trait TeeCall: Send + Sync { } /// Optional sub-trait that defines get derived key interface for TEE. -/// -/// # Arguments -/// * `policy` - The key derivation policy to use. -/// -/// Returns the derived key. pub trait TeeCallGetDerivedKey: TeeCall { /// Get the derived key that should be deterministic based on the hardware and software /// configurations. - fn get_derived_key( - &self, - policy: KeyDerivationPolicy, - ) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error>; + fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error>; } /// Implementation of [`TeeCall`] for SNP @@ -140,10 +119,7 @@ impl TeeCall for SnpCall { impl TeeCallGetDerivedKey for SnpCall { /// Get the derived key from /dev/sev-guest. - fn get_derived_key( - &self, - policy: KeyDerivationPolicy, - ) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error> { + fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; HW_DERIVED_KEY_LENGTH], Error> { let dev = sev_guest_device::SevGuestDevice::open().map_err(Error::OpenDevSevGuest)?; // Derive a key mixing in following data: @@ -152,7 +128,7 @@ impl TeeCallGetDerivedKey for SnpCall { // - TcbVersion (do not derive same key on older TCB that might have a bug) let guest_field_select = x86defs::snp::GuestFieldSelect::default() .with_guest_policy(true) - .with_measurement(policy.mix_measurement) + .with_measurement(true) .with_tcb_version(true); let derived_key = dev @@ -161,7 +137,7 @@ impl TeeCallGetDerivedKey for SnpCall { guest_field_select.into(), 0, // VMPL 0 0, // default guest svn to 0 - policy.tcb_version, + tcb_version, ) .map_err(Error::GetSnpDerivedKey)?; diff --git a/openhcl/underhill_attestation/src/hardware_key_sealing.rs b/openhcl/underhill_attestation/src/hardware_key_sealing.rs index f013165821..94937fe2c5 100644 --- a/openhcl/underhill_attestation/src/hardware_key_sealing.rs +++ b/openhcl/underhill_attestation/src/hardware_key_sealing.rs @@ -16,8 +16,6 @@ use zerocopy::IntoBytes; #[derive(Debug, Error)] pub(crate) enum HardwareDerivedKeysError { - #[error("key derivation policy does not match VM configuration")] - KeyDerivationPolicyMismatch, #[error("failed to initialize hardware secret")] InitializeHardwareSecret(#[source] tee_call::Error), #[error("KDF derivation with hardware secret failed")] @@ -43,47 +41,32 @@ pub(crate) enum HardwareKeySealingError { } /// Hold the hardware-derived keys. -#[derive(Debug)] pub struct HardwareDerivedKeys { - policy: tee_call::KeyDerivationPolicy, + tcb_version: u64, aes_key: [u8; vmgs::AES_CBC_KEY_LENGTH], hmac_key: [u8; vmgs::HMAC_SHA_256_KEY_LENGTH], } impl HardwareDerivedKeys { - /// Derive an AES and HMAC keys based on the hardware secret, VM configuration, and policy for key sealing. + /// Derive an AES and HMAC keys based on the hardware secret for key sealing. pub fn derive_key( tee_call: &dyn tee_call::TeeCallGetDerivedKey, vm_config: &igvm_attest::get::runtime_claims::AttestationVmConfig, - policy: tee_call::KeyDerivationPolicy, + tcb_version: u64, ) -> Result { - let mix_measurement_from_vm_config = matches!( - vm_config.hardware_sealing_policy, - igvm_attest::get::runtime_claims::HardwareSealingPolicy::Hash - ); - - // Policy is based on the VM configuration (`hardware_sealing_policy`) on the - // sealing path and on VMGS file (`HardwareKeyProtector`) on the unsealing path. - // On both paths, the policy must be consistent with the VM configuration. - // An inconsistency will cause mismatch in the key derivation function that takes - // VM configuration as input. - if policy.mix_measurement != mix_measurement_from_vm_config { - return Err(HardwareDerivedKeysError::KeyDerivationPolicyMismatch); - } - let hardware_secret = tee_call - .get_derived_key(policy) + .get_derived_key(tcb_version) .map_err(HardwareDerivedKeysError::InitializeHardwareSecret)?; let label = b"ISOHWKEY"; - let vm_config_json = serde_json::to_string(vm_config).expect("JSON serialization failed"); + let vm_config = serde_json::to_string(vm_config).expect("JSON serialization failed"); let mut kdf = Kbkdf::new( openssl::hash::MessageDigest::sha256(), label.to_vec(), hardware_secret.to_vec(), ); - kdf.set_context(vm_config_json.as_bytes().to_vec()); + kdf.set_context(vm_config.as_bytes().to_vec()); let mut output = [0u8; vmgs::AES_CBC_KEY_LENGTH + vmgs::HMAC_SHA_256_KEY_LENGTH]; openssl_kdf::kdf::derive(kdf, &mut output) @@ -96,7 +79,7 @@ impl HardwareDerivedKeys { hmac_key.copy_from_slice(&output[vmgs::AES_CBC_KEY_LENGTH..]); Ok(Self { - policy, + tcb_version, aes_key, hmac_key, }) @@ -124,10 +107,9 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { egress_key: &[u8], ) -> Result { let header = vmgs::HardwareKeyProtectorHeader::new( - vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION, + vmgs::HW_KEY_VERSION, vmgs::HW_KEY_PROTECTOR_SIZE as u32, - hardware_derived_keys.policy.tcb_version, - hardware_derived_keys.policy.mix_measurement as u8, + hardware_derived_keys.tcb_version, ); let mut iv = [0u8; vmgs::AES_CBC_IV_LENGTH]; @@ -200,16 +182,17 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { mod tests { use super::*; use crate::test_utils::MockTeeCall; - use igvm_attest::get::runtime_claims::AttestationVmConfig; - use igvm_attest::get::runtime_claims::HardwareSealingPolicy; use zerocopy::FromBytes; - const PLAINTEXT: [u8; 32] = [0xAB; 32]; - - fn create_test_vm_config( - hardware_sealing_policy: HardwareSealingPolicy, - ) -> AttestationVmConfig { - AttestationVmConfig { + #[test] + fn hardware_derived_keys() { + const PLAINTEXT: [u8; 32] = [ + 0x5e, 0xd7, 0xf3, 0xd4, 0x9e, 0xcf, 0xb5, 0x6c, 0x05, 0x54, 0x7c, 0x87, 0xe7, 0x30, + 0x59, 0xb1, 0x91, 0xcb, 0xa6, 0xc4, 0x0e, 0x4e, 0x30, 0x77, 0x65, 0x19, 0x71, 0xf5, + 0x20, 0x83, 0x2a, 0xc0, + ]; + + let vm_config = igvm_attest::get::runtime_claims::AttestationVmConfig { current_time: None, root_cert_thumbprint: "".to_string(), console_enabled: false, @@ -217,292 +200,30 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, - hardware_sealing_policy, filtered_vpci_devices_allowed: true, vm_unique_id: "".to_string(), - } - } - - #[test] - fn hardware_derived_keys_hash_policy() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let hardware_derived_keys = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: true, - }, - ) - .unwrap(); - - let output = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT).unwrap(); - let hardware_key_protector = HardwareKeyProtector::read_from_prefix(output.as_bytes()) - .unwrap() - .0; - let plaintext = hardware_key_protector - .unseal_key(&hardware_derived_keys) - .unwrap(); - assert_eq!(plaintext, PLAINTEXT); - } - - #[test] - fn hardware_derived_keys_signer_policy() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let k1 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: false, - }, - ) - .unwrap(); - let output = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); - let hardware_key_protector = HardwareKeyProtector::read_from_prefix(output.as_bytes()) - .unwrap() - .0; - - // Unseal should succeed with different measurements when using signer policy - let mock_tee_call = Box::new(MockTeeCall::new([0x8bu8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let k2 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: false, - }, - ) - .unwrap(); - let plaintext = hardware_key_protector.unseal_key(&k2).unwrap(); - assert_eq!(plaintext, PLAINTEXT); - } - - #[test] - fn hardware_derived_keys_policy_mismatch() { - { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = - Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - - let result = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: false, - }, - ); - assert!(result.is_err()); - let err = result.unwrap_err(); - matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); - } - - { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); - let mock_tee_call = - Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - - let result = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: true, - }, - ); - assert!(result.is_err()); - let err = result.unwrap_err(); - matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); - } - } - - #[test] - fn hardware_key_protector_header_fields_set() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let policy = tee_call::KeyDerivationPolicy { - tcb_version: 0xDEAD_BEEF, - mix_measurement: false, }; - let k = - HardwareDerivedKeys::derive_key(mock_get_derived_key_call, &vm_config, policy).unwrap(); - let hwkp = HardwareKeyProtector::seal_key(&k, &PLAINTEXT).unwrap(); - - assert_eq!(hwkp.header.tcb_version, policy.tcb_version); - assert_eq!(hwkp.header.mix_measurement, policy.mix_measurement as u8); - assert_eq!(hwkp.header.length as usize, vmgs::HW_KEY_PROTECTOR_SIZE); - assert_eq!(hwkp.header.version, vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION); - } - - #[test] - fn unseal_key_fails_when_original_plaintext_not_32() { - // With CBC and no padding enabled, sealing must fail for non-16-aligned sizes. - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let k = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 2, - mix_measurement: true, - }, - ) - .unwrap(); - - let plaintext = [0x7Au8; 20]; - let err = HardwareKeyProtector::seal_key(&k, &plaintext) - .expect_err("expected seal to fail for non-block-multiple length"); - matches!(err, HardwareKeySealingError::EncryptEgressKey(_)); - } - - #[test] - fn hardware_key_protector_hmac_mismatch_detected() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let hardware_derived_keys = HardwareDerivedKeys::derive_key( + let mock_call = Box::new(MockTeeCall::new(0x1234)) as Box; + let mock_get_derived_key_call = mock_call.supports_get_derived_key().unwrap(); + let result = HardwareDerivedKeys::derive_key( mock_get_derived_key_call, &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x7308000000000003, - mix_measurement: true, - }, - ) - .unwrap(); - - let mut hwkp = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT).unwrap(); - - // Corrupt the HMAC to force verification failure - hwkp.hmac[0] ^= 0xFF; - - let err = hwkp - .unseal_key(&hardware_derived_keys) - .expect_err("expected HMAC verification to fail"); - - matches!( - err, - HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed + 0x7308000000000003, ); - } + assert!(result.is_ok()); + let hardware_derived_keys = result.unwrap(); - #[test] - fn unseal_fails_with_different_policy_mix_measurement() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); + let result = HardwareKeyProtector::seal_key(&hardware_derived_keys, &PLAINTEXT); + assert!(result.is_ok()); + let output = result.unwrap(); - let k1: HardwareDerivedKeys = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x1, - mix_measurement: true, - }, - ) - .unwrap(); - let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); - - let vm_config = create_test_vm_config(HardwareSealingPolicy::Signer); - let k2 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0x1, - mix_measurement: false, - }, - ) - .unwrap(); + let result = HardwareKeyProtector::read_from_prefix(output.as_bytes()); + assert!(result.is_ok()); + let hardware_key_protector = result.unwrap().0; - let err = hwkp - .unseal_key(&k2) - .expect_err("mix_measurement policy change should break unseal"); - matches!( - err, - HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); - } - - #[test] - fn unseal_fails_with_different_tcb_version() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - - let k1 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0xAAAAAAAAAAAAAAAA, - mix_measurement: true, - }, - ) - .unwrap(); - let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); - - let k2 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0xBBBBBBBBBBBBBBBB, - mix_measurement: true, - }, - ) - .unwrap(); - - let err = hwkp - .unseal_key(&k2) - .expect_err("TCB change should break unseal"); - matches!( - err, - HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); - } - - #[test] - fn unseal_fails_with_different_measurements() { - let vm_config = create_test_vm_config(HardwareSealingPolicy::Hash); - let mock_tee_call = Box::new(MockTeeCall::new([0x7au8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - - let k1 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0xAAAAAAAAAAAAAAAA, - mix_measurement: true, - }, - ) - .unwrap(); - let hwkp = HardwareKeyProtector::seal_key(&k1, &PLAINTEXT).unwrap(); - - let mock_tee_call = Box::new(MockTeeCall::new([0x8bu8; 32])) as Box; - let mock_get_derived_key_call = mock_tee_call.supports_get_derived_key().unwrap(); - let k2 = HardwareDerivedKeys::derive_key( - mock_get_derived_key_call, - &vm_config, - tee_call::KeyDerivationPolicy { - tcb_version: 0xAAAAAAAAAAAAAAAA, - mix_measurement: true, - }, - ) - .unwrap(); - - let err = hwkp - .unseal_key(&k2) - .expect_err("measurement change should break unseal"); - matches!( - err, - HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); + let result = hardware_key_protector.unseal_key(&hardware_derived_keys); + assert!(result.is_ok()); + let plaintext = result.unwrap(); + assert_eq!(plaintext, PLAINTEXT); } } diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index d86a11afcc..132a84b53f 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -354,7 +354,6 @@ fn runtime_claims_to_bytes( #[cfg(test)] mod tests { use super::*; - use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; #[test] fn test_create_request() { @@ -492,7 +491,7 @@ mod tests { #[test] fn test_vm_configuration_no_time() { - const EXPECTED_JWK: &str = r#"{"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"hardware-sealing-policy":"signer","filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; + const EXPECTED_JWK: &str = r#"{"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; let attestation_vm_config = AttestationVmConfig { current_time: None, @@ -502,7 +501,6 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, - hardware_sealing_policy: HardwareSealingPolicy::Signer, filtered_vpci_devices_allowed: true, vm_unique_id: String::new(), }; @@ -515,7 +513,7 @@ mod tests { #[test] fn test_vm_configuration_with_time() { - const EXPECTED_JWK: &str = r#"{"current-time":1691103220,"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"hardware-sealing-policy":"hash","filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; + const EXPECTED_JWK: &str = r#"{"current-time":1691103220,"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#; let attestation_vm_config = AttestationVmConfig { current_time: None, @@ -525,7 +523,6 @@ mod tests { secure_boot: false, tpm_enabled: false, tpm_persisted: false, - hardware_sealing_policy: HardwareSealingPolicy::Hash, filtered_vpci_devices_allowed: true, vm_unique_id: String::new(), }; diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index 6fe981c9f2..a277076e02 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -24,7 +24,6 @@ pub use igvm_attest::Error as IgvmAttestError; pub use igvm_attest::IgvmAttestRequestHelper; pub use igvm_attest::ak_cert::parse_response as parse_ak_cert_response; -use crate::hardware_key_sealing::HardwareKeySealingError; use ::vmgs::EncryptionAlgorithm; use ::vmgs::GspType; use ::vmgs::Vmgs; @@ -41,13 +40,8 @@ use key_protector::GetKeysFromKeyProtectorError; use key_protector::KeyProtectorExt as _; use mesh::MeshPayload; use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; -use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; -use openhcl_attestation_protocol::vmgs::AES_CBC_KEY_LENGTH; use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH; use openhcl_attestation_protocol::vmgs::AGENT_DATA_MAX_SIZE; -use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION; -use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_VERSION_1; -use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_VERSION_2; use openhcl_attestation_protocol::vmgs::HardwareKeyProtector; use openhcl_attestation_protocol::vmgs::KeyProtector; use openhcl_attestation_protocol::vmgs::SecurityProfile; @@ -57,8 +51,6 @@ use pal_async::local::LocalDriver; use secure_key_release::VmgsEncryptionKeys; use static_assertions::const_assert_eq; use std::fmt::Debug; -use tee_call::KeyDerivationPolicy; -use tee_call::REPORT_DATA_SIZE; use tee_call::TeeCall; use thiserror::Error; use zerocopy::FromZeros; @@ -89,8 +81,6 @@ enum AttestationErrorInner { UnlockVmgsDataStore(#[source] UnlockVmgsDataStoreError), #[error("failed to read guest secret key from vmgs")] ReadGuestSecretKey(#[source] vmgs::ReadFromVmgsError), - #[error("failed to get an attestation report")] - GetAttestationReport(#[source] tee_call::Error), } #[derive(Debug, Error)] @@ -104,9 +94,9 @@ enum GetDerivedKeysError { #[error("GSP By Id required, but no GSP By Id found")] GspByIdRequiredButNotFound, #[error("failed to unseal the ingress key using hardware derived keys")] - UnsealIngressKeyUsingHardwareDerivedKeys(#[source] HardwareKeySealingError), - #[error("failed to get an ingress key from hardware key protector")] - GetIngressKeyFromHardwareKeyProtectorFailed, + UnsealIngressKeyUsingHardwareDerivedKeys( + #[source] hardware_key_sealing::HardwareKeySealingError, + ), #[error("failed to get an ingress key from key protector")] GetIngressKeyFromKpFailed, #[error("failed to get an ingress key from guest state protection")] @@ -118,7 +108,7 @@ enum GetDerivedKeysError { #[error("VMGS encryption is required, but no encryption sources were found")] EncryptionRequiredButNotFound, #[error("failed to seal the egress key using hardware derived keys")] - SealEgressKeyUsingHardwareDerivedKeys(#[source] HardwareKeySealingError), + SealEgressKeyUsingHardwareDerivedKeys(#[source] hardware_key_sealing::HardwareKeySealingError), #[error("failed to write to `FileId::HW_KEY_PROTECTOR` in vmgs")] VmgsWriteHardwareKeyProtector(#[source] vmgs::WriteToVmgsError), #[error("failed to get derived key by id")] @@ -129,8 +119,6 @@ enum GetDerivedKeysError { DeriveEgressKey(#[source] crypto::KbkdfError), #[error("skipped hardware unsealing for VMGS DEK as signaled by IGVM agent")] HardwareUnsealingSkipped, - #[error("Hardware sealing is required, but not supported")] - HardwareSealingRequiredButNotSupported, } #[derive(Debug, Error)] @@ -170,11 +158,9 @@ enum UnlockVmgsDataStoreError { #[derive(Debug, Error)] enum PersistAllKeyProtectorsError { #[error("failed to write key protector to vmgs")] - KeyProtector(#[source] vmgs::WriteToVmgsError), + WriteKeyProtector(#[source] vmgs::WriteToVmgsError), #[error("failed to read key protector by id to vmgs")] - KeyProtectorById(#[source] vmgs::WriteToVmgsError), - #[error("failed to write hardware key protector to vmgs")] - HardwareKeyProtector(#[source] vmgs::WriteToVmgsError), + WriteKeyProtectorById(#[source] vmgs::WriteToVmgsError), } // Operation types for provisioning telemetry. @@ -232,8 +218,6 @@ struct DerivedKeyResult { key_protector_settings: KeyProtectorSettings, /// The instance of [`GspExtendedStatusFlags`] returned by GSP. gsp_extended_status_flags: GspExtendedStatusFlags, - /// Optional hardware key protector. - hardware_key_protector: Option, } /// The return values of [`initialize_platform_security`]. @@ -247,6 +231,7 @@ pub struct PlatformAttestationData { } /// The attestation type to use. +// TODO: Support VBS #[derive(Debug, MeshPayload, Copy, Clone, PartialEq, Eq)] pub enum AttestationType { /// Use the SEV-SNP TEE for attestation. @@ -271,38 +256,21 @@ async fn try_unlock_vmgs( tee_call: Option<&dyn TeeCall>, guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, - require_hardware_sealing: bool, agent_data: &mut [u8; AGENT_DATA_MAX_SIZE], key_protector_by_id: &mut KeyProtectorById, ) -> Result { let skr_response = if let Some(tee_call) = tee_call { - if !require_hardware_sealing { - tracing::info!(CVM_ALLOWED, "Retrieving key-encryption key"); - // Retrieve the tenant key via attestation - secure_key_release::request_vmgs_encryption_keys( - get, - tee_call, - vmgs, - attestation_vm_config, - agent_data, - ) - .await - } else { - tracing::info!( - CVM_ALLOWED, - "Getting attestation report only for hardware sealing" - ); - - let report = tee_call - .get_attestation_report(&[0; REPORT_DATA_SIZE]) - .map_err(|e| (AttestationErrorInner::GetAttestationReport(e), false))?; + tracing::info!(CVM_ALLOWED, "Retrieving key-encryption key"); - Ok(VmgsEncryptionKeys { - ingress_rsa_kek: None, - wrapped_des_key: None, - tcb_version: report.tcb_version, - }) - } + // Retrieve the tenant key via attestation + secure_key_release::request_vmgs_encryption_keys( + get, + tee_call, + vmgs, + attestation_vm_config, + agent_data, + ) + .await } else { tracing::info!(CVM_ALLOWED, "Key-encryption key retrieval not required"); @@ -351,53 +319,27 @@ async fn try_unlock_vmgs( } }; - let mut key_protector = if !require_hardware_sealing { - // Determine the minimal size of a DEK entry based on whether `wrapped_des_key` presents - let dek_minimal_size = if wrapped_des_key.is_some() { - key_protector::AES_WRAPPED_AES_KEY_LENGTH - } else { - key_protector::RSA_WRAPPED_AES_KEY_LENGTH - }; - - // Read Key Protector blob from VMGS - tracing::info!( - CVM_ALLOWED, - dek_minimal_size = dek_minimal_size, - "Reading key protector from VMGS" - ); - - vmgs::read_key_protector(vmgs, dek_minimal_size) - .await - .map_err(|e| (AttestationErrorInner::ReadKeyProtector(e), false))? + // Determine the minimal size of a DEK entry based on whether `wrapped_des_key` presents + let dek_minimal_size = if wrapped_des_key.is_some() { + key_protector::AES_WRAPPED_AES_KEY_LENGTH } else { - tracing::info!( - CVM_ALLOWED, - "Hardware sealing is required, skip reading key protector from VMGS" - ); - KeyProtector::new_zeroed() + key_protector::RSA_WRAPPED_AES_KEY_LENGTH }; + // Read Key Protector blob from VMGS + tracing::info!( + CVM_ALLOWED, + dek_minimal_size = dek_minimal_size, + "Reading key protector from VMGS" + ); + let mut key_protector = vmgs::read_key_protector(vmgs, dek_minimal_size) + .await + .map_err(|e| (AttestationErrorInner::ReadKeyProtector(e), false))?; + let start_time = std::time::SystemTime::now(); let vmgs_encrypted = vmgs.encrypted(); - - // Determine mix_measurement based on hardware sealing policy: - // - None: false (no hardware sealing) - // - Hash: true (mix measurement for strong binding) - // - Signer: false (use signer-based policy only) - let mix_measurement = match attestation_vm_config.hardware_sealing_policy { - HardwareSealingPolicy::None => false, - HardwareSealingPolicy::Hash => true, - HardwareSealingPolicy::Signer => false, - }; - - let key_derivation_policy = tcb_version.map(|tcb_version| KeyDerivationPolicy { - tcb_version, - mix_measurement, - }); - tracing::info!( - CVM_ALLOWED, - key_derivation_policy=?key_derivation_policy, + ?tcb_version, vmgs_encrypted, op_type = ?LogOpType::BeginDecryptVmgs, "Deriving keys" @@ -414,10 +356,9 @@ async fn try_unlock_vmgs( vmgs_encrypted, ingress_rsa_kek.as_ref(), wrapped_des_key.as_deref(), - key_derivation_policy, + tcb_version, guest_state_encryption_policy, strict_encryption_policy, - require_hardware_sealing, skip_hw_unsealing, ) .await @@ -435,14 +376,13 @@ async fn try_unlock_vmgs( (AttestationErrorInner::GetDerivedKeys(e), retry) })?; + // All Underhill VMs use VMGS encryption tracing::info!("Unlocking VMGS"); - if let Err(e) = unlock_vmgs_data_store( vmgs, vmgs_encrypted, &mut key_protector, key_protector_by_id, - derived_keys_result.hardware_key_protector, derived_keys_result.derived_keys, derived_keys_result.key_protector_settings, bios_guid, @@ -517,20 +457,9 @@ pub async fn initialize_platform_security( .await .map_err(AttestationErrorInner::ReadSecurityProfile)?; - let require_hardware_sealing = tee_call.is_some() - && matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::HardwareSealing - ) - && !matches!( - attestation_vm_config.hardware_sealing_policy, - HardwareSealingPolicy::None - ); - - // Attestation is suppressed and `guest_state_encryption_policy` is not - // `HardwareSealing` indicates that VMGS encryption is bypassed. Skip the attestation flow - // and return the `agent_data` that is required by TPM AK cert request. - if suppress_attestation && !require_hardware_sealing { + // If attestation is suppressed, return the `agent_data` that is required by + // TPM AK cert request. + if suppress_attestation { tracing::info!(CVM_ALLOWED, "Suppressing attestation"); return Ok(PlatformAttestationData { @@ -542,44 +471,31 @@ pub async fn initialize_platform_security( }); } - let (mut key_protector_by_id, vm_id_changed) = if !require_hardware_sealing { - // Read VM id from VMGS - tracing::info!(CVM_ALLOWED, "Reading VM ID from VMGS"); - let key_protector_by_id = match vmgs::read_key_protector_by_id(vmgs).await { - Ok(key_protector_by_id) => KeyProtectorById { - inner: key_protector_by_id, - found_id: true, - }, - Err(vmgs::ReadFromVmgsError::EntryNotFound(_)) => KeyProtectorById { - inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), - found_id: false, - }, - Err(e) => { Err(AttestationErrorInner::ReadKeyProtectorById(e)) }?, - }; + // Read VM id from VMGS + tracing::info!(CVM_ALLOWED, "Reading VM ID from VMGS"); + let mut key_protector_by_id = match vmgs::read_key_protector_by_id(vmgs).await { + Ok(key_protector_by_id) => KeyProtectorById { + inner: key_protector_by_id, + found_id: true, + }, + Err(vmgs::ReadFromVmgsError::EntryNotFound(_)) => KeyProtectorById { + inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), + found_id: false, + }, + Err(e) => { Err(AttestationErrorInner::ReadKeyProtectorById(e)) }?, + }; - // Check if the VM id has been changed since last boot with KP write - let vm_id_changed = if key_protector_by_id.found_id { - let changed = key_protector_by_id.inner.id_guid != bios_guid; - if changed { - tracing::info!("VM Id has changed since last boot"); - }; - changed - } else { - // Previous id in KP not found means this is the first boot or the GspById - // is not provisioned, treat id as unchanged for this case. - false + // Check if the VM id has been changed since last boot with KP write + let vm_id_changed = if key_protector_by_id.found_id { + let changed = key_protector_by_id.inner.id_guid != bios_guid; + if changed { + tracing::info!("VM Id has changed since last boot"); }; - - (key_protector_by_id, vm_id_changed) + changed } else { - // When hardware sealing is required, the key protector by id is not used, and VM id change does not trigger state refresh. - ( - KeyProtectorById { - inner: openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed(), - found_id: false, - }, - false, - ) + // Previous id in KP not found means this is the first boot or the GspById + // is not provisioned, treat id as unchanged for this case. + false }; // Retry attestation call-out if necessary (if VMGS encrypted). @@ -606,7 +522,6 @@ pub async fn initialize_platform_security( tee_call, guest_state_encryption_policy, strict_encryption_policy, - require_hardware_sealing, &mut agent_data, &mut key_protector_by_id, ) @@ -663,7 +578,6 @@ async fn unlock_vmgs_data_store( vmgs_encrypted: bool, key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, - hardware_key_protector: Option, derived_keys: Option, key_protector_settings: KeyProtectorSettings, bios_guid: Guid, @@ -750,7 +664,6 @@ async fn unlock_vmgs_data_store( vmgs, key_protector, key_protector_by_id, - hardware_key_protector.as_ref(), bios_guid, key_protector_settings, ) @@ -785,10 +698,9 @@ async fn get_derived_keys( is_encrypted: bool, ingress_rsa_kek: Option<&Rsa>, wrapped_des_key: Option<&[u8]>, - key_derivation_policy: Option, + tcb_version: Option, guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, - require_hardware_sealing: bool, skip_hw_unsealing: bool, ) -> Result { tracing::info!( @@ -798,6 +710,14 @@ async fn get_derived_keys( "encryption policy" ); + // TODO: implement hardware sealing only + if matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::HardwareSealing + ) { + todo!("hardware sealing") + } + let mut key_protector_settings = KeyProtectorSettings { should_write_kp: true, use_gsp_by_id: false, @@ -963,11 +883,7 @@ async fn get_derived_keys( }; // If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key - if (no_kek && found_dek) - || (no_gsp && requires_gsp) - || (no_gsp_by_id && requires_gsp_by_id) - || (require_hardware_sealing && is_encrypted) - { + if (no_kek && found_dek) || (no_gsp && requires_gsp) || (no_gsp_by_id && requires_gsp_by_id) { // If possible, get ingressKey from hardware sealed data let (hardware_key_protector, hardware_derived_keys) = if skip_hw_unsealing { tracing::warn!( @@ -976,8 +892,7 @@ async fn get_derived_keys( ); get.event_log_fatal( guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED, - ) - .await; + ); return Err(GetDerivedKeysError::HardwareUnsealingSkipped); } else if let Some(tee_call) = tee_call { @@ -996,48 +911,17 @@ async fn get_derived_keys( let hardware_derived_keys = tee_call.supports_get_derived_key().and_then(|tee_call| { if let Some(hardware_key_protector) = &hardware_key_protector { - let policy = match hardware_key_protector.header.version { - HW_KEY_PROTECTOR_VERSION_1 => { - // Version 1 is not forward compatible with other versions because it always mixes the OpenHCL - // measurement into the hardware key derivation function (KDF). This means that any version - // change implying an OpenHCL measurement change will result in a different hardware sealing key, - // causing the unsealing process to fail. To prevent this issue, we return None here and log - // the appropriate information. - // - // NOTE: In future implementations, we should handle version 2 and above differently. - // These versions support forward compatibility when using signer-based sealing policy that - // does not mix the OpenHCL measurement into the hardware KDF. - tracing::error!( - CVM_ALLOWED, - current_version = HW_KEY_PROTECTOR_CURRENT_VERSION, - "HW_KEY_PROTECTOR version 1 is incompatible with newer versions. Skip VMGS DEK unsealing with hardware key protector." - ); - return None; - } - HW_KEY_PROTECTOR_VERSION_2 => KeyDerivationPolicy { - tcb_version: hardware_key_protector.header.tcb_version, - mix_measurement: hardware_key_protector.header.mix_measurement == 1, - }, - unsupported_version => { - // unsupported version - tracing::warn!( - CVM_ALLOWED, - unsupported_version, - "unsupported HW_KEY_PROTECTOR version", - ); - return None; - } - }; - - match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, policy) { + match HardwareDerivedKeys::derive_key( + tee_call, + attestation_vm_config, + hardware_key_protector.header.tcb_version, + ) { Ok(hardware_derived_key) => Some(hardware_derived_key), Err(e) => { // non-fatal tracing::warn!( CVM_ALLOWED, error = &e as &dyn std::error::Error, - tcb_version = hardware_key_protector.header.tcb_version, - mix_measurement = hardware_key_protector.header.mix_measurement, "failed to derive hardware keys using HW_KEY_PROTECTOR", ); None @@ -1056,78 +940,27 @@ async fn get_derived_keys( if let (Some(hardware_key_protector), Some(hardware_derived_keys)) = (hardware_key_protector, hardware_derived_keys) { - let dek = match hardware_key_protector.unseal_key(&hardware_derived_keys) { - Ok(dek) => dek, - Err(e @ HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed) - if require_hardware_sealing => - { - tracing::error!( - CVM_ALLOWED, - "hardware unsealing failed due to inconsistent hardware-derived keys" - ); - - get.event_log_fatal( - guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_INVALID_KEY, - ) - .await; - - return Err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys(e)); - } - Err(e) => { - return Err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys(e)); - } - }; - - derived_keys.ingress = dek; + derived_keys.ingress = hardware_key_protector + .unseal_key(&hardware_derived_keys) + .map_err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys)?; derived_keys.decrypt_egress = None; - - let hardware_key_protector = if require_hardware_sealing && is_encrypted { - // Generate a new key on every boot for key rotation - let mut new_dek = [0u8; AES_CBC_KEY_LENGTH]; - getrandom::fill(&mut new_dek).expect("rng failure"); - - let updated_hardware_key_protector = - HardwareKeyProtector::seal_key(&hardware_derived_keys, &new_dek) - .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; - - derived_keys.encrypt_egress = new_dek; - - tracing::info!( - CVM_ALLOWED, - "Non-first boot with VMGS hardware sealing mode. Generate a new random key for VMGS DEK rotation." - ); - - // Use the updated key protector in the exclusive hardware sealing scenario - // to support per-boot key rotation - updated_hardware_key_protector - } else { - derived_keys.encrypt_egress = derived_keys.ingress; - - tracing::warn!( - CVM_ALLOWED, - "Using hardware-derived key to recover VMGS DEK" - ); - - // Use the same key protector in the VMGS DEK backup scenario - hardware_key_protector - }; + derived_keys.encrypt_egress = derived_keys.ingress; key_protector_settings.should_write_kp = false; + key_protector_settings.use_hardware_unlock = true; + + tracing::warn!( + CVM_ALLOWED, + "Using hardware-derived key to recover VMGS DEK" + ); return Ok(DerivedKeyResult { derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: Some(hardware_key_protector), }); } else { - if require_hardware_sealing && is_encrypted { - get.event_log_fatal( - guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, - ) - .await; - return Err(GetDerivedKeysError::GetIngressKeyFromHardwareKeyProtectorFailed); - } else if no_kek && found_dek { + if no_kek && found_dek { return Err(GetDerivedKeysError::GetIngressKeyFromKpFailed); } else if no_gsp && requires_gsp { return Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed); @@ -1145,76 +978,9 @@ async fn get_derived_keys( gsp = !no_gsp, gsp_by_id_available = ?gsp_by_id_available, gsp_by_id = !no_gsp_by_id, - hw_sealing = require_hardware_sealing, "Encryption sources" ); - // Attempt to get hardware derived keys - let hardware_derived_keys = tee_call - .and_then(|tee_call| tee_call.supports_get_derived_key()) - .and_then(|tee_call| { - if let Some(policy) = key_derivation_policy { - match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, policy) { - Ok(keys) => Some(keys), - Err(e) => { - // non-fatal - tracing::warn!( - CVM_ALLOWED, - error = &e as &dyn std::error::Error, - "failed to derive hardware keys" - ); - None - } - } - } else { - None - } - }); - - // Let hardware sealing take precedence over other sources if it's required - if require_hardware_sealing && !is_encrypted { - let Some(hardware_derived_keys) = hardware_derived_keys else { - get.event_log_fatal( - guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, - ) - .await; - return Err(GetDerivedKeysError::HardwareSealingRequiredButNotSupported); - }; - - let mut new_dek = [0u8; AES_CBC_KEY_LENGTH]; - getrandom::fill(&mut new_dek).expect("rng failure"); - - let hardware_key_protector = - match HardwareKeyProtector::seal_key(&hardware_derived_keys, &new_dek) { - Ok(hardware_key_protector) => hardware_key_protector, - Err(e) => { - get.event_log_fatal( - guest_emulation_transport::api::EventLogId::DEK_HARDWARE_SEALING_FAILED, - ) - .await; - return Err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys( - e, - )); - } - }; - - derived_keys.ingress = [0u8; AES_GCM_KEY_LENGTH]; - derived_keys.decrypt_egress = None; - derived_keys.encrypt_egress = new_dek; - - tracing::info!( - CVM_ALLOWED, - "First boot with VMGS hardware sealing mode. Generate a new random key for VMGS encryption." - ); - - return Ok(DerivedKeyResult { - derived_keys: Some(derived_keys), - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: Some(hardware_key_protector), - }); - } - // Check if sources of encryption are available if no_kek && no_gsp && no_gsp_by_id { if is_encrypted { @@ -1234,12 +1000,34 @@ async fn get_derived_keys( derived_keys: None, key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: None, }); } } } + // Attempt to get hardware derived keys + let hardware_derived_keys = tee_call + .and_then(|tee_call| tee_call.supports_get_derived_key()) + .and_then(|tee_call| { + if let Some(tcb_version) = tcb_version { + match HardwareDerivedKeys::derive_key(tee_call, attestation_vm_config, tcb_version) + { + Ok(keys) => Some(keys), + Err(e) => { + // non-fatal + tracing::warn!( + CVM_ALLOWED, + error = &e as &dyn std::error::Error, + "failed to derive hardware keys" + ); + None + } + } + } else { + None + } + }); + // Use tenant key (KEK only) if no_gsp && no_gsp_by_id { tracing::info!(CVM_ALLOWED, "No GSP used with SKR"); @@ -1265,7 +1053,6 @@ async fn get_derived_keys( derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: None, }); } @@ -1305,7 +1092,6 @@ async fn get_derived_keys( derived_keys: Some(derived_keys_by_id), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: None, }); } @@ -1461,7 +1247,6 @@ async fn get_derived_keys( derived_keys: Some(derived_keys), key_protector_settings, gsp_extended_status_flags: gsp_response.extended_status_flags, - hardware_key_protector: None, }) } @@ -1558,7 +1343,6 @@ async fn persist_all_key_protectors( vmgs: &mut Vmgs, key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, - hardware_key_protector: Option<&HardwareKeyProtector>, bios_guid: Guid, key_protector_settings: KeyProtectorSettings, ) -> Result<(), PersistAllKeyProtectorsError> { @@ -1567,14 +1351,10 @@ async fn persist_all_key_protectors( if key_protector_settings.use_gsp_by_id && !key_protector_settings.should_write_kp { vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, false, bios_guid) .await - .map_err(PersistAllKeyProtectorsError::KeyProtectorById)?; + .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; } else { // If HW Key unlocked VMGS, do not alter KP - if let Some(hardware_key_protector) = hardware_key_protector { - vmgs::write_hardware_key_protector(hardware_key_protector, vmgs) - .await - .map_err(PersistAllKeyProtectorsError::HardwareKeyProtector)?; - } else { + if !key_protector_settings.use_hardware_unlock { // Remove ingress KP & DEK, no longer applies to data store key_protector.dek[key_protector.active_kp as usize % NUMBER_KP] .dek_buffer @@ -1584,7 +1364,7 @@ async fn persist_all_key_protectors( vmgs::write_key_protector(key_protector, vmgs) .await - .map_err(PersistAllKeyProtectorsError::KeyProtector)?; + .map_err(PersistAllKeyProtectorsError::WriteKeyProtector)?; } // Update Id data to indicate this scheme is no longer in use @@ -1595,7 +1375,7 @@ async fn persist_all_key_protectors( key_protector_by_id.inner.ported = 1; vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, true, bios_guid) .await - .map_err(PersistAllKeyProtectorsError::KeyProtectorById)?; + .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; } } @@ -1607,7 +1387,6 @@ async fn persist_all_key_protectors( pub mod test_utils { use tee_call::GetAttestationReportResult; use tee_call::HW_DERIVED_KEY_LENGTH; - use tee_call::KeyDerivationPolicy; use tee_call::REPORT_DATA_SIZE; use tee_call::TeeCall; use tee_call::TeeCallGetDerivedKey; @@ -1615,24 +1394,14 @@ pub mod test_utils { /// Mock implementation of [`TeeCall`] with get derived key support for testing purposes pub struct MockTeeCall { - /// Mock measurement data - pub measurement: [u8; 32], - /// Mock TCB version returned in attestation reports + /// Mock TCB version to return from get_attestation_report pub tcb_version: u64, } impl MockTeeCall { /// Create a new instance of [`MockTeeCall`]. - pub fn new(measurement: [u8; 32]) -> Self { - Self { - measurement, - tcb_version: 0x1234, - } - } - - /// Update the mock measurement data. - pub fn update_measurement(&mut self, measurement: [u8; 32]) { - self.measurement = measurement; + pub fn new(tcb_version: u64) -> Self { + Self { tcb_version } } } @@ -1662,21 +1431,14 @@ pub mod test_utils { } impl TeeCallGetDerivedKey for MockTeeCall { - fn get_derived_key( - &self, - policy: KeyDerivationPolicy, - ) -> Result<[u8; 32], tee_call::Error> { + fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; 32], tee_call::Error> { // Base test key; mix in policy so different policies yield different derived secrets let mut key: [u8; HW_DERIVED_KEY_LENGTH] = [0xab; HW_DERIVED_KEY_LENGTH]; // Use mutation to simulate the policy - let tcb = policy.tcb_version.to_le_bytes(); + let tcb = tcb_version.to_le_bytes(); for (i, b) in key.iter_mut().enumerate() { - if policy.mix_measurement { - *b ^= self.measurement[i] ^ tcb[i % tcb.len()]; - } else { - *b ^= tcb[i % tcb.len()]; - } + *b ^= tcb[i % tcb.len()]; } Ok(key) @@ -1881,7 +1643,6 @@ mod tests { secure_boot: false, tpm_enabled: true, tpm_persisted: true, - hardware_sealing_policy: HardwareSealingPolicy::None, filtered_vpci_devices_allowed: false, vm_unique_id: String::new(), } @@ -1910,7 +1671,6 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, - None, key_protector_settings, bios_guid, ) @@ -1936,7 +1696,6 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, - None, key_protector_settings, bios_guid, ) @@ -1979,7 +1738,6 @@ mod tests { false, &mut key_protector, &mut key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2036,7 +1794,6 @@ mod tests { true, &mut new_key_protector, &mut new_key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2105,7 +1862,6 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2180,7 +1936,6 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2260,7 +2015,6 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2315,7 +2069,6 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, - None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2403,7 +2156,6 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, - Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2418,178 +2170,6 @@ mod tests { assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); } - #[async_test] - async fn hardware_sealing_first_boot_creates_hwkp_and_encrypts_vmgs(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - // Start with an empty KP to simulate brand-new VMGS with no DEK/GSP present - let mut key_protector = KeyProtector::new_zeroed(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - let bios_guid = Guid::new_random(); - - // Create a GET client backed by the test host - let get_pair = guest_emulation_transport::test_utilities::new_transport_pair( - driver, - None, - get_protocol::ProtocolVersion::NICKEL_REV2, - None, - None, - ) - .await; - - let mock_tee_call = MockTeeCall::new([0x8a; 32]); - - // No KEK, no GSP. Require HardwareSealing and VMGS is not encrypted. - let derived = get_derived_keys( - &get_pair.client, - Some(&mock_tee_call), - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - &AttestationVmConfig { - current_time: None, - root_cert_thumbprint: String::new(), - console_enabled: false, - interactive_console_enabled: false, - secure_boot: false, - tpm_enabled: false, - tpm_persisted: false, - hardware_sealing_policy: HardwareSealingPolicy::Hash, - filtered_vpci_devices_allowed: true, - vm_unique_id: String::new(), - }, - false, - None, - None, - Some(KeyDerivationPolicy { - tcb_version: 0x1234, - mix_measurement: true, - }), - GuestStateEncryptionPolicy::HardwareSealing, - true, - true, - false, - ) - .await - .unwrap(); - - // It must produce an egress key and HWKP - assert!(derived.derived_keys.is_some()); - assert!(derived.hardware_key_protector.is_some()); - - // Apply to VMGS and verify encryption using egress key - unlock_vmgs_data_store( - &mut vmgs, - false, - &mut key_protector, - &mut key_protector_by_id, - derived.hardware_key_protector, - derived.derived_keys, - derived.key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - // VMGS should now be unlockable with only the egress key (ingress zeroed) - vmgs.unlock_with_encryption_key(&[0; AES_GCM_KEY_LENGTH]) - .await - .unwrap_err(); - } - - #[async_test] - async fn hardware_sealing_recovery_uses_hwkp_v2_when_encrypted(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // Pre-encrypt VMGS to simulate previous boot - let bootstrap = [0x33; AES_GCM_KEY_LENGTH]; - vmgs.test_add_new_encryption_key(&bootstrap, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - let bios_guid = Guid::new_random(); - - // Create a HWKP V2 by sealing current key and writing to VMGS - let mock_tee_call = MockTeeCall::new([0x8a; 32]); - - let hdk = HardwareDerivedKeys::derive_key( - mock_tee_call.supports_get_derived_key().unwrap(), - &AttestationVmConfig { - current_time: None, - root_cert_thumbprint: String::new(), - console_enabled: false, - interactive_console_enabled: false, - secure_boot: false, - tpm_enabled: false, - tpm_persisted: false, - hardware_sealing_policy: HardwareSealingPolicy::Hash, - filtered_vpci_devices_allowed: true, - vm_unique_id: String::new(), - }, - KeyDerivationPolicy { - tcb_version: 0x1234, - mix_measurement: true, - }, - ) - .unwrap(); - let hwkp = HardwareKeyProtector::seal_key(&hdk, &bootstrap).unwrap(); - vmgs::write_hardware_key_protector(&hwkp, &mut vmgs) - .await - .unwrap(); - - // Now call get_derived_keys with HardwareSealing required and VMGS encrypted - // Create a GET client backed by the test host - let get_pair = guest_emulation_transport::test_utilities::new_transport_pair( - driver, - None, - get_protocol::ProtocolVersion::NICKEL_REV2, - None, - None, - ) - .await; - - let derived = get_derived_keys( - &get_pair.client, - Some(&mock_tee_call), - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - &AttestationVmConfig { - current_time: None, - root_cert_thumbprint: String::new(), - console_enabled: false, - interactive_console_enabled: false, - secure_boot: false, - tpm_enabled: false, - tpm_persisted: false, - hardware_sealing_policy: HardwareSealingPolicy::Hash, - filtered_vpci_devices_allowed: true, - vm_unique_id: String::new(), - }, - true, - None, - None, - Some(KeyDerivationPolicy { - tcb_version: 0x1234, - mix_measurement: true, - }), - GuestStateEncryptionPolicy::HardwareSealing, - true, - true, - false, - ) - .await - .unwrap(); - - // Should have recovered ingress from HWKP and rotated egress - let keys = derived.derived_keys.unwrap(); - assert_eq!(keys.ingress, bootstrap); - assert_ne!(keys.encrypt_egress, keys.ingress); - } - #[async_test] async fn persist_all_key_protectors_write_key_protector_by_id() { let mut vmgs = new_formatted_vmgs().await; @@ -2613,7 +2193,6 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, - None, bios_guid, key_protector_settings, ) @@ -2658,7 +2237,6 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, - None, bios_guid, key_protector_settings, ) @@ -2707,7 +2285,6 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, - Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2784,7 +2361,7 @@ mod tests { let bios_guid = Guid::new_random(); let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new([0x12u8; 32]); + let tee = MockTeeCall::new(0x1234); // Ensure VMGS is not encrypted and agent data is empty before the call assert!(!vmgs.encrypted()); @@ -2870,7 +2447,7 @@ mod tests { let bios_guid = Guid::new_random(); let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new([0x12u8; 32]); + let tee = MockTeeCall::new(0x1234); // Ensure VMGS is not encrypted and agent data is empty before the call assert!(!vmgs.encrypted()); @@ -2955,7 +2532,7 @@ mod tests { assert!(!vmgs.encrypted()); // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new([0x12u8; 32]); + let tee = MockTeeCall::new(0x1234); let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); let res = initialize_platform_security( &get_pair.client, @@ -3040,7 +2617,7 @@ mod tests { assert!(!vmgs.encrypted()); // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new([0x12u8; 32]); + let tee = MockTeeCall::new(0x1234); let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); let res = initialize_platform_security( &get_pair.client, diff --git a/openhcl/underhill_attestation/src/vmgs.rs b/openhcl/underhill_attestation/src/vmgs.rs index 67648f0c42..58bc14c02e 100644 --- a/openhcl/underhill_attestation/src/vmgs.rs +++ b/openhcl/underhill_attestation/src/vmgs.rs @@ -287,7 +287,6 @@ mod tests { use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; use openhcl_attestation_protocol::vmgs::GspKp; use openhcl_attestation_protocol::vmgs::HMAC_SHA_256_KEY_LENGTH; - use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_CURRENT_VERSION; use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_SIZE; use openhcl_attestation_protocol::vmgs::HardwareKeyProtectorHeader; use openhcl_attestation_protocol::vmgs::KEY_PROTECTOR_SIZE; @@ -309,12 +308,7 @@ mod tests { } fn new_hardware_key_protector() -> HardwareKeyProtector { - let header = HardwareKeyProtectorHeader::new( - HW_KEY_PROTECTOR_CURRENT_VERSION, - HW_KEY_PROTECTOR_SIZE as u32, - 2, - 1, - ); + let header = HardwareKeyProtectorHeader::new(1, HW_KEY_PROTECTOR_SIZE as u32, 2); let iv = [3; AES_CBC_IV_LENGTH]; let ciphertext = [4; AES_GCM_KEY_LENGTH]; let hmac = [5; HMAC_SHA_256_KEY_LENGTH]; diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 2b1a882cab..6524877036 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -104,7 +104,6 @@ use mesh_worker::WorkerId; use mesh_worker::WorkerRpc; use net_packet_capture::PacketCaptureParams; use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; -use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::HardwareSealingPolicy; use openhcl_dma_manager::AllocationVisibility; use openhcl_dma_manager::DmaClientParameters; use openhcl_dma_manager::DmaClientSpawner; @@ -1954,23 +1953,6 @@ async fn new_underhill_vm( tracing::warn!(CVM_ALLOWED, "confidential debug enabled"); } - let tpm_persisted = !dps.general.suppress_attestation.unwrap_or(false); - let hardware_sealing_policy = if tpm_persisted { - // If TPM is persisted, use the hash policy to match the existing implementation. - // TODO: Support sealing policy for persisted TPM mode. - HardwareSealingPolicy::Hash - } else { - match dps.general.hardware_sealing_policy { - get_protocol::dps_json::HardwareSealingPolicy::NoSealing => HardwareSealingPolicy::None, - get_protocol::dps_json::HardwareSealingPolicy::HashPolicy => { - HardwareSealingPolicy::Hash - } - get_protocol::dps_json::HardwareSealingPolicy::SignerPolicy => { - HardwareSealingPolicy::Signer - } - } - }; - // Create the `AttestationVmConfig` from `dps`, which will be used in // - stateful mode (the attestation is not suppressed) // - stateless mode (isolated VM with attestation suppressed) @@ -1988,8 +1970,7 @@ async fn new_underhill_vm( interactive_console_enabled: interactive_console, secure_boot: dps.general.secure_boot_enabled, tpm_enabled: dps.general.tpm_enabled, - tpm_persisted, - hardware_sealing_policy, + tpm_persisted: !dps.general.suppress_attestation.unwrap_or(false), filtered_vpci_devices_allowed: with_vmbus_relay && dps.general.vpci_boot_enabled && isolation.is_isolated(), @@ -2899,32 +2880,14 @@ async fn new_underhill_vm( }); if dps.general.tpm_enabled { - let no_persistent_secrets = vmgs_client.is_none() - || (dps.general.suppress_attestation.unwrap_or(false) - && matches!( - attestation_vm_config.hardware_sealing_policy, - HardwareSealingPolicy::None - )); + let no_persistent_secrets = + vmgs_client.is_none() || dps.general.suppress_attestation.unwrap_or(false); let (ppi_store, nvram_store) = if no_persistent_secrets { - tracing::info!( - CVM_ALLOWED, - suppress_attestation=?dps.general.suppress_attestation, - hardware_sealing_policy=?attestation_vm_config.hardware_sealing_policy, - "TPM configured without persistent secrets, using ephemeral stores" - ); - ( EphemeralNonVolatileStoreHandle.into_resource(), EphemeralNonVolatileStoreHandle.into_resource(), ) } else { - tracing::info!( - CVM_ALLOWED, - suppress_attestation=?dps.general.suppress_attestation, - hardware_sealing_policy=?attestation_vm_config.hardware_sealing_policy, - "TPM configured with persistent secrets, using VMGS stores" - ); - ( VmgsFileHandle::new(vmgs::FileId::TPM_PPI, true).into_resource(), VmgsFileHandle::new(vmgs::FileId::TPM_NVRAM, true).into_resource(), @@ -3707,7 +3670,6 @@ fn validate_isolated_configuration(dps: &DevicePlatformSettings) -> Result<(), a suppress_attestation: _, bios_guid: _, vpci_boot_enabled: _, - hardware_sealing_policy: _, // Validated below processor_idle_enabled, diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index 05a8bf9188..d0e035d856 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -791,21 +791,6 @@ function Set-GuestStateIsolationMode Set-VmSystemSettings $vssd } -function Set-ManagementVtlEncryptionPolicy -{ - [CmdletBinding()] - Param ( - [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [System.Object] $Vm, - - [int] $Policy - ) - - $vssd = Get-VmSystemSettings $Vm - $vssd.ManagementVtlEncryptionPolicy = $Policy - Set-VmSystemSettings $vssd -} - # # CIM Helpers # diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index 0aa21afc50..b27cfb8292 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -18,7 +18,6 @@ use crate::OpenHclConfig; use crate::OpenHclServicingFlags; use crate::OpenvmmLogConfig; use crate::PetriHaltReason; -use crate::PetriHardwareSealingPolicy; use crate::PetriVmConfig; use crate::PetriVmResources; use crate::PetriVmRuntime; @@ -551,7 +550,6 @@ impl PetriVmmBackend for HyperVPetriBackend { // Configure the TPM if let Some(TpmConfig { no_persistent_secrets, - hardware_sealing_policy, }) = tpm { if generation == powershell::HyperVGeneration::One { @@ -566,19 +564,6 @@ impl PetriVmmBackend for HyperVPetriBackend { powershell::HyperVGuestStateIsolationMode::Default }) .await?; - - if hardware_sealing_policy != PetriHardwareSealingPolicy::Default { - vm.set_management_vtl_encryption_policy(match hardware_sealing_policy { - PetriHardwareSealingPolicy::HashPolicy => { - powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsHashPolicy - } - PetriHardwareSealingPolicy::SignerPolicy => { - powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsSignerPolicy - } - PetriHardwareSealingPolicy::Default => unreachable!(), - }) - .await?; - } } else if no_persistent_secrets { anyhow::bail!("no persistent secrets requires an hcl"); } diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index e9af5fdc6b..54ddae5dfe 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -1321,59 +1321,3 @@ pub async fn run_disable_vmtpm(vmid: &Guid) -> anyhow::Result<()> { .map(|_| ()) .context("run_disable_vmtpm") } - -/// VTL encryption policies for Hyper-V VMs. -#[derive(Debug)] -pub enum HyperVManagementVtlEncryptionPolicy { - /// Default encryption policy. - Default = 0, - /// Require GSP key encryption policy. - RequireGspKey = 1, - /// Forbid GSP key encryption policy. - ForbidGspKey = 2, - /// No encryption policy. - None = 3, - /// Hardware sealed secrets hash policy. - HardwareSealedSecretsHashPolicy = 4, - /// Hardware sealed secrets signer policy. - HardwareSealedSecretsSignerPolicy = 5, -} - -impl ps::AsVal for HyperVManagementVtlEncryptionPolicy { - fn as_val(&self) -> impl '_ + AsRef { - match self { - HyperVManagementVtlEncryptionPolicy::Default => "0", - HyperVManagementVtlEncryptionPolicy::RequireGspKey => "1", - HyperVManagementVtlEncryptionPolicy::ForbidGspKey => "2", - HyperVManagementVtlEncryptionPolicy::None => "3", - HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsHashPolicy => "4", - HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsSignerPolicy => "5", - } - } -} - -/// Sets the management VTL encryption policy for a VM. -pub async fn run_set_management_vtl_encryption_policy( - vmid: &Guid, - ps_mod: &Path, - policy: HyperVManagementVtlEncryptionPolicy, -) -> anyhow::Result<()> { - tracing::trace!(?policy, ?vmid, "set management vtl encryption policy"); - - run_host_cmd( - PowerShellBuilder::new() - .cmdlet("Import-Module") - .positional(ps_mod) - .next() - .cmdlet("Get-VM") - .arg("Id", vmid) - .pipeline() - .cmdlet("Set-ManagementVtlEncryptionPolicy") - .arg("Policy", policy) - .finish() - .build(), - ) - .await - .map(|_| ()) - .context("set_management_vtl_encryption_policy") -} diff --git a/petri/src/vm/hyperv/vm.rs b/petri/src/vm/hyperv/vm.rs index 825aaa0582..7f04eb6bb3 100644 --- a/petri/src/vm/hyperv/vm.rs +++ b/petri/src/vm/hyperv/vm.rs @@ -648,14 +648,6 @@ impl HyperVVM { pub async fn disable_tpm(&self) -> anyhow::Result<()> { powershell::run_disable_vmtpm(&self.vmid).await } - - /// Set the management VTL encryption policy - pub async fn set_management_vtl_encryption_policy( - &self, - policy: powershell::HyperVManagementVtlEncryptionPolicy, - ) -> anyhow::Result<()> { - powershell::run_set_management_vtl_encryption_policy(&self.vmid, &self.ps_mod, policy).await - } } impl Drop for HyperVVM { diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 66d499f107..35172e1a4c 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1081,16 +1081,6 @@ impl PetriVmBuilder { self } - /// Set the hardware sealing policy for the VM's TPM. - pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self { - self.config - .tpm - .as_mut() - .expect("hardware sealing policy requires a TPM") - .hardware_sealing_policy = policy; - self - } - /// Add custom VTL 2 settings. // TODO: At some point we want to replace uses of this with nicer with_disk, // with_nic, etc. methods. @@ -1892,34 +1882,16 @@ impl Default for OpenHclConfig { pub struct TpmConfig { /// Use ephemeral TPM state (do not persist to VMGS) pub no_persistent_secrets: bool, - /// Hardware sealing policy for sealed secrets - pub hardware_sealing_policy: PetriHardwareSealingPolicy, } impl Default for TpmConfig { fn default() -> Self { Self { no_persistent_secrets: true, - hardware_sealing_policy: PetriHardwareSealingPolicy::Default, } } } -/// Hardware sealing policy used by the test infrastructure. -/// -/// Maps to Hyper-V `Set-ManagementVtlEncryptionPolicy` values and -/// underhill's `HardwareSealingPolicy`. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum PetriHardwareSealingPolicy { - /// No explicit policy — the backend picks its default. - #[default] - Default, - /// Derive the hardware sealing key from measurement hash. - HashPolicy, - /// Derive the hardware sealing key from signer information. - SignerPolicy, -} - /// Firmware to load into the test VM. // TODO: remove the guests from the firmware enum so that we don't pass them // to the VMM backend after we have already used them generically. diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index b454184443..5f826463a3 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -911,7 +911,6 @@ impl PetriVmConfigSetupCore<'_> { if !self.firmware.is_openhcl() && let Some(TpmConfig { no_persistent_secrets, - .. }) = self.tpm_config { let register_layout = match self.arch { diff --git a/vm/devices/get/get_protocol/src/dps_json.rs b/vm/devices/get/get_protocol/src/dps_json.rs index 1bf40cd671..98008fd5eb 100644 --- a/vm/devices/get/get_protocol/src/dps_json.rs +++ b/vm/devices/get/get_protocol/src/dps_json.rs @@ -121,7 +121,7 @@ pub enum GuestStateEncryptionPolicy { /// Prefer (or require, if strict) GspById. /// /// This prevents a VM from being created as or migrated to GspKey even - /// if it is available. Existing GspKey encryption will be used unless + /// if it is available. Exisiting GspKey encryption will be used unless /// strict encryption policy is enabled. Fails if the data cannot be /// encrypted. GspById, @@ -131,9 +131,8 @@ pub enum GuestStateEncryptionPolicy { /// be used if GspKey is unavailable unless strict encryption policy is /// enabled. Fails if the data cannot be encrypted. GspKey, - /// Use hardware sealing exclusively. - /// - /// Expect to be set only when `no_persistent_secrets` is true on CVMs. + /// Use hardware sealing + // TODO: update this doc comment once hardware sealing is implemented HardwareSealing, } @@ -150,23 +149,6 @@ open_enum! { } } -/// Hardware sealing policy -/// -/// Used when `no_persistent_secrets` is true -/// By default, the policy will be applied to hardware-sealing-based -/// VMGS DEK backup on CVMs. If [`GuestStateEncryptionPolicy::HardwareSealing`] -/// is selected, this policy will be applied to the exclusive hardware sealing. -#[derive(Debug, Copy, Clone, Deserialize, Serialize, Default)] -pub enum HardwareSealingPolicy { - /// No hardware sealing - #[default] - NoSealing, - /// Hash-based hardware sealing - HashPolicy, - /// Signer-based hardware sealing - SignerPolicy, -} - /// Management VTL Feature Flags #[bitfield(u64)] #[derive(Deserialize, Serialize)] @@ -184,7 +166,7 @@ pub struct ManagementVtlFeatures { #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct HclDevicePlatformSettingsV2Static { - // UEFI flags + //UEFI flags pub legacy_memory_map: bool, pub pause_after_boot_failure: bool, pub pxe_ip_v6: bool, @@ -239,8 +221,6 @@ pub struct HclDevicePlatformSettingsV2Static { pub management_vtl_features: ManagementVtlFeatures, #[serde(default)] pub hv_sint_enabled: bool, - #[serde(default)] - pub hardware_sealing_policy: HardwareSealingPolicy, } #[derive(Debug, Default, Deserialize, Serialize)] diff --git a/vm/devices/get/get_protocol/src/lib.rs b/vm/devices/get/get_protocol/src/lib.rs index 06d08279fd..86f7dcd267 100644 --- a/vm/devices/get/get_protocol/src/lib.rs +++ b/vm/devices/get/get_protocol/src/lib.rs @@ -350,8 +350,6 @@ open_enum! { TPM_INVALID_STATE = 17, TPM_IDENTITY_CHANGE_FAILED = 18, WRAPPED_KEY_REQUIRED_BUT_INVALID = 19, - DEK_HARDWARE_SEALING_INVALID_KEY = 20, - DEK_HARDWARE_SEALING_FAILED = 21, DEK_HARDWARE_UNSEALING_SKIPPED = 23, } } diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index 84fe2c2231..588da4b69f 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -254,8 +254,5 @@ pub mod ged { AkCertPersistentAcrossBoot, /// Config for testing skip hardware unsealing signal from IGVMAgent. KeyReleaseFailureSkipHwUnsealing, - /// Config for testing key release failure without skip_hw_unsealing - /// signal — hardware unsealing fallback should be attempted. - KeyReleaseFailure, } } diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index bc0849bb05..4075990cce 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -42,7 +42,6 @@ use get_protocol::VmgsIoStatus; use get_protocol::dps_json::EfiDiagnosticsLogLevelType; use get_protocol::dps_json::GuestStateEncryptionPolicy; use get_protocol::dps_json::GuestStateLifetime; -use get_protocol::dps_json::HardwareSealingPolicy; use get_protocol::dps_json::HclSecureBootTemplateId; use get_protocol::dps_json::ManagementVtlFeatures; use get_protocol::dps_json::PcatBootDevice; @@ -159,9 +158,6 @@ pub struct GuestConfig { /// Management VTL feature flags #[inspect(debug)] pub management_vtl_features: ManagementVtlFeatures, - /// Hardware sealing policy - #[inspect(debug)] - pub hardware_sealing_policy: HardwareSealingPolicy, /// EFI diagnostics log level #[inspect(debug)] pub efi_diagnostics_log_level: EfiDiagnosticsLogLevelType, @@ -1358,7 +1354,6 @@ impl GedChannel { guest_state_lifetime: state.config.guest_state_lifetime, guest_state_encryption_policy: state.config.guest_state_encryption_policy, management_vtl_features: state.config.management_vtl_features, - hardware_sealing_policy: state.config.hardware_sealing_policy, efi_diagnostics_log_level: state.config.efi_diagnostics_log_level, hv_sint_enabled: state.config.hv_sint_enabled, }, diff --git a/vm/devices/get/guest_emulation_device/src/resolver.rs b/vm/devices/get/guest_emulation_device/src/resolver.rs index 4539034cde..195727504f 100644 --- a/vm/devices/get/guest_emulation_device/src/resolver.rs +++ b/vm/devices/get/guest_emulation_device/src/resolver.rs @@ -196,7 +196,6 @@ impl AsyncResolveResource guest_state_lifetime, guest_state_encryption_policy, management_vtl_features, - hardware_sealing_policy: get_protocol::dps_json::HardwareSealingPolicy::default(), efi_diagnostics_log_level: match resource.efi_diagnostics_log_level { EfiDiagnosticsLogLevelType::Default => { get_protocol::dps_json::EfiDiagnosticsLogLevelType::DEFAULT diff --git a/vm/devices/get/guest_emulation_device/src/test_utilities.rs b/vm/devices/get/guest_emulation_device/src/test_utilities.rs index 4026bf5c9e..34c7c15276 100644 --- a/vm/devices/get/guest_emulation_device/src/test_utilities.rs +++ b/vm/devices/get/guest_emulation_device/src/test_utilities.rs @@ -261,7 +261,6 @@ pub fn create_host_channel( guest_state_lifetime: Default::default(), guest_state_encryption_policy: Default::default(), management_vtl_features: Default::default(), - hardware_sealing_policy: Default::default(), efi_diagnostics_log_level: Default::default(), hv_sint_enabled: false, }; diff --git a/vm/devices/get/guest_emulation_transport/src/api.rs b/vm/devices/get/guest_emulation_transport/src/api.rs index caf9dc03b9..de9f74e4c7 100644 --- a/vm/devices/get/guest_emulation_transport/src/api.rs +++ b/vm/devices/get/guest_emulation_transport/src/api.rs @@ -31,7 +31,6 @@ pub mod platform_settings { use get_protocol::dps_json::EfiDiagnosticsLogLevelType; use get_protocol::dps_json::GuestStateEncryptionPolicy; use get_protocol::dps_json::GuestStateLifetime; - use get_protocol::dps_json::HardwareSealingPolicy; use get_protocol::dps_json::ManagementVtlFeatures; use guid::Guid; use inspect::Inspect; @@ -136,8 +135,6 @@ pub mod platform_settings { pub guest_state_encryption_policy: GuestStateEncryptionPolicy, #[inspect(debug)] pub management_vtl_features: ManagementVtlFeatures, - #[inspect(debug)] - pub hardware_sealing_policy: HardwareSealingPolicy, pub hv_sint_enabled: bool, } diff --git a/vm/devices/get/guest_emulation_transport/src/client.rs b/vm/devices/get/guest_emulation_transport/src/client.rs index 3007b24469..fc361818a6 100644 --- a/vm/devices/get/guest_emulation_transport/src/client.rs +++ b/vm/devices/get/guest_emulation_transport/src/client.rs @@ -344,7 +344,6 @@ impl GuestEmulationTransportClient { guest_state_lifetime: json.v2.r#static.guest_state_lifetime, guest_state_encryption_policy: json.v2.r#static.guest_state_encryption_policy, management_vtl_features: json.v2.r#static.management_vtl_features, - hardware_sealing_policy: json.v2.r#static.hardware_sealing_policy, hv_sint_enabled: json.v2.r#static.hv_sint_enabled, }, acpi_tables: json.v2.dynamic.acpi_tables, diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 5fa253c724..3f0dd09b53 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -177,15 +177,6 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ]), ); } - IgvmAttestTestConfig::KeyReleaseFailure => { - plan.insert( - IgvmAttestRequestType::KEY_RELEASE_REQUEST, - VecDeque::from([ - IgvmAgentAction::RespondSuccess, - IgvmAgentAction::RespondFailure, - ]), - ); - } } plan diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index b47fae069a..f951f826ac 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -30,8 +30,6 @@ enum TestConfig { AkCertPersistentAcrossBoot, /// Test skip hardware unsealing signal from IGVM Agent KeyReleaseFailureSkipHwUnsealing, - /// Test key release failure without skip_hw_unsealing signal - KeyReleaseFailure, } impl From for IgvmAttestTestConfig { @@ -46,7 +44,6 @@ impl From for IgvmAttestTestConfig { TestConfig::KeyReleaseFailureSkipHwUnsealing => { IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing } - TestConfig::KeyReleaseFailure => IgvmAttestTestConfig::KeyReleaseFailure, } } } diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index 569215ba6d..bab479bad2 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -80,7 +80,6 @@ fn config_for_vm_name(vm_name: &str) -> Option { "skip_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, ), - ("hw_unseal", IgvmAttestTestConfig::KeyReleaseFailure), ]; for &(pattern, config) in KNOWN_TEST_CONFIGS { @@ -178,26 +177,11 @@ mod tests { #[test] fn no_match_unknown_vm() { - assert!( - config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test") - .is_none() - ); - } - - #[test] - fn match_hw_unseal() { - let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_hw_unseal"; - assert!(vm_name.len() <= 100, "name must fit in 100 chars"); - match config_for_vm_name(vm_name) { - Some(IgvmAgentTestSetting::TestConfig(c)) => { - assert!(matches!(c, IgvmAttestTestConfig::KeyReleaseFailure)) - } - other => panic!("expected KeyReleaseFailure, got {:?}", other), - } + assert!(config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test").is_none()); } #[test] fn no_match_empty() { assert!(config_for_vm_name("").is_none()); } -} +} \ No newline at end of file diff --git a/vm/devices/tpm/tpm_guest_tests/src/main.rs b/vm/devices/tpm/tpm_guest_tests/src/main.rs index 8e6e08e5bd..6fb46a1a35 100644 --- a/vm/devices/tpm/tpm_guest_tests/src/main.rs +++ b/vm/devices/tpm/tpm_guest_tests/src/main.rs @@ -54,27 +54,6 @@ struct Config { report: bool, user_data: Option>, show_runtime_claims: bool, - nv_define: Option, - nv_write: Option, - nv_read: Option, -} - -#[derive(Debug)] -struct NvDefineConfig { - index: u32, - size: u16, -} - -#[derive(Debug)] -struct NvWriteConfig { - index: u32, - data: Vec, -} - -#[derive(Debug)] -struct NvReadConfig { - index: u32, - expected: Option>, } #[derive(Parser, Debug)] @@ -98,15 +77,6 @@ enum Command { /// Write guest input and read the attestation report #[command(name = "report")] Report(ReportArgs), - /// Define an NV index with a given size - #[command(name = "nv_define")] - NvDefine(NvDefineArgs), - /// Write data to an NV index - #[command(name = "nv_write")] - NvWrite(NvWriteArgs), - /// Read data from an NV index - #[command(name = "nv_read")] - NvRead(NvReadArgs), } #[derive(Args, Debug, Default)] @@ -139,45 +109,6 @@ struct ReportArgs { show_runtime_claims: bool, } -#[derive(Args, Debug)] -struct NvDefineArgs { - /// NV index handle (hex, e.g. 0x1500016) - #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] - index: u32, - - /// Size of the NV index in bytes - #[arg(long, value_name = "BYTES")] - size: u16, -} - -#[derive(Args, Debug)] -struct NvWriteArgs { - /// NV index handle (hex, e.g. 0x1500016) - #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] - index: u32, - - /// Data to write (hex) - #[arg(long, value_name = "HEX")] - data_hex: String, -} - -#[derive(Args, Debug)] -struct NvReadArgs { - /// NV index handle (hex, e.g. 0x1500016) - #[arg(long, value_name = "HEX", value_parser = parse_nv_index)] - index: u32, - - /// Expected data contents (hex); if provided, verifies the read result - #[arg(long, value_name = "HEX")] - expected_data_hex: Option, -} - -fn parse_nv_index(s: &str) -> Result { - let trimmed = s.trim(); - let hex = trimmed.strip_prefix("0x").unwrap_or(trimmed); - u32::from_str_radix(hex, 16).map_err(|e| format!("invalid NV index: {e}")) -} - fn main() { let cli = Cli::parse(); let config = match config_from_cli(cli) { @@ -221,18 +152,6 @@ fn run(config: &Config) -> Result<(), Box> { } } - if let Some(nv_def) = &config.nv_define { - handle_nv_define(&mut helper, nv_def.index, nv_def.size)?; - } - - if let Some(nv_wr) = &config.nv_write { - handle_nv_write(&mut helper, nv_wr.index, &nv_wr.data)?; - } - - if let Some(nv_rd) = &config.nv_read { - handle_nv_read(&mut helper, nv_rd.index, nv_rd.expected.as_deref())?; - } - Ok(()) } @@ -330,31 +249,6 @@ fn config_from_cli(cli: Cli) -> Result { config.user_data = Some(bytes); } } - Command::NvDefine(args) => { - config.nv_define = Some(NvDefineConfig { - index: args.index, - size: args.size, - }); - } - Command::NvWrite(args) => { - let data = parse_hex_bytes(&args.data_hex).map_err(|e| format!("--data-hex: {e}"))?; - config.nv_write = Some(NvWriteConfig { - index: args.index, - data, - }); - } - Command::NvRead(args) => { - let expected = args - .expected_data_hex - .as_deref() - .map(parse_hex_bytes) - .transpose() - .map_err(|e| format!("--expected-data-hex: {e}"))?; - config.nv_read = Some(NvReadConfig { - index: args.index, - expected, - }); - } } Ok(config) @@ -392,69 +286,6 @@ fn handle_report( Ok(att_report) } -fn handle_nv_define( - helper: &mut TpmEngineHelper, - nv_index: u32, - size: u16, -) -> Result<(), Box> { - if helper.nv_read_public(nv_index).is_ok() { - println!("NV index {nv_index:#x} already defined, undefining first…"); - helper - .nv_undefine_space(TPM20_RH_OWNER, nv_index) - .map_err(|e| -> Box { Box::new(e) })?; - } - - println!("Defining NV index {nv_index:#x} with {size} bytes…"); - helper - .nv_define_space(TPM20_RH_OWNER, 0, nv_index, size) - .map_err(|e| -> Box { Box::new(e) })?; - - println!("NV index {nv_index:#x} defined successfully ({size} bytes)."); - Ok(()) -} - -fn handle_nv_write( - helper: &mut TpmEngineHelper, - nv_index: u32, - data: &[u8], -) -> Result<(), Box> { - println!("Writing {} bytes to NV index {nv_index:#x}…", data.len()); - helper.nv_write(TPM20_RH_OWNER, None, nv_index, data)?; - println!( - "NV write to {nv_index:#x} succeeded ({} bytes).", - data.len() - ); - Ok(()) -} - -fn handle_nv_read( - helper: &mut TpmEngineHelper, - nv_index: u32, - expected: Option<&[u8]>, -) -> Result<(), Box> { - println!("Reading NV index {nv_index:#x}…"); - let data = read_nv_index(helper, nv_index)?; - print_nv_summary("NV read", &data); - - if let Some(expected) = expected { - if data == expected { - println!( - "NV index {nv_index:#x} matches expected value ({} bytes).", - data.len() - ); - } else { - return Err(format!( - "NV index {nv_index:#x} contents did not match expected value (got {} bytes, expected {} bytes)", - data.len(), - expected.len() - ) - .into()); - } - } - - Ok(()) -} - fn print_runtime_claims(attestation_report: &[u8]) -> Result<(), Box> { match runtime_claims_json(attestation_report)? { Some(json) => { diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 2200eb1b00..cdfb28bbbd 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -5,7 +5,6 @@ use anyhow::Context; use anyhow::ensure; use petri::PetriGuestStateLifetime; use petri::PetriHaltReason; -use petri::PetriHardwareSealingPolicy; use petri::PetriVmBuilder; use petri::PetriVmmBackend; use petri::ResolvedArtifact; @@ -189,88 +188,6 @@ impl<'a> TpmGuestTests<'a> { _ => unreachable!(), } } - - /// Define an NV index with the given size. - async fn nv_define(&self, index: &str, size: &str) -> anyhow::Result { - let guest_binary_path = &self.guest_binary_path; - match self.os_flavor { - OsFlavor::Linux => { - let sh = self.agent.unix_shell(); - cmd!(sh, "{guest_binary_path}") - .args(["nv_define", "--index", index, "--size", size]) - .read() - .await - } - OsFlavor::Windows => { - let sh = self.agent.windows_shell(); - cmd!(sh, "{guest_binary_path}") - .args(["nv_define", "--index", index, "--size", size]) - .read() - .await - } - _ => unreachable!(), - } - } - - /// Write hex data to an NV index. - async fn nv_write(&self, index: &str, data_hex: &str) -> anyhow::Result { - let guest_binary_path = &self.guest_binary_path; - match self.os_flavor { - OsFlavor::Linux => { - let sh = self.agent.unix_shell(); - cmd!(sh, "{guest_binary_path}") - .args(["nv_write", "--index", index, "--data-hex", data_hex]) - .read() - .await - } - OsFlavor::Windows => { - let sh = self.agent.windows_shell(); - cmd!(sh, "{guest_binary_path}") - .args(["nv_write", "--index", index, "--data-hex", data_hex]) - .read() - .await - } - _ => unreachable!(), - } - } - - /// Read an NV index and verify against expected hex data. - async fn nv_read_with_expected_hex( - &self, - index: &str, - expected_hex: &str, - ) -> anyhow::Result { - let guest_binary_path = &self.guest_binary_path; - match self.os_flavor { - OsFlavor::Linux => { - let sh = self.agent.unix_shell(); - cmd!(sh, "{guest_binary_path}") - .args([ - "nv_read", - "--index", - index, - "--expected-data-hex", - expected_hex, - ]) - .read() - .await - } - OsFlavor::Windows => { - let sh = self.agent.windows_shell(); - cmd!(sh, "{guest_binary_path}") - .args([ - "nv_read", - "--index", - index, - "--expected-data-hex", - expected_hex, - ]) - .read() - .await - } - _ => unreachable!(), - } - } } /// Basic boot tests with TPM enabled. @@ -647,7 +564,6 @@ async fn cvm_tpm_guest_tests( /// `KeyReleaseFailureSkipHwUnsealing` configuration. #[cfg(windows)] #[vmm_test( - hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], )] async fn skip_hwun_seal( @@ -668,7 +584,6 @@ async fn skip_hwun_seal( .await?; let guest_binary_path = match os_flavor { - OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, _ => unreachable!(), }; @@ -754,275 +669,3 @@ async fn tpm_servicing( vm.wait_for_clean_teardown().await?; Ok(()) } - -/// Test that KEY_RELEASE failure without skip_hw_unsealing signal allows -/// hardware unsealing fallback to succeed. -/// -/// First boot: KEY_RELEASE succeeds, VMGS is encrypted with hardware -/// key protector, TPM state is sealed. AK cert is verified. -/// Second boot: KEY_RELEASE fails (plain failure, no skip_hw_unsealing -/// signal), hardware unsealing fallback is attempted and succeeds because -/// the hardware key protector was saved on first boot. The VM boots -/// normally and the AK cert remains accessible. -/// -/// The test function name contains `hw_unseal` so the per-VM agent -/// registry in test_igvm_agent_rpc_server matches it to the -/// `KeyReleaseFailure` configuration. -#[cfg(windows)] -#[vmm_test( - hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], - hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], -)] -async fn hw_unseal( - config: PetriVmBuilder, - extra_deps: (ResolvedArtifact, ResolvedArtifact), -) -> anyhow::Result<()> { - let os_flavor = config.os_flavor(); - let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; - - let rpc_server_path = rpc_server_artifact.get(); - let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - - let (mut vm, agent) = config - .with_tpm(true) - .with_tpm_state_persistence(true) - .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) - .run() - .await?; - - let guest_binary_path = match os_flavor { - OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, - OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, - _ => unreachable!(), - }; - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - // First boot: KEY_RELEASE succeeds. Verify AK cert is present. - let expected_hex = expected_ak_cert_hex(); - let ak_cert_output = tpm_guest_tests - .read_ak_cert_with_expected_hex(expected_hex.as_str()) - .await?; - - ensure!( - ak_cert_output.contains("AK certificate matches expected value"), - format!("{ak_cert_output}") - ); - - // Reboot: triggers second KEY_RELEASE which fails (plain failure, - // no skip_hw_unsealing signal). Hardware unsealing fallback kicks - // in and succeeds — the VM boots normally. - agent.reboot().await?; - let agent = vm.wait_for_reset().await?; - - // Verify AK cert is still accessible after the hw unsealing fallback. - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - let ak_cert_output = tpm_guest_tests - .read_ak_cert_with_expected_hex(expected_hex.as_str()) - .await?; - - ensure!( - ak_cert_output.contains("AK certificate matches expected value"), - "AK cert should still be accessible after hw unsealing fallback: {ak_cert_output}" - ); - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - -/// NV index used by the hardware sealing persistence tests. -#[cfg(windows)] -const TEST_NV_INDEX: &str = "0x1500016"; -/// Size of the test NV index in bytes. -#[cfg(windows)] -const TEST_NV_SIZE: &str = "64"; -/// Test data written to the NV index (hex). -#[cfg(windows)] -const TEST_NV_DATA_HEX: &str = "0xdeadbeefcafebabe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"; - -/// Test that hardware sealing with hash-based key derivation persists -/// TPM NV index data across reboots. -/// -/// Configuration: `no_persistent_secrets=true` (NoPersistentSecrets -/// isolation) with `HardwareSealedSecretsHashPolicy`. The VMGS is -/// encrypted using a hardware-sealed key derived from the measurement -/// hash. -/// -/// First boot: define NV index, write test data, read and verify. -/// Second boot: read the same NV index and verify data persisted. -#[cfg(windows)] -#[vmm_test( - hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], - hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], -)] -async fn hw_seal_hash( - config: PetriVmBuilder, - extra_deps: (ResolvedArtifact, ResolvedArtifact), -) -> anyhow::Result<()> { - let os_flavor = config.os_flavor(); - let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; - - let rpc_server_path = rpc_server_artifact.get(); - let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - - let (mut vm, agent) = config - .with_tpm(true) - .with_tpm_state_persistence(false) - .with_hardware_sealing_policy(PetriHardwareSealingPolicy::HashPolicy) - .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) - .run() - .await?; - - let guest_binary_path = match os_flavor { - OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, - OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, - _ => unreachable!(), - }; - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - // First boot: define NV index, write test data, read and verify. - let define_output = tpm_guest_tests - .nv_define(TEST_NV_INDEX, TEST_NV_SIZE) - .await?; - ensure!( - define_output.contains("defined successfully"), - "NV define should succeed: {define_output}" - ); - - let write_output = tpm_guest_tests - .nv_write(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - write_output.contains("succeeded"), - "NV write should succeed: {write_output}" - ); - - let read_output = tpm_guest_tests - .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - read_output.contains("matches expected value"), - "NV read should match on first boot: {read_output}" - ); - - // Reboot to test persistence. - agent.reboot().await?; - let agent = vm.wait_for_reset().await?; - - // Second boot: re-send the binary and verify NV data persisted. - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - let read_output = tpm_guest_tests - .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - read_output.contains("matches expected value"), - "NV data should persist across reboot with HashPolicy: {read_output}" - ); - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - -/// Test that hardware sealing with signer-based key derivation persists -/// TPM NV index data across reboots. -/// -/// Same as `hw_seal_hash` but uses `HardwareSealedSecretsSignerPolicy` -/// instead. -#[cfg(windows)] -#[vmm_test( - hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], - hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], -)] -async fn hw_seal_signer( - config: PetriVmBuilder, - extra_deps: (ResolvedArtifact, ResolvedArtifact), -) -> anyhow::Result<()> { - let os_flavor = config.os_flavor(); - let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; - - let rpc_server_path = rpc_server_artifact.get(); - let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - - let (mut vm, agent) = config - .with_tpm(true) - .with_tpm_state_persistence(false) - .with_hardware_sealing_policy(PetriHardwareSealingPolicy::SignerPolicy) - .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) - .run() - .await?; - - let guest_binary_path = match os_flavor { - OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, - OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, - _ => unreachable!(), - }; - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - // First boot: define NV index, write test data, read and verify. - let define_output = tpm_guest_tests - .nv_define(TEST_NV_INDEX, TEST_NV_SIZE) - .await?; - ensure!( - define_output.contains("defined successfully"), - "NV define should succeed: {define_output}" - ); - - let write_output = tpm_guest_tests - .nv_write(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - write_output.contains("succeeded"), - "NV write should succeed: {write_output}" - ); - - let read_output = tpm_guest_tests - .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - read_output.contains("matches expected value"), - "NV read should match on first boot: {read_output}" - ); - - // Reboot to test persistence. - agent.reboot().await?; - let agent = vm.wait_for_reset().await?; - - // Second boot: re-send the binary and verify NV data persisted. - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - let read_output = tpm_guest_tests - .nv_read_with_expected_hex(TEST_NV_INDEX, TEST_NV_DATA_HEX) - .await?; - ensure!( - read_output.contains("matches expected value"), - "NV data should persist across reboot with SignerPolicy: {read_output}" - ); - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} From 2f11da244a3c5f43991dc4fcb18db200a1c939ae Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 06:42:37 +0000 Subject: [PATCH 05/16] update Signed-off-by: Ming-Wei Shih --- openhcl/underhill_attestation/src/lib.rs | 2 +- vm/devices/get/get_resources/src/lib.rs | 3 + vm/devices/get/test_igvm_agent_lib/src/lib.rs | 9 + .../test_igvm_agent_rpc_server/src/main.rs | 3 + .../src/rpc/igvm_agent.rs | 55 +--- .../vmm_tests/tests/tests/multiarch/tpm.rs | 237 +++++++++++++++++- 6 files changed, 251 insertions(+), 58 deletions(-) diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index a277076e02..00abc07c30 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -892,7 +892,7 @@ async fn get_derived_keys( ); get.event_log_fatal( guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED, - ); + ).await; return Err(GetDerivedKeysError::HardwareUnsealingSkipped); } else if let Some(tee_call) = tee_call { diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index 588da4b69f..84fe2c2231 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -254,5 +254,8 @@ pub mod ged { AkCertPersistentAcrossBoot, /// Config for testing skip hardware unsealing signal from IGVMAgent. KeyReleaseFailureSkipHwUnsealing, + /// Config for testing key release failure without skip_hw_unsealing + /// signal — hardware unsealing fallback should be attempted. + KeyReleaseFailure, } } diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 3f0dd09b53..5fa253c724 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -177,6 +177,15 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ]), ); } + IgvmAttestTestConfig::KeyReleaseFailure => { + plan.insert( + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondFailure, + ]), + ); + } } plan diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index f951f826ac..b47fae069a 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -30,6 +30,8 @@ enum TestConfig { AkCertPersistentAcrossBoot, /// Test skip hardware unsealing signal from IGVM Agent KeyReleaseFailureSkipHwUnsealing, + /// Test key release failure without skip_hw_unsealing signal + KeyReleaseFailure, } impl From for IgvmAttestTestConfig { @@ -44,6 +46,7 @@ impl From for IgvmAttestTestConfig { TestConfig::KeyReleaseFailureSkipHwUnsealing => { IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing } + TestConfig::KeyReleaseFailure => IgvmAttestTestConfig::KeyReleaseFailure, } } } diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index bab479bad2..a3d8b086cf 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -80,6 +80,7 @@ fn config_for_vm_name(vm_name: &str) -> Option { "skip_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, ), + ("use_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailure), ]; for &(pattern, config) in KNOWN_TEST_CONFIGS { @@ -131,57 +132,3 @@ pub fn process_igvm_attest(vm_name: &str, report: &[u8]) -> TestAgentResult assert!(matches!( - c, - IgvmAttestTestConfig::AkCertRequestFailureAndRetry - )), - other => panic!("expected AkCertRequestFailureAndRetry, got {:?}", other), - } - } - - #[test] - fn match_akcert_cache() { - let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_akcert_cache"; - assert!(vm_name.len() <= 100, "name must fit in 100 chars"); - match config_for_vm_name(vm_name) { - Some(IgvmAgentTestSetting::TestConfig(c)) => assert!(matches!( - c, - IgvmAttestTestConfig::AkCertPersistentAcrossBoot - )), - other => panic!("expected AkCertPersistentAcrossBoot, got {:?}", other), - } - } - - #[test] - fn match_skip_hwunseal() { - let vm_name = "multiarch::tpm::hyperv_openhcl_uefi_x64_windows_datacenter_core_2025_x64_prepped_snp_skip_hwunseal"; - assert!(vm_name.len() <= 100, "name must fit in 100 chars"); - match config_for_vm_name(vm_name) { - Some(IgvmAgentTestSetting::TestConfig(c)) => assert!(matches!( - c, - IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing - )), - other => panic!("expected KeyReleaseFailureSkipHwUnsealing, got {:?}", other), - } - } - - #[test] - fn no_match_unknown_vm() { - assert!(config_for_vm_name("multiarch::tpm::hyperv_openhcl_uefi_x64_some_random_test").is_none()); - } - - #[test] - fn no_match_empty() { - assert!(config_for_vm_name("").is_none()); - } -} \ No newline at end of file diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index cdfb28bbbd..e10c272abb 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -4,6 +4,7 @@ use anyhow::Context; use anyhow::ensure; use petri::PetriGuestStateLifetime; +#[cfg(windows)] use petri::PetriHaltReason; use petri::PetriVmBuilder; use petri::PetriVmmBackend; @@ -232,7 +233,7 @@ async fn boot_with_tpm(config: PetriVmBuilder) -> anyhow: openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64], openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64] )] -async fn ak_cert_cache( +async fn tpm_ak_cert_persisted( config: PetriVmBuilder, extra_deps: (ResolvedArtifact,), ) -> anyhow::Result<()> { @@ -294,7 +295,7 @@ async fn ak_cert_cache( openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64], openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64] )] -async fn ak_cert_retry( +async fn tpm_ak_cert_retry( config: PetriVmBuilder, extra_deps: (ResolvedArtifact,), ) -> anyhow::Result<()> { @@ -355,6 +356,151 @@ async fn ak_cert_retry( Ok(()) } +/// Hyper-V variant of TPM AK cert persisted test. +/// +/// First boot: AK cert request is served by the RPC agent. +/// Second boot: AK cert is served from the persistent cache. +/// +/// The test function name contains `ak_cert_cache` so the per-VM agent +/// registry in test_igvm_agent_rpc_server matches it to the +/// `AkCertPersistentAcrossBoot` configuration. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[vbs](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[vbs](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[tdx](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[tdx](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn ak_cert_cache( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, mut agent) = config + .with_tpm(true) + .with_tpm_state_persistence(true) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => { + // First boot - AK cert request will be served by RPC agent. + // Second boot - AK cert request will be bypassed (cached). + TPM_GUEST_TESTS_LINUX_GUEST_PATH + } + OsFlavor::Windows => { + // First boot - AK cert request will be served by RPC agent. + // Reboot so second boot uses cached cert. + agent.reboot().await?; + agent = vm.wait_for_reset().await?; + TPM_GUEST_TESTS_WINDOWS_GUEST_PATH + } + _ => unreachable!(), + }; + + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + let expected_hex = expected_ak_cert_hex(); + let output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + output.contains("AK certificate matches expected value"), + format!("{output}") + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Hyper-V variant of TPM AK cert retry test. +/// +/// The RPC agent is configured to fail the first AK cert request and +/// succeed on retry. The guest-side `tpm_guest_tests` binary verifies +/// that the first read fails and the second (retry) read succeeds with +/// the expected certificate data. +/// +/// The test function name contains `ak_cert_retry` so the per-VM agent +/// registry in test_igvm_agent_rpc_server matches it to the +/// `AkCertRequestFailureAndRetry` configuration. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64(vhd(windows_datacenter_core_2022_x64))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[vbs](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[vbs](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[tdx](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[tdx](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn ak_cert_retry( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(true) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // The read attempt is expected to fail and trigger an AK cert renewal request. + let attempt = tpm_guest_tests.read_ak_cert().await; + assert!( + attempt.is_err(), + "AK certificate read unexpectedly succeeded" + ); + + let expected_hex = expected_ak_cert_hex(); + let output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + output.contains("AK certificate matches expected value"), + format!("{output}") + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + /// VBS boot test with attestation enabled #[openvmm_test( openhcl_uefi_x64[vbs](vhd(windows_datacenter_core_2025_x64_prepped)), @@ -564,9 +710,10 @@ async fn cvm_tpm_guest_tests( /// `KeyReleaseFailureSkipHwUnsealing` configuration. #[cfg(windows)] #[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], )] -async fn skip_hwun_seal( +async fn skip_hw_unseal( config: PetriVmBuilder, extra_deps: (ResolvedArtifact, ResolvedArtifact), ) -> anyhow::Result<()> { @@ -584,6 +731,7 @@ async fn skip_hwun_seal( .await?; let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, _ => unreachable!(), }; @@ -627,6 +775,89 @@ async fn skip_hwun_seal( Ok(()) } +/// Test that KEY_RELEASE failure without skip_hw_unsealing signal allows +/// hardware unsealing fallback to succeed. +/// +/// First boot: KEY_RELEASE succeeds, VMGS is encrypted with hardware +/// key protector, TPM state is sealed. AK cert is verified. +/// Second boot: KEY_RELEASE fails (plain failure, no skip_hw_unsealing +/// signal), hardware unsealing fallback is attempted and succeeds because +/// the hardware key protector was saved on first boot. The VM boots +/// normally and the AK cert remains accessible. +/// +/// The test function name contains `use_hw_unseal` so the per-VM agent +/// registry in test_igvm_agent_rpc_server matches it to the +/// `KeyReleaseFailure` configuration. +#[cfg(windows)] +#[vmm_test( + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], +)] +async fn use_hw_unseal( + config: PetriVmBuilder, + extra_deps: (ResolvedArtifact, ResolvedArtifact), +) -> anyhow::Result<()> { + let os_flavor = config.os_flavor(); + let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + + let rpc_server_path = rpc_server_artifact.get(); + let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; + + let (mut vm, agent) = config + .with_tpm(true) + .with_tpm_state_persistence(true) + .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) + .run() + .await?; + + let guest_binary_path = match os_flavor { + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, + _ => unreachable!(), + }; + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + // First boot: KEY_RELEASE succeeds. Verify AK cert is present. + let expected_hex = expected_ak_cert_hex(); + let ak_cert_output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + ak_cert_output.contains("AK certificate matches expected value"), + format!("{ak_cert_output}") + ); + + // Reboot: triggers second KEY_RELEASE which fails (plain failure, + // no skip_hw_unsealing signal). Hardware unsealing fallback kicks + // in and succeeds — the VM boots normally. + agent.reboot().await?; + let agent = vm.wait_for_reset().await?; + + // Verify AK cert is still accessible after the hw unsealing fallback. + let host_binary_path = tpm_guest_tests_artifact.get(); + let tpm_guest_tests = + TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) + .await?; + + let ak_cert_output = tpm_guest_tests + .read_ak_cert_with_expected_hex(expected_hex.as_str()) + .await?; + + ensure!( + ak_cert_output.contains("AK certificate matches expected value"), + "AK cert should still be accessible after hw unsealing fallback: {ak_cert_output}" + ); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + /// Test that TPM NVRAM size persists across servicing. #[vmm_test( openvmm_openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))[LATEST_STANDARD_X64, VMGS_WITH_16K_TPM], From 1c7c745514a9648feb3b681863a144ad2f209ace Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 06:59:19 +0000 Subject: [PATCH 06/16] x Signed-off-by: Ming-Wei Shih --- openhcl/underhill_attestation/src/lib.rs | 3 ++- .../get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs | 2 +- vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index 00abc07c30..0dbd96c134 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -892,7 +892,8 @@ async fn get_derived_keys( ); get.event_log_fatal( guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED, - ).await; + ) + .await; return Err(GetDerivedKeysError::HardwareUnsealingSkipped); } else if let Some(tee_call) = tee_call { diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index a3d8b086cf..2d70c46d42 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -73,7 +73,7 @@ fn config_for_vm_name(vm_name: &str) -> Option { IgvmAttestTestConfig::AkCertRequestFailureAndRetry, ), ( - "akcert_cache", + "ak_cert_cache", IgvmAttestTestConfig::AkCertPersistentAcrossBoot, ), ( diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index e10c272abb..7f344f6729 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -705,7 +705,7 @@ async fn cvm_tpm_guest_tests( /// failure to the host via `complete_start_vtl0`, and the host terminates /// the VM. /// -/// The test function name contains `skip_hwunseal` so the per-VM agent +/// The test function name contains `skip_hw_unseal` so the per-VM agent /// registry in test_igvm_agent_rpc_server matches it to the /// `KeyReleaseFailureSkipHwUnsealing` configuration. #[cfg(windows)] From f0db87dfedf04996a8e322a1bd27b4f028f02c72 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 18:27:32 +0000 Subject: [PATCH 07/16] x Signed-off-by: Ming-Wei Shih --- .../src/rpc/igvm_agent.rs | 75 ++++++++++++++++++- .../vmm_tests/tests/tests/multiarch/tpm.rs | 34 ++------- 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index 2d70c46d42..6dc27346a9 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -69,18 +69,85 @@ fn config_for_vm_name(vm_name: &str) -> Option { /// pattern is unique. const KNOWN_TEST_CONFIGS: &[(&str, IgvmAttestTestConfig)] = &[ ( - "ak_cert_retry", + "ubuntu_2504_server_x64_ak_cert_retry", IgvmAttestTestConfig::AkCertRequestFailureAndRetry, ), ( - "ak_cert_cache", + "windows_datacenter_core_2022_x64_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "ubuntu_2504_server_x64_vbs_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "windows_datacenter_core_2025_x64_prepped_vbs_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "ubuntu_2504_server_x64_snp_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "windows_datacenter_core_2025_x64_prepped_snp_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "ubuntu_2504_server_x64_tdx_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "windows_datacenter_core_2025_x64_prepped_tdx_ak_cert_retry", + IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + ), + ( + "ubuntu_2504_server_x64_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "windows_datacenter_core_2022_x64_ak_cert_cache", IgvmAttestTestConfig::AkCertPersistentAcrossBoot, ), ( - "skip_hw_unseal", + "ubuntu_2504_server_x64_vbs_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "windows_datacenter_core_2025_x64_prepped_vbs_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "ubuntu_2504_server_x64_snp_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "windows_datacenter_core_2025_x64_prepped_snp_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "ubuntu_2504_server_x64_tdx_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "windows_datacenter_core_2025_x64_prepped_tdx_ak_cert_cache", + IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + ), + ( + "ubuntu_2504_server_x64_snp_skip_hw_unseal", + IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, + ), + ( + "windows_datacenter_core_2025_x64_prepped_snp_skip_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing, ), - ("use_hw_unseal", IgvmAttestTestConfig::KeyReleaseFailure), + ( + "ubuntu_2504_server_x64_snp_use_hw_unseal", + IgvmAttestTestConfig::KeyReleaseFailure, + ), + ( + "windows_datacenter_core_2025_x64_prepped_snp_use_hw_unseal", + IgvmAttestTestConfig::KeyReleaseFailure, + ), ]; for &(pattern, config) in KNOWN_TEST_CONFIGS { diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 7f344f6729..fdaf13d1ca 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -710,15 +710,14 @@ async fn cvm_tpm_guest_tests( /// `KeyReleaseFailureSkipHwUnsealing` configuration. #[cfg(windows)] #[vmm_test( - hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TPM_GUEST_TESTS_LINUX_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], - hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TPM_GUEST_TESTS_WINDOWS_X64, TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(ubuntu_2504_server_x64))[TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], + hyperv_openhcl_uefi_x64[snp](vhd(windows_datacenter_core_2025_x64_prepped))[TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64], )] -async fn skip_hw_unseal( +async fn skip_hw_unseal( config: PetriVmBuilder, - extra_deps: (ResolvedArtifact, ResolvedArtifact), + extra_deps: (ResolvedArtifact,), ) -> anyhow::Result<()> { - let os_flavor = config.os_flavor(); - let (tpm_guest_tests_artifact, rpc_server_artifact) = extra_deps; + let (rpc_server_artifact,) = extra_deps; let rpc_server_path = rpc_server_artifact.get(); let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; @@ -730,26 +729,9 @@ async fn skip_hw_unseal( .run() .await?; - let guest_binary_path = match os_flavor { - OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, - OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, - _ => unreachable!(), - }; - let host_binary_path = tpm_guest_tests_artifact.get(); - let tpm_guest_tests = - TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) - .await?; - - // First boot: KEY_RELEASE succeeds. Verify AK cert is present. - let expected_hex = expected_ak_cert_hex(); - let ak_cert_output = tpm_guest_tests - .read_ak_cert_with_expected_hex(expected_hex.as_str()) - .await?; - - ensure!( - ak_cert_output.contains("AK certificate matches expected value"), - format!("{ak_cert_output}") - ); + // First boot: KEY_RELEASE succeeds. TPM state is sealed with hardware + // key protector. No guest-side verification needed — just let the boot + // complete so the VMGS state is populated. // Reboot: triggers second KEY_RELEASE which fails with skip_hw_unsealing. // VMGS unlock will fail because hardware unsealing fallback is skipped. From 22ad3e395826384317f8f989adcb24ca5cac4fe3 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Tue, 10 Mar 2026 21:24:57 +0000 Subject: [PATCH 08/16] debug Signed-off-by: Ming-Wei Shih --- vm/devices/get/guest_emulation_device/src/lib.rs | 2 +- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 14 +++++++++++--- .../get/test_igvm_agent_rpc_server/src/main.rs | 2 +- .../src/rpc/igvm_agent.rs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 4075990cce..756ff24ef4 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -271,7 +271,7 @@ impl GuestEmulationDevice { waiting_for_vtl0_start: Vec::new(), last_save_restore_buf_len: 0, igvm_agent_setting, - igvm_agent: TestIgvmAgent::new(), + igvm_agent: TestIgvmAgent::new("openvmm"), test_gsp_by_id, } } diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 5fa253c724..657ee95aa8 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -98,6 +98,8 @@ pub enum KeyReleaseError { /// Test IGVM agent includes states that need to be persisted. #[derive(Debug, Clone, Default)] pub struct TestIgvmAgent { + /// VM name for log correlation. + vm_name: String, /// Optional RSA private key used for attestation. secret_key: Option, /// Optional DES key @@ -192,9 +194,13 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan } impl TestIgvmAgent { - /// Create an instance. - pub fn new() -> Self { + /// Create an instance associated with the given VM name. + /// + /// The `vm_name` is included in all tracing output so that log + /// messages from the library can be correlated with a specific VM. + pub fn new(vm_name: impl Into) -> Self { Self { + vm_name: vm_name.into(), secret_key: None, des_key: None, plan: None, @@ -210,7 +216,7 @@ impl TestIgvmAgent { return; } - tracing::info!("install the scripted plan for test IGVM Agent"); + tracing::info!(vm_name = %self.vm_name, "install the scripted plan for test IGVM Agent"); match setting { IgvmAgentTestSetting::TestPlan(plan) => { @@ -236,6 +242,8 @@ impl TestIgvmAgent { /// Request handler. pub fn handle_request(&mut self, request_bytes: &[u8]) -> Result<(Vec, u32), Error> { + let _span = tracing::info_span!("igvm_agent", vm_name = %self.vm_name).entered(); + let request = IgvmAttestRequestBase::read_from_prefix(request_bytes) .map_err(|_| Error::InvalidIgvmAttestRequest)? .0; // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759) diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs index b47fae069a..aafabf32f4 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/main.rs @@ -61,7 +61,7 @@ fn main() -> ExitCode { let args = Args::parse(); let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("test_igvm_agent_rpc_server=info")); + .unwrap_or_else(|_| EnvFilter::new("test_igvm_agent_rpc_server=info,test_igvm_agent_lib=info")); let _ = fmt() .with_env_filter(filter) diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index 6dc27346a9..ad02d2e695 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -180,7 +180,7 @@ pub fn process_igvm_attest(vm_name: &str, report: &[u8]) -> TestAgentResult Date: Tue, 10 Mar 2026 23:56:23 +0000 Subject: [PATCH 09/16] test Signed-off-by: Ming-Wei Shih --- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 11 ++- .../vmm_tests/tests/tests/multiarch/tpm.rs | 74 ++++++++++++------- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 657ee95aa8..2dab1170e3 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -155,9 +155,12 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan match test_config { IgvmAttestTestConfig::AkCertRequestFailureAndRetry => { + // Inject enough failures to trigger the retry logic in the agent for different platforms. + // For example, HyperV Linux tests might reboot the VM twice in the first boot. plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, VecDeque::from([ + IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondSuccess, @@ -165,9 +168,15 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ); } IgvmAttestTestConfig::AkCertPersistentAcrossBoot => { + // Inject enough NoResponse actions to simulate the AK cert request failing due to a missing AK cert for different platforms. + // For example, HyperV Linux tests might reboot the VM twice in the first boot. plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, - VecDeque::from([IgvmAgentAction::RespondSuccess, IgvmAgentAction::NoResponse]), + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::NoResponse, + IgvmAgentAction::NoResponse, + ]), ); } IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index fdaf13d1ca..2d1e9e561a 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -146,7 +146,7 @@ impl<'a> TpmGuestTests<'a> { "--expected-data-hex", expected_hex, "--retry", - "3", + "5", ]) .read() .await @@ -159,7 +159,7 @@ impl<'a> TpmGuestTests<'a> { "--expected-data-hex", expected_hex, "--retry", - "3", + "5", ]) .read() .await @@ -393,21 +393,13 @@ async fn ak_cert_cache( .await?; let guest_binary_path = match os_flavor { - OsFlavor::Linux => { - // First boot - AK cert request will be served by RPC agent. - // Second boot - AK cert request will be bypassed (cached). - TPM_GUEST_TESTS_LINUX_GUEST_PATH - } - OsFlavor::Windows => { - // First boot - AK cert request will be served by RPC agent. - // Reboot so second boot uses cached cert. - agent.reboot().await?; - agent = vm.wait_for_reset().await?; - TPM_GUEST_TESTS_WINDOWS_GUEST_PATH - } + OsFlavor::Linux => TPM_GUEST_TESTS_LINUX_GUEST_PATH, + OsFlavor::Windows => TPM_GUEST_TESTS_WINDOWS_GUEST_PATH, _ => unreachable!(), }; + agent.reboot().await?; + let host_binary_path = tpm_guest_tests_artifact.get(); let tpm_guest_tests = TpmGuestTests::send_tpm_guest_tests(&agent, host_binary_path, guest_binary_path, os_flavor) @@ -740,19 +732,47 @@ async fn skip_hw_unseal( // the VM. agent.reboot().await?; - // Phase 1: Consume the Reset halt event. For isolated CVMs, this also - // confirms the VM reaches Running state after the reset. - let halt_reason = vm.wait_for_halt().await?; - assert_eq!( - halt_reason, - PetriHaltReason::Reset, - "Expected reset from reboot" - ); - - // Phase 2: Wait for the VM to be terminated by the host after underhill - // fails to start on the second boot. - let halt_reason = vm.wait_for_teardown().await?; - tracing::info!("Second boot halt reason: {halt_reason:?}"); + // Wait for the VM to reset and then fail on the second boot. + // + // Depending on timing, two outcomes are possible: + // + // 1. wait_for_halt() returns Reset (the CVM restart check saw the VM + // briefly reach Running state before underhill failed), and then the + // subsequent wait_for_teardown() fails because the VM termination + // does not produce a recognized halt event. + // + // 2. wait_for_halt() itself fails because the VM never reached Running + // state within the allowed CVM restart timeout (underhill failed + // before Hyper-V reported the VM as Running). + // + // Both outcomes confirm the expected behavior: the VM cannot boot after + // hardware unsealing is skipped. + match vm.wait_for_halt().await { + Ok(PetriHaltReason::Reset) => { + tracing::info!("Got reset event; waiting for second boot termination..."); + // The VM termination after second boot failure may not produce + // a recognized Hyper-V halt event (e.g., event 18620 from + // Hyper-V-Chipset is not in the standard halt event filter). + // Ignore the error — the VM going off IS the expected outcome. + match vm.wait_for_teardown().await { + Ok(halt_reason) => { + tracing::info!("Second boot halt reason: {halt_reason:?}"); + } + Err(e) => { + tracing::info!("Second boot terminated as expected: {e:#}"); + } + } + } + Ok(other) => { + anyhow::bail!("Expected Reset or VM start failure, got {other:?}"); + } + Err(e) => { + // The VM failed to restart within the allowed time, which is + // the expected behavior when underhill cannot unlock VMGS. + tracing::info!("VM failed to restart as expected: {e:#}"); + vm.teardown().await?; + } + } Ok(()) } From 97b8119fa653a0d8f95384d03479907f4820051c Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Wed, 11 Mar 2026 01:45:20 +0000 Subject: [PATCH 10/16] x Signed-off-by: Ming-Wei Shih --- vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 2d1e9e561a..07f2a7729e 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -385,7 +385,7 @@ async fn ak_cert_cache( let rpc_server_path = rpc_server_artifact.get(); let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - let (mut vm, mut agent) = config + let (vm, agent) = config .with_tpm(true) .with_tpm_state_persistence(true) .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) From 4461cfb94f652cb658ff8a927d07d923fa2d3f03 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Wed, 11 Mar 2026 18:34:16 +0000 Subject: [PATCH 11/16] x Signed-off-by: Ming-Wei Shih --- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 6 ------ vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs | 5 +++++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 2dab1170e3..1b785296c1 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -155,12 +155,9 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan match test_config { IgvmAttestTestConfig::AkCertRequestFailureAndRetry => { - // Inject enough failures to trigger the retry logic in the agent for different platforms. - // For example, HyperV Linux tests might reboot the VM twice in the first boot. plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, VecDeque::from([ - IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondFailure, IgvmAgentAction::RespondSuccess, @@ -168,14 +165,11 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ); } IgvmAttestTestConfig::AkCertPersistentAcrossBoot => { - // Inject enough NoResponse actions to simulate the AK cert request failing due to a missing AK cert for different platforms. - // For example, HyperV Linux tests might reboot the VM twice in the first boot. plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, VecDeque::from([ IgvmAgentAction::RespondSuccess, IgvmAgentAction::NoResponse, - IgvmAgentAction::NoResponse, ]), ); } diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 07f2a7729e..eedc904fd8 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -398,7 +398,9 @@ async fn ak_cert_cache( _ => unreachable!(), }; + tracing::info!("first boot"); agent.reboot().await?; + tracing::info!("second boot"); let host_binary_path = tpm_guest_tests_artifact.get(); let tpm_guest_tests = @@ -724,6 +726,7 @@ async fn skip_hw_unseal( // First boot: KEY_RELEASE succeeds. TPM state is sealed with hardware // key protector. No guest-side verification needed — just let the boot // complete so the VMGS state is populated. + tracing::info!("first boot"); // Reboot: triggers second KEY_RELEASE which fails with skip_hw_unsealing. // VMGS unlock will fail because hardware unsealing fallback is skipped. @@ -732,6 +735,8 @@ async fn skip_hw_unseal( // the VM. agent.reboot().await?; + tracing::info!("second boot"); + // Wait for the VM to reset and then fail on the second boot. // // Depending on timing, two outcomes are possible: From ba0817860536c47d5378f94e635dd76c5ede4a31 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Wed, 11 Mar 2026 19:58:45 +0000 Subject: [PATCH 12/16] update Signed-off-by: Ming-Wei Shih --- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 16 +++++++++------- vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 1b785296c1..f9e3ff3580 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -167,10 +167,7 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan IgvmAttestTestConfig::AkCertPersistentAcrossBoot => { plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, - VecDeque::from([ - IgvmAgentAction::RespondSuccess, - IgvmAgentAction::NoResponse, - ]), + VecDeque::from([IgvmAgentAction::RespondSuccess, IgvmAgentAction::NoResponse]), ); } IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { @@ -278,7 +275,7 @@ impl TestIgvmAgent { let (response, length) = if let Some(action) = self.take_next_action(request.header.request_type) { - // If a plan is provided and has a queued action for this request type, + // If a plan is installed and has a queued action for this request type, // execute it. This allows tests to force success/no-response, etc. match action { IgvmAgentAction::NoResponse => { @@ -458,9 +455,14 @@ impl TestIgvmAgent { } } } + } else if self.plan.is_some() { + // A plan is installed but has no more actions for this request type. + // Return NoResponse to avoid silently falling back to normal mode. + tracing::info!(?request.header.request_type, "Plan exhausted: NoResponse"); + (vec![], 0) } else { - // If no plan is provided, fall back to the default behavior that - // always return valid responses. + // No plan is installed — fall back to default behavior that + // always returns valid responses. match request.header.request_type { IgvmAttestRequestType::AK_CERT_REQUEST => { tracing::info!("Send a response for AK_CERT_REQUEST"); diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index eedc904fd8..4ed4800e4c 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -399,7 +399,9 @@ async fn ak_cert_cache( }; tracing::info!("first boot"); + agent.reboot().await?; + tracing::info!("second boot"); let host_binary_path = tpm_guest_tests_artifact.get(); From d6a1926fb8ae25dacb342a50384c31f7a261cbc2 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Wed, 11 Mar 2026 21:20:06 +0000 Subject: [PATCH 13/16] x Signed-off-by: Ming-Wei Shih --- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index f9e3ff3580..17b07c4faa 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -119,8 +119,12 @@ pub enum IgvmAgentAction { RespondFailure, /// Emit a response that indicates a protocol error with skip_hw_unsealing signal. RespondFailureSkipHwUnsealing, - /// Skip responding to simulate a timeout. + /// Skip responding to simulate a timeout (consumed once). NoResponse, + /// Skip responding for this and all subsequent requests of the same type. + /// Unlike [`NoResponse`](Self::NoResponse), this action is never consumed + /// from the queue. + AlwaysNoResponse, } /// IGVM Agent test plan specifying scripted actions for a request type. @@ -167,7 +171,10 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan IgvmAttestTestConfig::AkCertPersistentAcrossBoot => { plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, - VecDeque::from([IgvmAgentAction::RespondSuccess, IgvmAgentAction::NoResponse]), + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::AlwaysNoResponse, + ]), ); } IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { @@ -176,6 +183,7 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan VecDeque::from([ IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondFailureSkipHwUnsealing, + IgvmAgentAction::AlwaysNoResponse, ]), ); } @@ -185,6 +193,7 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan VecDeque::from([ IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondFailure, + IgvmAgentAction::AlwaysNoResponse, ]), ); } @@ -231,13 +240,21 @@ impl TestIgvmAgent { } /// Take the next scripted action for the given request type, if any. + /// + /// [`IgvmAgentAction::AlwaysNoResponse`] is sticky: it is returned but + /// never removed from the queue, so every subsequent call for the same + /// request type will keep returning it. pub fn take_next_action( &mut self, request_type: IgvmAttestRequestType, ) -> Option { // Fast path: no plan installed. let plan = self.plan.as_mut()?; - plan.get_mut(&request_type)?.pop_front() + let queue = plan.get_mut(&request_type)?; + match queue.front()? { + IgvmAgentAction::AlwaysNoResponse => Some(IgvmAgentAction::AlwaysNoResponse), + _ => queue.pop_front(), + } } /// Request handler. @@ -278,7 +295,7 @@ impl TestIgvmAgent { // If a plan is installed and has a queued action for this request type, // execute it. This allows tests to force success/no-response, etc. match action { - IgvmAgentAction::NoResponse => { + IgvmAgentAction::NoResponse | IgvmAgentAction::AlwaysNoResponse => { tracing::info!(?request.header.request_type, "Test plan: NoResponse"); (vec![], 0) } @@ -455,14 +472,11 @@ impl TestIgvmAgent { } } } - } else if self.plan.is_some() { - // A plan is installed but has no more actions for this request type. - // Return NoResponse to avoid silently falling back to normal mode. - tracing::info!(?request.header.request_type, "Plan exhausted: NoResponse"); - (vec![], 0) } else { - // No plan is installed — fall back to default behavior that - // always returns valid responses. + // No scripted action for this request type (either no plan is + // installed, or the plan does not cover this request type / is + // exhausted). Fall back to the default behavior that always + // returns valid responses. match request.header.request_type { IgvmAttestRequestType::AK_CERT_REQUEST => { tracing::info!("Send a response for AK_CERT_REQUEST"); From e0f9bb4cce309ed619161037f0a5214e247a5ab6 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 12 Mar 2026 01:54:19 +0000 Subject: [PATCH 14/16] fix Signed-off-by: Ming-Wei Shih --- vm/devices/get/get_resources/src/lib.rs | 8 ++++++ vm/devices/get/test_igvm_agent_lib/src/lib.rs | 27 +++++++++++++++++++ .../src/rpc/igvm_agent.rs | 8 +++--- vm/devices/tpm/tpm_guest_tests/src/main.rs | 2 +- .../vmm_tests/tests/tests/multiarch/tpm.rs | 7 ++--- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index 84fe2c2231..eb436b1768 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -250,6 +250,14 @@ pub mod ged { pub enum IgvmAttestTestConfig { /// Config for testing AK cert retry after failure. AkCertRequestFailureAndRetry, + /// Config for testing AK cert retry after failure — extended plan. + /// + /// Windows guests with CVM isolation (SNP, TDX) or VBS generate + /// additional boot-time AK cert requests (background retries + /// during the initial boot and the initial_reboot) that consume + /// plan actions before the test code runs. This plan has extra + /// failure actions to absorb those requests. + AkCertRequestFailureAndRetryExtended, /// Config for testing AK cert persistency across boots. AkCertPersistentAcrossBoot, /// Config for testing skip hardware unsealing signal from IGVMAgent. diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 17b07c4faa..18164044db 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -168,6 +168,25 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ]), ); } + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended => { + // Windows CVM guests (SNP, TDX, VBS) can generate multiple + // boot-time AK_CERT_REQUEST calls (background retries during + // the initial boot and the initial_reboot). Six failures + // ensure the SUCCESS action is never consumed during boot, + // so it remains available for the guest test. + plan.insert( + IgvmAttestRequestType::AK_CERT_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondSuccess, + ]), + ); + } IgvmAttestTestConfig::AkCertPersistentAcrossBoot => { plan.insert( IgvmAttestRequestType::AK_CERT_REQUEST, @@ -178,9 +197,13 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ); } IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { + // Two RespondSuccess entries to cover the initial boot and + // the initial_reboot (guest quirk) — both consume a + // KEY_RELEASE during run() before the test code starts. plan.insert( IgvmAttestRequestType::KEY_RELEASE_REQUEST, VecDeque::from([ + IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondFailureSkipHwUnsealing, IgvmAgentAction::AlwaysNoResponse, @@ -188,9 +211,13 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ); } IgvmAttestTestConfig::KeyReleaseFailure => { + // Two RespondSuccess entries to cover the initial boot and + // the initial_reboot (guest quirk) — both consume a + // KEY_RELEASE during run() before the test code starts. plan.insert( IgvmAttestRequestType::KEY_RELEASE_REQUEST, VecDeque::from([ + IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondSuccess, IgvmAgentAction::RespondFailure, IgvmAgentAction::AlwaysNoResponse, diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index ad02d2e695..96f481f715 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -82,7 +82,7 @@ fn config_for_vm_name(vm_name: &str) -> Option { ), ( "windows_datacenter_core_2025_x64_prepped_vbs_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "ubuntu_2504_server_x64_snp_ak_cert_retry", @@ -90,15 +90,15 @@ fn config_for_vm_name(vm_name: &str) -> Option { ), ( "windows_datacenter_core_2025_x64_prepped_snp_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "ubuntu_2504_server_x64_tdx_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "windows_datacenter_core_2025_x64_prepped_tdx_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "ubuntu_2504_server_x64_ak_cert_cache", diff --git a/vm/devices/tpm/tpm_guest_tests/src/main.rs b/vm/devices/tpm/tpm_guest_tests/src/main.rs index 6fb46a1a35..e03b505454 100644 --- a/vm/devices/tpm/tpm_guest_tests/src/main.rs +++ b/vm/devices/tpm/tpm_guest_tests/src/main.rs @@ -44,7 +44,7 @@ const MAX_NV_READ_SIZE: usize = 4096; const MAX_ATTESTATION_READ_SIZE: usize = 2600; const GUEST_INPUT_SIZE: u16 = 64; const GUEST_INPUT_AUTH: u64 = 0; -const AK_CERT_RETRY_DELAY_MS: u64 = 200; +const AK_CERT_RETRY_DELAY_MS: u64 = 1000; #[derive(Debug, Default)] struct Config { diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index 4ed4800e4c..b49bbe6ae8 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -146,7 +146,7 @@ impl<'a> TpmGuestTests<'a> { "--expected-data-hex", expected_hex, "--retry", - "5", + "10", ]) .read() .await @@ -159,7 +159,7 @@ impl<'a> TpmGuestTests<'a> { "--expected-data-hex", expected_hex, "--retry", - "5", + "10", ]) .read() .await @@ -385,7 +385,7 @@ async fn ak_cert_cache( let rpc_server_path = rpc_server_artifact.get(); let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - let (vm, agent) = config + let (mut vm, agent) = config .with_tpm(true) .with_tpm_state_persistence(true) .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) @@ -401,6 +401,7 @@ async fn ak_cert_cache( tracing::info!("first boot"); agent.reboot().await?; + let agent = vm.wait_for_reset().await?; tracing::info!("second boot"); From a2b183fd7ac5891ec89005b2cf81a8cf11e87582 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 12 Mar 2026 03:15:19 +0000 Subject: [PATCH 15/16] x Signed-off-by: Ming-Wei Shih --- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 2 +- .../get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index 18164044db..d1f5f95cc6 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -169,7 +169,7 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ); } IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended => { - // Windows CVM guests (SNP, TDX, VBS) can generate multiple + // CVM guests (SNP, TDX, VBS) with Hyper-V can generate multiple // boot-time AK_CERT_REQUEST calls (background retries during // the initial boot and the initial_reboot). Six failures // ensure the SUCCESS action is never consumed during boot, diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index 96f481f715..bccb4935f8 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -70,15 +70,15 @@ fn config_for_vm_name(vm_name: &str) -> Option { const KNOWN_TEST_CONFIGS: &[(&str, IgvmAttestTestConfig)] = &[ ( "ubuntu_2504_server_x64_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "windows_datacenter_core_2022_x64_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "ubuntu_2504_server_x64_vbs_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "windows_datacenter_core_2025_x64_prepped_vbs_ak_cert_retry", @@ -86,7 +86,7 @@ fn config_for_vm_name(vm_name: &str) -> Option { ), ( "ubuntu_2504_server_x64_snp_ak_cert_retry", - IgvmAttestTestConfig::AkCertRequestFailureAndRetry, + IgvmAttestTestConfig::AkCertRequestFailureAndRetryExtended, ), ( "windows_datacenter_core_2025_x64_prepped_snp_ak_cert_retry", From 026a1eafa3ad484d968fb83923411e55724adc8a Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 12 Mar 2026 06:24:22 +0000 Subject: [PATCH 16/16] fix Signed-off-by: Ming-Wei Shih --- .../src/igvm_attest/get.rs | 2 +- vm/devices/get/get_resources/src/lib.rs | 8 ++++++++ vm/devices/get/test_igvm_agent_lib/src/lib.rs | 16 ++++++++++++++++ .../src/rpc/igvm_agent.rs | 16 ++++++++-------- vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs | 11 ++--------- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 7d65d65970..08f6ab0169 100644 --- a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs +++ b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs @@ -230,7 +230,7 @@ impl IgvmAttestRequestDataExt { } /// Bitmap indicates a signal to requestor -/// 0 - IGVM_SIGNAL_RETRY_RCOMMENDED_BIT: Retry recommendation +/// 0 - IGVM_SIGNAL_RETRY_RECOMMENDED_BIT: Retry recommendation /// 1 - IGVM_SIGNAL_SKIP_HW_UNSEALING_RECOMMENDED_BIT: Skip hardware unsealing #[bitfield(u32)] #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] diff --git a/vm/devices/get/get_resources/src/lib.rs b/vm/devices/get/get_resources/src/lib.rs index eb436b1768..f217d26781 100644 --- a/vm/devices/get/get_resources/src/lib.rs +++ b/vm/devices/get/get_resources/src/lib.rs @@ -260,6 +260,14 @@ pub mod ged { AkCertRequestFailureAndRetryExtended, /// Config for testing AK cert persistency across boots. AkCertPersistentAcrossBoot, + /// Config for testing AK cert persistency across boots — extended + /// plan. + /// + /// VBS guests send a background AK cert request during the initial + /// boot whose response may be lost when the VM resets for the + /// `initial_reboot`. The extra `RespondSuccess` absorbs that + /// request so the second boot still gets a cert provisioned. + AkCertPersistentAcrossBootExtended, /// Config for testing skip hardware unsealing signal from IGVMAgent. KeyReleaseFailureSkipHwUnsealing, /// Config for testing key release failure without skip_hw_unsealing diff --git a/vm/devices/get/test_igvm_agent_lib/src/lib.rs b/vm/devices/get/test_igvm_agent_lib/src/lib.rs index d1f5f95cc6..d65031a254 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -196,6 +196,22 @@ fn test_config_to_plan(test_config: &IgvmAttestTestConfig) -> IgvmAgentTestPlan ]), ); } + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended => { + // VBS guests generate a background AK cert request during the + // initial boot. The response is often lost when the VM resets + // for the `initial_reboot`, consuming the first action. The + // extra RespondSuccess ensures the second boot can still + // provision the cert via RPC, and the third boot validates that + // the cert is cached. + plan.insert( + IgvmAttestRequestType::AK_CERT_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::AlwaysNoResponse, + ]), + ); + } IgvmAttestTestConfig::KeyReleaseFailureSkipHwUnsealing => { // Two RespondSuccess entries to cover the initial boot and // the initial_reboot (guest quirk) — both consume a diff --git a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs index bccb4935f8..1dabf07456 100644 --- a/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs +++ b/vm/devices/get/test_igvm_agent_rpc_server/src/rpc/igvm_agent.rs @@ -102,35 +102,35 @@ fn config_for_vm_name(vm_name: &str) -> Option { ), ( "ubuntu_2504_server_x64_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "windows_datacenter_core_2022_x64_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "ubuntu_2504_server_x64_vbs_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "windows_datacenter_core_2025_x64_prepped_vbs_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "ubuntu_2504_server_x64_snp_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "windows_datacenter_core_2025_x64_prepped_snp_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "ubuntu_2504_server_x64_tdx_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "windows_datacenter_core_2025_x64_prepped_tdx_ak_cert_cache", - IgvmAttestTestConfig::AkCertPersistentAcrossBoot, + IgvmAttestTestConfig::AkCertPersistentAcrossBootExtended, ), ( "ubuntu_2504_server_x64_snp_skip_hw_unseal", diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs index b49bbe6ae8..39874766bb 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -385,7 +385,7 @@ async fn ak_cert_cache( let rpc_server_path = rpc_server_artifact.get(); let _rpc_guard = ensure_rpc_server_running(rpc_server_path)?; - let (mut vm, agent) = config + let (mut vm, mut agent) = config .with_tpm(true) .with_tpm_state_persistence(true) .with_guest_state_lifetime(PetriGuestStateLifetime::Disk) @@ -398,12 +398,8 @@ async fn ak_cert_cache( _ => unreachable!(), }; - tracing::info!("first boot"); - agent.reboot().await?; - let agent = vm.wait_for_reset().await?; - - tracing::info!("second boot"); + agent = vm.wait_for_reset().await?; let host_binary_path = tpm_guest_tests_artifact.get(); let tpm_guest_tests = @@ -729,7 +725,6 @@ async fn skip_hw_unseal( // First boot: KEY_RELEASE succeeds. TPM state is sealed with hardware // key protector. No guest-side verification needed — just let the boot // complete so the VMGS state is populated. - tracing::info!("first boot"); // Reboot: triggers second KEY_RELEASE which fails with skip_hw_unsealing. // VMGS unlock will fail because hardware unsealing fallback is skipped. @@ -738,8 +733,6 @@ async fn skip_hw_unseal( // the VM. agent.reboot().await?; - tracing::info!("second boot"); - // Wait for the VM to reset and then fail on the second boot. // // Depending on timing, two outcomes are possible: