From 6151c2855abf9115b39fa06623941428420fb37d Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Wed, 13 May 2026 23:36:56 +0000 Subject: [PATCH 1/2] Support hardware sealing policy for stateless mode 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 | 681 ++++++++++++++---- 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 | 19 + 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 + .../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 + vm/devices/tpm/tpm_guest_tests/src/main.rs | 169 +++++ .../vmm_tests/tests/tests/multiarch/tpm.rs | 355 +++++++++ 25 files changed, 1696 insertions(+), 182 deletions(-) diff --git a/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs b/openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs index 08f6ab0169..1f263a448a 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 92dab5a09e..59a0f8b134 100644 --- a/openhcl/underhill_attestation/src/hardware_key_sealing.rs +++ b/openhcl/underhill_attestation/src/hardware_key_sealing.rs @@ -14,6 +14,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")] @@ -39,29 +41,44 @@ 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 output = crypto::kdf::kbkdf_hmac_sha256( &hardware_secret, - vm_config.as_bytes(), + vm_config_json.as_bytes(), label, vmgs::AES_CBC_KEY_LENGTH + vmgs::HMAC_SHA_256_KEY_LENGTH, ) @@ -74,7 +91,7 @@ impl HardwareDerivedKeys { hmac_key.copy_from_slice(&output[vmgs::AES_CBC_KEY_LENGTH..]); Ok(Self { - tcb_version, + policy, aes_key, hmac_key, }) @@ -102,9 +119,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]; @@ -180,17 +198,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, @@ -198,30 +215,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 bf5b46dea2..abc12be453 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 dbb1205083..a4a7baaff7 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -23,6 +23,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; @@ -49,6 +55,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; @@ -79,6 +87,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)] @@ -92,9 +102,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")] @@ -106,7 +116,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")] @@ -115,6 +125,10 @@ enum GetDerivedKeysError { DeriveIngressKey(#[source] crypto::kdf::KdfError), #[error("failed to derive an egress key")] DeriveEgressKey(#[source] crypto::kdf::KdfError), + #[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)] @@ -154,9 +168,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. @@ -224,6 +240,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`]. @@ -237,7 +255,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. @@ -262,21 +279,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"); @@ -325,27 +359,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" @@ -362,9 +422,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 @@ -382,13 +443,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, @@ -463,9 +525,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 { @@ -477,31 +550,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). @@ -528,6 +614,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, ) @@ -584,6 +671,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, @@ -670,6 +758,7 @@ async fn unlock_vmgs_data_store( vmgs, key_protector, key_protector_by_id, + hardware_key_protector.as_ref(), bios_guid, key_protector_settings, ) @@ -704,9 +793,10 @@ async fn get_derived_keys( is_encrypted: bool, ingress_rsa_kek: Option<&RsaKeyPair>, 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!( @@ -716,14 +806,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, @@ -889,9 +971,24 @@ 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 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, + ) + .await; + + 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) => { @@ -907,17 +1004,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 @@ -965,27 +1093,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); @@ -1003,9 +1182,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 { @@ -1025,34 +1271,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"); @@ -1078,6 +1302,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, }); } @@ -1117,6 +1342,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, }); } @@ -1272,6 +1498,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, }) } @@ -1368,6 +1595,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> { @@ -1376,10 +1604,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 @@ -1389,7 +1621,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 @@ -1400,7 +1632,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)?; } } @@ -1412,6 +1644,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; @@ -1419,14 +1652,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; } } @@ -1456,14 +1699,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) @@ -1668,6 +1918,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(), } @@ -1696,6 +1947,7 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, + None, key_protector_settings, bios_guid, ) @@ -1721,6 +1973,7 @@ mod tests { &mut key_protector, &mut key_protector_by_id, None, + None, key_protector_settings, bios_guid, ) @@ -1763,6 +2016,7 @@ mod tests { false, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1819,6 +2073,7 @@ mod tests { true, &mut new_key_protector, &mut new_key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1887,6 +2142,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -1961,6 +2217,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2040,6 +2297,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2094,6 +2352,7 @@ mod tests { true, &mut key_protector, &mut key_protector_by_id, + None, Some(derived_keys), key_protector_settings, bios_guid, @@ -2181,6 +2440,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2195,6 +2455,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; @@ -2218,6 +2650,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + None, bios_guid, key_protector_settings, ) @@ -2262,6 +2695,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + None, bios_guid, key_protector_settings, ) @@ -2310,6 +2744,7 @@ mod tests { &mut vmgs, &mut key_protector, &mut key_protector_by_id, + Some(&HardwareKeyProtector::new_zeroed()), bios_guid, key_protector_settings, ) @@ -2386,7 +2821,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()); @@ -2472,7 +2907,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()); @@ -2557,7 +2992,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, @@ -2642,7 +3077,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 9eb32a722c..d54cb9ace7 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -105,6 +105,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; @@ -1974,6 +1975,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) @@ -1991,7 +2009,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(), @@ -2858,14 +2877,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(), @@ -3650,6 +3687,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 486e3fadb1..686eee6485 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -1188,6 +1188,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 6395e8323e..4e16dea8dd 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -16,6 +16,7 @@ use crate::NoPetriVmInspector; use crate::OpenHclServicingFlags; use crate::OpenvmmLogConfig; use crate::PetriHaltReason; +use crate::PetriHardwareSealingPolicy; use crate::PetriVmConfig; use crate::PetriVmResources; use crate::PetriVmRuntime; @@ -420,6 +421,24 @@ impl PetriVmmBackend for HyperVPetriBackend { hyperv_serial_log_task(driver.clone(), serial_pipe_path, serial_log_file), )); + // Apply hardware sealing policy override (other TPM/isolation + // configuration is handled by HyperVNewCustomVMArgs::from_config). + if properties.is_openhcl + && let Some(tpm) = config.tpm.as_ref() + && tpm.hardware_sealing_policy != PetriHardwareSealingPolicy::Default + { + vm.set_management_vtl_encryption_policy(match tpm.hardware_sealing_policy { + PetriHardwareSealingPolicy::HashPolicy => { + powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsHashPolicy + } + PetriHardwareSealingPolicy::SignerPolicy => { + powershell::HyperVManagementVtlEncryptionPolicy::HardwareSealedSecretsSignerPolicy + } + PetriHardwareSealingPolicy::Default => unreachable!(), + }) + .await?; + } + vm.start().await?; Ok(( diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index 5e565b9840..8b202129fe 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -1966,3 +1966,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 86ed362cef..b4ac6f9776 100644 --- a/petri/src/vm/hyperv/vm.rs +++ b/petri/src/vm/hyperv/vm.rs @@ -598,6 +598,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 661e50827d..0cff179560 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1411,6 +1411,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. @@ -2274,16 +2284,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 271a1c4859..9d9e0a2634 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -1031,6 +1031,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/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 756ff24ef4..c280ec7bab 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 0460f9ddb6..a057bcbb98 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 c07ccf8c2c..dd793404d4 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -240,6 +240,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/tpm/tpm_guest_tests/src/main.rs b/vm/devices/tpm/tpm_guest_tests/src/main.rs index e03b505454..4cb6614449 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 29dc0484f9..60aab147e9 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/tpm.rs @@ -6,6 +6,7 @@ use anyhow::ensure; use petri::PetriGuestStateLifetime; #[cfg(windows)] 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. @@ -921,3 +1004,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 d9dbc397e6f85cadb1ba13dabc72a26d5d5e1b23 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 14 May 2026 00:51:46 +0000 Subject: [PATCH 2/2] fix Signed-off-by: Ming-Wei Shih --- .../src/hardware_key_sealing.rs | 28 ++++++---- openhcl/underhill_attestation/src/lib.rs | 51 +++++++++++++++++-- vm/devices/get/test_igvm_agent_lib/src/lib.rs | 9 ---- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/openhcl/underhill_attestation/src/hardware_key_sealing.rs b/openhcl/underhill_attestation/src/hardware_key_sealing.rs index 59a0f8b134..39020fb892 100644 --- a/openhcl/underhill_attestation/src/hardware_key_sealing.rs +++ b/openhcl/underhill_attestation/src/hardware_key_sealing.rs @@ -299,7 +299,10 @@ mod tests { ); assert!(result.is_err()); let err = result.unwrap_err(); - matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); + assert!(matches!( + err, + HardwareDerivedKeysError::KeyDerivationPolicyMismatch + )); } { @@ -318,7 +321,10 @@ mod tests { ); assert!(result.is_err()); let err = result.unwrap_err(); - matches!(err, HardwareDerivedKeysError::KeyDerivationPolicyMismatch); + assert!(matches!( + err, + HardwareDerivedKeysError::KeyDerivationPolicyMismatch + )); } } @@ -360,7 +366,7 @@ mod tests { 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(_)); + assert!(matches!(err, HardwareKeySealingError::EncryptEgressKey(_))); } #[test] @@ -387,10 +393,10 @@ mod tests { .unseal_key(&hardware_derived_keys) .expect_err("expected HMAC verification to fail"); - matches!( + assert!(matches!( err, HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); + )); } #[test] @@ -424,10 +430,10 @@ mod tests { let err = hwkp .unseal_key(&k2) .expect_err("mix_measurement policy change should break unseal"); - matches!( + assert!(matches!( err, HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); + )); } #[test] @@ -460,10 +466,10 @@ mod tests { let err = hwkp .unseal_key(&k2) .expect_err("TCB change should break unseal"); - matches!( + assert!(matches!( err, HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); + )); } #[test] @@ -498,9 +504,9 @@ mod tests { let err = hwkp .unseal_key(&k2) .expect_err("measurement change should break unseal"); - matches!( + assert!(matches!( err, HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed - ); + )); } } diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index a4a7baaff7..d32d73f826 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -89,6 +89,14 @@ enum AttestationErrorInner { ReadGuestSecretKey(#[source] vmgs::ReadFromVmgsError), #[error("failed to get an attestation report")] GetAttestationReport(#[source] tee_call::Error), + #[error( + "host requested HardwareSealing GSP, but hardware sealing is not available \ + (tee_available={tee_available}, hardware_sealing_policy={hardware_sealing_policy:?})" + )] + HardwareSealingRequestedButNotAvailable { + tee_available: bool, + hardware_sealing_policy: HardwareSealingPolicy, + }, } #[derive(Debug, Error)] @@ -525,7 +533,20 @@ pub async fn initialize_platform_security( .await .map_err(AttestationErrorInner::ReadSecurityProfile)?; + // Hardware sealing is required (i.e., it is the *only* source of the VMGS DEK) + // when ALL of the following hold: + // - the VM is a CVM (tee_call is available), + // - the VM is stateless (suppress_attestation = true, so SKR is bypassed), + // - the host requests hardware sealing as the guest state encryption policy, and + // - the attested VM config permits it (hardware_sealing_policy != None). + // In stateful mode, hardware sealing may still be used as a *backup* recovery + // path, but it is never strictly required, so this flag stays false. + // + // Host invariant: the host sets `GuestStateEncryptionPolicy::HardwareSealing` only + // when suppress_attestation is true and only paired with `HardwareSealingPolicy::{Hash, Signer}`. + // Any other combination is treated as a host bug below. let require_hardware_sealing = tee_call.is_some() + && suppress_attestation && matches!( guest_state_encryption_policy, GuestStateEncryptionPolicy::HardwareSealing @@ -535,11 +556,33 @@ pub async fn initialize_platform_security( 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. + // Attestation is suppressed and hardware sealing is not required, indicating 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"); + // Reaching this branch means the host violated the invariant above: + // it requested HardwareSealing GSP without providing a usable hardware + // sealing policy on a CVM. Fail closed rather than silently downgrading + // to no encryption. + if matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::HardwareSealing + ) { + return Err( + AttestationErrorInner::HardwareSealingRequestedButNotAvailable { + tee_available: tee_call.is_some(), + hardware_sealing_policy: attestation_vm_config.hardware_sealing_policy, + } + .into(), + ); + } + + tracing::info!( + CVM_ALLOWED, + ?guest_state_encryption_policy, + hardware_sealing_policy = ?attestation_vm_config.hardware_sealing_policy, + tee_available = tee_call.is_some(), + "Suppressing attestation; VMGS encryption is bypassed" + ); return Ok(PlatformAttestationData { host_attestation_settings: HostAttestationSettings { 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 dd793404d4..c07ccf8c2c 100644 --- a/vm/devices/get/test_igvm_agent_lib/src/lib.rs +++ b/vm/devices/get/test_igvm_agent_lib/src/lib.rs @@ -240,15 +240,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