From 7808bd2e16a8acd2dd8e1defbf5e840b03eaf650 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 14 May 2026 19:31:43 +0000 Subject: [PATCH 1/2] underhill_attestation: refactoring Signed-off-by: Ming-Wei Shih --- .../underhill_attestation/src/derived_keys.rs | 1014 +++++++ .../src/hardware_key_sealing.rs | 34 +- .../src/igvm_attest/ak_cert.rs | 34 +- .../src/igvm_attest/key_release.rs | 4 +- .../src/igvm_attest/mod.rs | 85 +- .../src/igvm_attest/wrapped_key.rs | 2 +- openhcl/underhill_attestation/src/jwt.rs | 19 +- .../src/key_protector.rs | 407 +-- openhcl/underhill_attestation/src/lib.rs | 2436 ++--------------- .../src/secure_key_release.rs | 99 +- .../underhill_attestation/src/test_helpers.rs | 151 + openhcl/underhill_attestation/src/tests.rs | 1173 ++++++++ openhcl/underhill_attestation/src/vmgs.rs | 152 +- openhcl/underhill_core/src/emuplat/tpm.rs | 7 +- 14 files changed, 3028 insertions(+), 2589 deletions(-) create mode 100644 openhcl/underhill_attestation/src/derived_keys.rs create mode 100644 openhcl/underhill_attestation/src/tests.rs diff --git a/openhcl/underhill_attestation/src/derived_keys.rs b/openhcl/underhill_attestation/src/derived_keys.rs new file mode 100644 index 0000000000..50ab242264 --- /dev/null +++ b/openhcl/underhill_attestation/src/derived_keys.rs @@ -0,0 +1,1014 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! VMGS encryption-key derivation: orchestrates the multi-source key flow +//! (tenant KEK + key-based GSP + ID-based GSP + hardware sealing) that +//! produces ingress/egress data-encryption keys (DEKs) for the VMGS data +//! store. +//! +//! [`get_derived_keys`] is the single entry point; the other items in this +//! module are private helpers that decompose the flow into focused steps. + +use crate::DerivedKeyResult; +use crate::GspTypeRecord; +use crate::KeyProtectorActions; +use crate::KeyProtectorById; +use crate::Keys; +use crate::LogOpType; +use crate::hardware_key_sealing::HardwareDerivedKeys; +use crate::hardware_key_sealing::HardwareKeyProtectorExt as _; +use crate::key_protector; +use crate::key_protector::GetKeysFromKeyProtectorError; +use crate::key_protector::KeyProtectorExt as _; +use crate::vmgs; +use ::vmgs::GspType; +use ::vmgs::Vmgs; +use crypto::rsa::RsaKeyPair; +use cvm_tracing::CVM_ALLOWED; +use get_protocol::dps_json::GuestStateEncryptionPolicy; +use guest_emulation_transport::GuestEmulationTransportClient; +use guest_emulation_transport::api::GspExtendedStatusFlags; +use guest_emulation_transport::api::GuestStateProtection; +use guest_emulation_transport::api::GuestStateProtectionById; +use guid::Guid; +use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; +use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH; +use openhcl_attestation_protocol::vmgs::HardwareKeyProtector; +use openhcl_attestation_protocol::vmgs::KeyProtector; +use static_assertions::const_assert_eq; +use tee_call::TeeCall; +use thiserror::Error; +use zerocopy::FromZeros; +use zerocopy::IntoBytes; + +#[derive(Debug, Error)] +pub(crate) enum GetDerivedKeysError { + #[error("failed to get ingress/egress keys from the key protector")] + GetKeysFromKeyProtector(#[source] GetKeysFromKeyProtectorError), + #[error("failed to fetch GSP")] + FetchGuestStateProtectionById( + #[source] guest_emulation_transport::error::GuestStateProtectionByIdError, + ), + #[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] crate::hardware_key_sealing::HardwareKeySealingError, + ), + #[error("failed to get an ingress key from key protector")] + GetIngressKeyFromKpFailed, + #[error("failed to get an ingress key from guest state protection")] + GetIngressKeyFromGspFailed, + #[error("failed to get an ingress key from guest state protection by id")] + GetIngressKeyFromGspByIdFailed, + #[error("encryption cannot be disabled if VMGS was previously encrypted")] + DisableVmgsEncryptionFailed, + #[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] crate::hardware_key_sealing::HardwareKeySealingError, + ), + #[error("failed to write to `FileId::HW_KEY_PROTECTOR` in vmgs")] + VmgsWriteHardwareKeyProtector(#[source] vmgs::WriteToVmgsError), + #[error("failed to get derived key by id")] + GetDerivedKeyById(#[source] GetDerivedKeysByIdError), + #[error("failed to derive an ingress key")] + DeriveIngressKey(#[source] crypto::kdf::KdfError), + #[error("failed to derive an egress key")] + DeriveEgressKey(#[source] crypto::kdf::KdfError), +} + +#[derive(Debug, Error)] +pub(crate) enum GetDerivedKeysByIdError { + #[error("failed to derive an egress key based on current vm bios guid")] + DeriveEgressKeyUsingCurrentVmId(#[source] crypto::kdf::KdfError), + #[error("failed to derive an ingress key based on key protector Id from vmgs")] + DeriveIngressKeyUsingKeyProtectorId(#[source] crypto::kdf::KdfError), +} + +/// Label used by [`derive_key`]. +const VMGS_KEY_DERIVE_LABEL: &[u8; 7] = b"VMGSKEY"; + +/// KBKDF from SP800-108, using HMAC-SHA-256. +fn derive_key( + key: &[u8], + context: &[u8], + label: &[u8], +) -> Result<[u8; AES_GCM_KEY_LENGTH], crypto::kdf::KdfError> { + let output = crypto::kdf::kbkdf_hmac_sha256(key, context, label, AES_GCM_KEY_LENGTH)?; + Ok(output.try_into().unwrap()) +} + +/// Which encryption sources are usable to (un)lock the VMGS in the current +/// boot, after applying both source availability and the active +/// [`GuestStateEncryptionPolicy`]. +/// +/// Each field is `true` only when the source is both physically available +/// (e.g. RPC server responded, registry file exists) and not disabled by +/// policy. +#[derive(Clone, Copy, Default)] +struct EncryptionSources { + /// A tenant key (KEK) was released and can unwrap the DEK. + kek: bool, + /// Key-based Guest State Protection is usable. + gsp: bool, + /// ID-based Guest State Protection (GSP By Id) is usable. + gsp_by_id: bool, +} + +impl EncryptionSources { + /// Returns `true` when no encryption source is usable. + fn none(&self) -> bool { + !self.kek && !self.gsp && !self.gsp_by_id + } +} + +/// State of the VMGS observed at the start of [`get_derived_keys`]. +#[derive(Clone, Copy)] +struct InitialVmgsEncryptionState { + /// VMGS reports itself as encrypted. + is_encrypted: bool, + /// Encrypted GSP data is present in the active key protector slot. + is_gsp: bool, + /// A non-ported GSP-By-Id key protector entry was found for this VM. + is_gsp_by_id: bool, + /// The VMGS was not encrypted and was not provisioned this boot. + existing_unencrypted: bool, + /// A non-empty DEK is present in the active key protector slot. + found_dek: bool, +} + +/// Result of [`attempt_gsp`]. +struct GspAttempt { + response: GuestStateProtection, + /// True when an RPC server responded with non-zero-length GSP data. + available: bool, + /// True when GSP is usable under the current policy. + active: bool, + /// True when GSP must be used (existing GSP in KP, RPC server requires + /// it, or strict policy with `GspKey`). + requires: bool, + /// True when the VMGS is encrypted but no protector data is found; the + /// caller should also require GSP By Id. + force_gsp_by_id: bool, +} + +/// Result of [`attempt_gsp_by_id`]. +struct GspByIdAttempt { + response: GuestStateProtectionById, + /// Source availability; `None` if the source was not queried. + available: Option, + /// True when GSP By Id is usable under the current policy. + active: bool, +} + +/// Tenant keys produced by [`unwrap_kek_keys`]. +struct UnwrappedKekKeys { + ingress_key: [u8; AES_GCM_KEY_LENGTH], + decrypt_egress_key: Option<[u8; AES_GCM_KEY_LENGTH]>, + encrypt_egress_key: [u8; AES_GCM_KEY_LENGTH], + /// `true` when a tenant key was successfully unwrapped. + kek_active: bool, +} + +/// Update data store keys with key protectors. +/// VMGS encryption can come from combinations of three sources, +/// a Tenant Key (KEK), GSP, and GSP By Id. +/// There is an Ingress Key (previously used to lock the VMGS), +/// and an Egress Key (new key for locking the VMGS), and these +/// keys can be derived differently, where KEK is +/// always used if available, and GSP is preferred to GSP By Id. +/// Ingress Possible Egress in order of preference [Ingress] +/// - No Encryption - All +/// - GSP By Id - KEK + GSP, KEK + GSP By Id, GSP, [GSP By Id] +/// - GSP (v10 VM and later) - KEK + GSP, [GSP] +/// - KEK (IVM only) - KEK + GSP, KEK + GSP By Id, [KEK] +/// - KEK + GSP By Id - KEK + GSP, [KEK + GSP By Id] +/// - KEK + GSP - [KEK + GSP] +/// +/// NOTE: for TVM parity, only None, Gsp By Id v9.1, and Gsp By Id / Gsp v10.0 are used. +pub(crate) async fn get_derived_keys( + get: &GuestEmulationTransportClient, + tee_call: Option<&dyn TeeCall>, + vmgs: &mut Vmgs, + key_protector: &mut KeyProtector, + key_protector_by_id: &mut KeyProtectorById, + bios_guid: Guid, + attestation_vm_config: &AttestationVmConfig, + is_encrypted: bool, + ingress_rsa_kek: Option<&RsaKeyPair>, + wrapped_des_key: Option<&[u8]>, + tcb_version: Option, + guest_state_encryption_policy: GuestStateEncryptionPolicy, + strict_encryption_policy: bool, + skip_hw_unsealing: bool, +) -> Result { + tracing::info!( + CVM_ALLOWED, + ?guest_state_encryption_policy, + strict_encryption_policy, + "encryption policy" + ); + + // TODO: implement hardware sealing only + if matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::HardwareSealing + ) { + todo!("hardware sealing") + } + + let mut actions = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: false, + use_hardware_unlock: false, + }; + let mut gsp_types = GspTypeRecord::default(); + + let mut derived_keys = Keys { + ingress: [0u8; AES_GCM_KEY_LENGTH], + decrypt_egress: None, + encrypt_egress: [0u8; AES_GCM_KEY_LENGTH], + }; + + // Ingress / Egress seed values depend on what happened previously to the datastore + let ingress_idx = (key_protector.active_kp % 2) as usize; + let egress_idx = ingress_idx ^ 1; + + let found_dek = key_protector::dek_is_present(&key_protector.dek[ingress_idx]); + + // Handle key released via attestation process (tenant key) to get keys from KeyProtector + let UnwrappedKekKeys { + ingress_key, + mut decrypt_egress_key, + encrypt_egress_key, + kek_active, + } = unwrap_kek_keys( + get, + key_protector, + ingress_rsa_kek, + wrapped_des_key, + ingress_idx, + egress_idx, + ) + .await?; + + // Handle various sources of Guest State Protection + let state = InitialVmgsEncryptionState { + is_encrypted, + is_gsp: key_protector.gsp[ingress_idx].gsp_length != 0, + is_gsp_by_id: matches!( + key_protector_by_id, + KeyProtectorById::Found(inner) if inner.ported != 1, + ), + existing_unencrypted: !vmgs.encrypted() && !vmgs.was_provisioned_this_boot(), + found_dek, + }; + tracing::info!( + CVM_ALLOWED, + is_encrypted = state.is_encrypted, + is_gsp_by_id = state.is_gsp_by_id, + is_gsp = state.is_gsp, + found_dek = state.found_dek, + "initial vmgs encryption state" + ); + let mut requires_gsp_by_id = state.is_gsp_by_id; + + // Attempt GSP + let gsp = attempt_gsp( + get, + key_protector, + ingress_idx, + guest_state_encryption_policy, + strict_encryption_policy, + state, + requires_gsp_by_id, + ) + .await; + if gsp.force_gsp_by_id { + requires_gsp_by_id = true; + } + + // Attempt GSP By Id protection if GSP is not available, when changing + // schemes, or as requested + let gsp_by_id = if !gsp.active || requires_gsp_by_id { + attempt_gsp_by_id( + get, + guest_state_encryption_policy, + strict_encryption_policy, + state.existing_unencrypted, + requires_gsp_by_id, + ) + .await? + } else { + GspByIdAttempt { + response: GuestStateProtectionById::new_zeroed(), + available: None, + active: false, + } + }; + + let sources = EncryptionSources { + kek: kek_active, + gsp: gsp.active, + gsp_by_id: gsp_by_id.active, + }; + + // If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key + if (!sources.kek && found_dek) + || (!sources.gsp && gsp.requires) + || (!sources.gsp_by_id && requires_gsp_by_id) + { + if let Some(ingress) = try_hardware_unseal_ingress_key( + get, + vmgs, + tee_call, + attestation_vm_config, + skip_hw_unsealing, + ) + .await? + { + derived_keys.ingress = ingress; + derived_keys.decrypt_egress = None; + derived_keys.encrypt_egress = ingress; + + actions.should_write_kp = false; + actions.use_hardware_unlock = true; + + tracing::warn!( + CVM_ALLOWED, + "Using hardware-derived key to recover VMGS DEK" + ); + + return Ok(DerivedKeyResult { + derived_keys: Some(derived_keys), + actions, + gsp_types, + gsp_extended_status_flags: gsp.response.extended_status_flags, + }); + } + + return Err(if !sources.kek && found_dek { + GetDerivedKeysError::GetIngressKeyFromKpFailed + } else if !sources.gsp && gsp.requires { + GetDerivedKeysError::GetIngressKeyFromGspFailed + } else { + // !sources.gsp_by_id && requires_gsp_by_id + GetDerivedKeysError::GetIngressKeyFromGspByIdFailed + }); + } + + tracing::info!( + CVM_ALLOWED, + kek = sources.kek, + gsp_available = gsp.available, + gsp = sources.gsp, + gsp_by_id_available = ?gsp_by_id.available, + gsp_by_id = sources.gsp_by_id, + "Encryption sources" + ); + + // Check if sources of encryption are available + if sources.none() { + if is_encrypted { + return Err(GetDerivedKeysError::DisableVmgsEncryptionFailed); + } + match guest_state_encryption_policy { + // fail if some minimum level of encryption was required + GuestStateEncryptionPolicy::GspById + | GuestStateEncryptionPolicy::GspKey + | GuestStateEncryptionPolicy::HardwareSealing => { + return Err(GetDerivedKeysError::EncryptionRequiredButNotFound); + } + GuestStateEncryptionPolicy::Auto | GuestStateEncryptionPolicy::None => { + tracing::info!(CVM_ALLOWED, "No VMGS encryption used."); + + return Ok(DerivedKeyResult { + derived_keys: None, + actions, + gsp_types, + gsp_extended_status_flags: gsp.response.extended_status_flags, + }); + } + } + } + + // Attempt to get hardware derived keys + let hardware_derived_keys = + try_derive_hardware_keys(tee_call, attestation_vm_config, tcb_version); + + // Use tenant key (KEK only) + if !sources.gsp && !sources.gsp_by_id { + tracing::info!(CVM_ALLOWED, "No GSP used with SKR"); + + derived_keys.ingress = ingress_key; + derived_keys.decrypt_egress = decrypt_egress_key; + derived_keys.encrypt_egress = encrypt_egress_key; + + if let Some(hardware_derived_keys) = hardware_derived_keys { + let hardware_key_protector = HardwareKeyProtector::seal_key( + &hardware_derived_keys, + &derived_keys.encrypt_egress, + ) + .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; + vmgs::write_hardware_key_protector(&hardware_key_protector, vmgs) + .await + .map_err(GetDerivedKeysError::VmgsWriteHardwareKeyProtector)?; + + tracing::info!(CVM_ALLOWED, "hardware key protector updated (no GSP used)"); + } + + return Ok(DerivedKeyResult { + derived_keys: Some(derived_keys), + actions, + gsp_types, + gsp_extended_status_flags: gsp.response.extended_status_flags, + }); + } + + // GSP By Id derives keys differently, + // because key is shared across VMs different context must be used (Id GUID) + if (!sources.kek && !sources.gsp) || requires_gsp_by_id { + let derived_keys_by_id = + get_derived_keys_by_id(key_protector_by_id, bios_guid, gsp_by_id.response) + .map_err(GetDerivedKeysError::GetDerivedKeyById)?; + + if !sources.kek && !sources.gsp { + if matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::GspById | GuestStateEncryptionPolicy::Auto + ) { + tracing::info!(CVM_ALLOWED, "Using GspById"); + } else { + // Log a warning here to indicate that the VMGS state is out of + // sync with the VM's configuration. + // + // This should only happen if strict encryption policy is + // disabled and one of the following is true: + // - The VM is configured to have no encryption, but it already + // has GspById encryption. + // - The VM is configured to use GspKey, but GspKey is not + // available and GspById is. + tracing::warn!(CVM_ALLOWED, "Allowing GspById"); + }; + + // Not required for Id protection + actions.should_write_kp = false; + actions.use_gsp_by_id = true; + gsp_types.decrypt = GspType::GspById; + gsp_types.encrypt = GspType::GspById; + + return Ok(DerivedKeyResult { + derived_keys: Some(derived_keys_by_id), + actions, + gsp_types, + gsp_extended_status_flags: gsp.response.extended_status_flags, + }); + } + + derived_keys.ingress = derived_keys_by_id.ingress; + + tracing::info!( + CVM_ALLOWED, + op_type = ?LogOpType::ConvertEncryptionType, + "Converting GSP method." + ); + } + + let egress_seed; + let mut ingress_seed = None; + + // To get to this point, either KEK or GSP must be available + // Mix tenant key with GSP key to create data store encryption keys + // Covers possible egress combinations: + // GSP, GSP + KEK, GSP By Id + KEK + + if requires_gsp_by_id || !sources.gsp { + // If DEK exists, ingress is either KEK or KEK + GSP By Id + // If no DEK, then ingress was Gsp By Id (derived above) + if found_dek { + if requires_gsp_by_id { + ingress_seed = Some( + gsp_by_id.response.seed.buffer[..gsp_by_id.response.seed.length as usize] + .to_vec(), + ); + gsp_types.decrypt = GspType::GspById; + } else { + derived_keys.ingress = ingress_key; + } + } else { + gsp_types.decrypt = GspType::GspById; + } + + // Choose best available egress seed + if !sources.gsp { + egress_seed = + gsp_by_id.response.seed.buffer[..gsp_by_id.response.seed.length as usize].to_vec(); + actions.use_gsp_by_id = true; + gsp_types.encrypt = GspType::GspById; + } else { + egress_seed = + gsp.response.new_gsp.buffer[..gsp.response.new_gsp.length as usize].to_vec(); + gsp_types.encrypt = GspType::GspKey; + } + } else { + // `sources.gsp` is true, using `gsp.response` + + if gsp.response.decrypted_gsp[ingress_idx].length == 0 + && gsp.response.decrypted_gsp[egress_idx].length == 0 + { + tracing::info!(CVM_ALLOWED, "Applying GSP."); + + // VMGS has never had any GSP applied. + // Leave ingress key untouched, derive egress key with new seed. + egress_seed = + gsp.response.new_gsp.buffer[..gsp.response.new_gsp.length as usize].to_vec(); + + // Ingress key is either zero or tenant only. + // Only copy in the case where a tenant key was released. + if sources.kek { + derived_keys.ingress = ingress_key; + } + + gsp_types.encrypt = GspType::GspKey; + } else { + tracing::info!(CVM_ALLOWED, "Using existing GSP."); + + ingress_seed = Some( + gsp.response.decrypted_gsp[ingress_idx].buffer + [..gsp.response.decrypted_gsp[ingress_idx].length as usize] + .to_vec(), + ); + + if gsp.response.decrypted_gsp[egress_idx].length == 0 { + // Derive ingress with saved seed, derive egress with new seed. + egress_seed = + gsp.response.new_gsp.buffer[..gsp.response.new_gsp.length as usize].to_vec(); + } else { + // System failed during data store unlock, and is in indeterminate state. + // The egress key might have been applied, or the ingress key might be valid. + // Use saved KP, derive ingress/egress keys to attempt recovery. + // Do not update the saved KP with new seed value. + egress_seed = gsp.response.decrypted_gsp[egress_idx].buffer + [..gsp.response.decrypted_gsp[egress_idx].length as usize] + .to_vec(); + actions.should_write_kp = false; + decrypt_egress_key = Some(encrypt_egress_key); + } + + gsp_types.decrypt = GspType::GspKey; + gsp_types.encrypt = GspType::GspKey; + } + } + + // Derive key used to lock data store previously + if let Some(seed) = ingress_seed { + derived_keys.ingress = derive_key(&ingress_key, &seed, VMGS_KEY_DERIVE_LABEL) + .map_err(GetDerivedKeysError::DeriveIngressKey)?; + } + + // Always derive a new egress key using best available seed + derived_keys.decrypt_egress = decrypt_egress_key + .map(|key| derive_key(&key, &egress_seed, VMGS_KEY_DERIVE_LABEL)) + .transpose() + .map_err(GetDerivedKeysError::DeriveEgressKey)?; + + derived_keys.encrypt_egress = + derive_key(&encrypt_egress_key, &egress_seed, VMGS_KEY_DERIVE_LABEL) + .map_err(GetDerivedKeysError::DeriveEgressKey)?; + + if actions.should_write_kp { + // Update with all seeds used, but do not write until data store is unlocked + key_protector.gsp[egress_idx] + .gsp_buffer + .copy_from_slice(&gsp.response.encrypted_gsp.buffer); + key_protector.gsp[egress_idx].gsp_length = gsp.response.encrypted_gsp.length; + + if let Some(hardware_derived_keys) = hardware_derived_keys { + let hardware_key_protector = HardwareKeyProtector::seal_key( + &hardware_derived_keys, + &derived_keys.encrypt_egress, + ) + .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; + + vmgs::write_hardware_key_protector(&hardware_key_protector, vmgs) + .await + .map_err(GetDerivedKeysError::VmgsWriteHardwareKeyProtector)?; + + tracing::info!(CVM_ALLOWED, "hardware key protector updated"); + } + } + + if matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::GspKey | GuestStateEncryptionPolicy::Auto + ) { + tracing::info!(CVM_ALLOWED, "Using Gsp"); + } else { + // Log a warning here to indicate that the VMGS state is out of + // sync with the VM's configuration. + // + // This should only happen if the VM is configured to have no + // encryption or GspById encryption, but it already has GspKey + // encryption and strict encryption policy is disabled. + tracing::warn!(CVM_ALLOWED, "Allowing Gsp"); + } + + Ok(DerivedKeyResult { + derived_keys: Some(derived_keys), + actions, + gsp_types, + gsp_extended_status_flags: gsp.response.extended_status_flags, + }) +} + +/// Unwrap and rotate the keys stored in `key_protector` using +/// `ingress_rsa_kek`, or return all-zero keys when no tenant key is +/// available. On DEK or DES unwrap failure, emits the corresponding host +/// event before propagating the error. +async fn unwrap_kek_keys( + get: &GuestEmulationTransportClient, + key_protector: &mut KeyProtector, + ingress_rsa_kek: Option<&RsaKeyPair>, + wrapped_des_key: Option<&[u8]>, + ingress_idx: usize, + egress_idx: usize, +) -> Result { + let Some(ingress_kek) = ingress_rsa_kek else { + return Ok(UnwrappedKekKeys { + ingress_key: [0u8; AES_GCM_KEY_LENGTH], + decrypt_egress_key: None, + encrypt_egress_key: [0u8; AES_GCM_KEY_LENGTH], + kek_active: false, + }); + }; + + let keys = match key_protector.unwrap_and_rotate_keys( + ingress_kek, + wrapped_des_key, + ingress_idx, + egress_idx, + ) { + Ok(keys) => keys, + Err(e) + if matches!( + e, + GetKeysFromKeyProtectorError::DesKeyRsaUnwrap(_) + | GetKeysFromKeyProtectorError::IngressDekRsaUnwrap(_) + ) => + { + get.event_log_fatal(guest_emulation_transport::api::EventLogId::DEK_DECRYPTION_FAILED) + .await; + return Err(GetDerivedKeysError::GetKeysFromKeyProtector(e)); + } + Err(e) => return Err(GetDerivedKeysError::GetKeysFromKeyProtector(e)), + }; + Ok(UnwrappedKekKeys { + ingress_key: keys.ingress, + decrypt_egress_key: keys.decrypt_egress, + encrypt_egress_key: keys.encrypt_egress, + kek_active: true, + }) +} + +/// Query the host for key-based Guest State Protection and decide whether +/// it can be used under the current policy. +async fn attempt_gsp( + get: &GuestEmulationTransportClient, + key_protector: &mut KeyProtector, + ingress_idx: usize, + guest_state_encryption_policy: GuestStateEncryptionPolicy, + strict_encryption_policy: bool, + state: InitialVmgsEncryptionState, + requires_gsp_by_id: bool, +) -> GspAttempt { + tracing::info!(CVM_ALLOWED, "attempting GSP"); + + let response = get_gsp_data(get, key_protector).await; + + tracing::info!( + CVM_ALLOWED, + request_data_length_in_vmgs = key_protector.gsp[ingress_idx].gsp_length, + no_rpc_server = response.extended_status_flags.no_rpc_server(), + requires_rpc_server = response.extended_status_flags.requires_rpc_server(), + encrypted_gsp_length = response.encrypted_gsp.length, + "GSP response" + ); + + let no_gsp_available = + response.extended_status_flags.no_rpc_server() || response.encrypted_gsp.length == 0; + + let no_gsp = no_gsp_available + // disable if auto and pre-existing guest state is not encrypted or + // encrypted using GspById to prevent encryption changes without + // explicit intent + || (matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::Auto + ) && (state.is_gsp_by_id || state.existing_unencrypted)) + // disable per encryption policy (first boot only, unless strict) + || (matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::GspById | GuestStateEncryptionPolicy::None + ) && (!state.is_gsp || strict_encryption_policy)); + + let requires_gsp = state.is_gsp + || response.extended_status_flags.requires_rpc_server() + || (matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::GspKey + ) && strict_encryption_policy); + + // If the VMGS is encrypted, but no key protection data is found, + // assume GspById encryption is enabled, but no ID file was written. + let force_gsp_by_id = + state.is_encrypted && !requires_gsp_by_id && !requires_gsp && !state.found_dek; + + GspAttempt { + response, + available: !no_gsp_available, + active: !no_gsp, + requires: requires_gsp, + force_gsp_by_id, + } +} + +/// Query the host for ID-based Guest State Protection and decide whether +/// it can be used under the current policy. +async fn attempt_gsp_by_id( + get: &GuestEmulationTransportClient, + guest_state_encryption_policy: GuestStateEncryptionPolicy, + strict_encryption_policy: bool, + existing_unencrypted: bool, + requires_gsp_by_id: bool, +) -> Result { + tracing::info!(CVM_ALLOWED, "attempting GSP By Id"); + + let response = get + .guest_state_protection_data_by_id() + .await + .map_err(GetDerivedKeysError::FetchGuestStateProtectionById)?; + + let no_gsp_by_id_available = response.extended_status_flags.no_registry_file(); + + let no_gsp_by_id = no_gsp_by_id_available + // disable if auto and pre-existing guest state is unencrypted + // to prevent encryption changes without explicit intent + || (matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::Auto + ) && existing_unencrypted) + // disable per encryption policy (first boot only, unless strict) + || (matches!( + guest_state_encryption_policy, + GuestStateEncryptionPolicy::None + ) && (!requires_gsp_by_id || strict_encryption_policy)); + + if no_gsp_by_id && requires_gsp_by_id { + return Err(GetDerivedKeysError::GspByIdRequiredButNotFound); + } + + Ok(GspByIdAttempt { + response, + available: Some(!no_gsp_by_id_available), + active: !no_gsp_by_id, + }) +} + +/// Try to recover the ingress VMGS DEK using a hardware-sealed key +/// protector when sources of encryption used previously are missing. +/// +/// Returns: +/// - `Ok(Some(key))` — hardware unseal succeeded; the returned key can be +/// used directly as the ingress DEK. +/// - `Ok(None)` — no usable hardware sealing material is available; the +/// caller must surface a scheme-specific error. +/// - `Err(_)` — hardware unsealing was attempted but failed. +async fn try_hardware_unseal_ingress_key( + get: &GuestEmulationTransportClient, + vmgs: &mut Vmgs, + tee_call: Option<&dyn TeeCall>, + attestation_vm_config: &AttestationVmConfig, + skip_hw_unsealing: bool, +) -> Result, GetDerivedKeysError> { + let Some(tee_call) = tee_call else { + return Ok(None); + }; + + let hardware_key_protector = match vmgs::read_hardware_key_protector(vmgs).await { + Ok(hardware_key_protector) => Some(hardware_key_protector), + Err(e) => { + // non-fatal + tracing::warn!( + CVM_ALLOWED, + error = &e as &dyn std::error::Error, + "failed to read HW_KEY_PROTECTOR from Vmgs" + ); + None + } + }; + + let tcb_version = hardware_key_protector + .as_ref() + .map(|kp| kp.header.tcb_version); + let hardware_derived_keys = + try_derive_hardware_keys(Some(tee_call), attestation_vm_config, tcb_version); + + // When the IGVM agent signals skip_hw_unsealing, force both + // hardware_key_protector and hardware_derived_keys to None so the + // caller falls through to the scheme-specific error. When hardware + // sealing keys were actually available, emit a warning and a host + // event that make the skip visible. + let (hardware_key_protector, hardware_derived_keys) = if skip_hw_unsealing { + if hardware_key_protector.is_some() && hardware_derived_keys.is_some() { + 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; + } else { + tracing::info!( + CVM_ALLOWED, + hardware_key_protector = hardware_key_protector.is_some(), + hardware_derived_keys = hardware_derived_keys.is_some(), + "skip_hw_unsealing signaled but hardware key data not available, \ + falling through to scheme-specific error" + ); + } + (None, None) + } else { + (hardware_key_protector, hardware_derived_keys) + }; + + let (Some(hardware_key_protector), Some(hardware_derived_keys)) = + (hardware_key_protector, hardware_derived_keys) + else { + return Ok(None); + }; + + let ingress = hardware_key_protector + .unseal_key(&hardware_derived_keys) + .map_err(GetDerivedKeysError::UnsealIngressKeyUsingHardwareDerivedKeys)?; + Ok(Some(ingress)) +} + +/// Derive hardware-sealing keys via the active [`TeeCall`] if the platform +/// supports it. Returns `None` (and logs a warning) on failure; missing +/// `tee_call` or `tcb_version` returns `None` silently. +fn try_derive_hardware_keys( + tee_call: Option<&dyn TeeCall>, + attestation_vm_config: &AttestationVmConfig, + tcb_version: Option, +) -> Option { + let tee_call = tee_call?.supports_get_derived_key()?; + let 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 + } + } +} + +/// Update data store keys with key protectors based on VmUniqueId & host seed. +pub(crate) fn get_derived_keys_by_id( + key_protector_by_id: &mut KeyProtectorById, + bios_guid: Guid, + gsp_response_by_id: GuestStateProtectionById, +) -> Result { + // This does not handle tenant encrypted VMGS files or Isolated VM, + // or the case where an unlock/relock fails and a snapshot is + // made from that file (the Id cannot change in that failure path). + // When converted to a later scheme, Egress Key will be overwritten. + + // Always derive a new egress key from current VmUniqueId + let new_egress_key = derive_key( + &gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize], + bios_guid.as_bytes(), + VMGS_KEY_DERIVE_LABEL, + ) + .map_err(GetDerivedKeysByIdError::DeriveEgressKeyUsingCurrentVmId)?; + + // Ingress values depend on what happened previously to the datastore. + // If not previously encrypted (no saved Id), then Ingress Key not required. + let new_ingress_key = if key_protector_by_id.id_guid() != Guid::ZERO { + // Derive key used to lock data store previously + derive_key( + &gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize], + key_protector_by_id.id_guid().as_bytes(), + VMGS_KEY_DERIVE_LABEL, + ) + .map_err(GetDerivedKeysByIdError::DeriveIngressKeyUsingKeyProtectorId)? + } else { + // If data store is not encrypted, Ingress should equal Egress + new_egress_key + }; + + Ok(Keys { + ingress: new_ingress_key, + decrypt_egress: None, + encrypt_egress: new_egress_key, + }) +} + +/// Prepare the request payload and request GSP from the host via GET. +async fn get_gsp_data( + get: &GuestEmulationTransportClient, + key_protector: &mut KeyProtector, +) -> GuestStateProtection { + use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; + use openhcl_attestation_protocol::vmgs::NUMBER_KP; + + const_assert_eq!(guest_emulation_transport::api::NUMBER_GSP, NUMBER_KP as u32); + const_assert_eq!( + guest_emulation_transport::api::GSP_CIPHERTEXT_MAX, + GSP_BUFFER_SIZE as u32 + ); + + let mut encrypted_gsp = + [guest_emulation_transport::api::GspCiphertextContent::new_zeroed(); NUMBER_KP]; + + for (i, gsp) in encrypted_gsp.iter_mut().enumerate() { + if key_protector.gsp[i].gsp_length == 0 { + continue; + } + + gsp.buffer[..key_protector.gsp[i].gsp_length as usize].copy_from_slice( + &key_protector.gsp[i].gsp_buffer[..key_protector.gsp[i].gsp_length as usize], + ); + + gsp.length = key_protector.gsp[i].gsp_length; + } + + get.guest_state_protection_data(encrypted_gsp, GspExtendedStatusFlags::new()) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helpers::new_key_protector_by_id; + use get_protocol::GSP_CLEARTEXT_MAX; + use pal_async::async_test; + + #[async_test] + async fn get_derived_keys_using_id() { + let bios_guid = Guid::new_random(); + + let gsp_response_by_id = GuestStateProtectionById { + seed: guest_emulation_transport::api::GspCleartextContent { + length: GSP_CLEARTEXT_MAX, + buffer: [1; GSP_CLEARTEXT_MAX as usize * 2], + }, + extended_status_flags: GspExtendedStatusFlags::from_bits(0), + }; + + // When the key protector by id inner `id_guid` is all zeroes, the derived ingress and egress keys + // should be identical. + let mut key_protector_by_id = + new_key_protector_by_id(Some(Guid::new_zeroed()), None, false); + let derived_keys = + get_derived_keys_by_id(&mut key_protector_by_id, bios_guid, gsp_response_by_id) + .unwrap(); + + assert_eq!(derived_keys.ingress, derived_keys.encrypt_egress); + + // When the key protector by id inner `id_guid` is not all zeroes, the derived ingress and egress keys + // should be different. + let mut key_protector_by_id = new_key_protector_by_id(None, None, true); + let derived_keys = + get_derived_keys_by_id(&mut key_protector_by_id, bios_guid, gsp_response_by_id) + .unwrap(); + + assert_ne!(derived_keys.ingress, derived_keys.encrypt_egress); + + // When the `gsp_response_by_id` seed length is 0, deriving a key will fail. + let gsp_response_by_id_with_0_length_seed = GuestStateProtectionById { + seed: guest_emulation_transport::api::GspCleartextContent { + length: 0, + buffer: [1; GSP_CLEARTEXT_MAX as usize * 2], + }, + extended_status_flags: GspExtendedStatusFlags::from_bits(0), + }; + + let derived_keys_response = get_derived_keys_by_id( + &mut key_protector_by_id, + bios_guid, + gsp_response_by_id_with_0_length_seed, + ); + assert!(matches!( + derived_keys_response, + Err(GetDerivedKeysByIdError::DeriveEgressKeyUsingCurrentVmId(_)) + )); + } +} diff --git a/openhcl/underhill_attestation/src/hardware_key_sealing.rs b/openhcl/underhill_attestation/src/hardware_key_sealing.rs index 92dab5a09e..bfba5920bd 100644 --- a/openhcl/underhill_attestation/src/hardware_key_sealing.rs +++ b/openhcl/underhill_attestation/src/hardware_key_sealing.rs @@ -24,18 +24,18 @@ pub(crate) enum HardwareDerivedKeysError { pub(crate) enum HardwareKeySealingError { #[error("failed to encrypt the egress key")] EncryptEgressKey(#[source] crypto::aes_256_cbc::Aes256CbcError), - #[error("invalid egress key encryption size {0}, expected {1}")] - InvalidEgressKeyEncryptionSize(usize, usize), + #[error("invalid egress key encryption size {size}, expected {expected_size}")] + InvalidEgressKeyEncryptionSize { size: usize, expected_size: usize }, #[error("HMAC-SHA-256 after encryption failed")] HmacAfterEncrypt(#[source] crypto::hmac_sha_256::HmacSha256Error), - #[error("HMAC-SHA-256 before ecryption failed")] + #[error("HMAC-SHA-256 before decryption failed")] HmacBeforeDecrypt(#[source] crypto::hmac_sha_256::HmacSha256Error), - #[error("Hardware key protector HMAC verification failed")] + #[error("hardware key protector HMAC verification failed")] HardwareKeyProtectorHmacVerificationFailed, #[error("failed to decrypt the ingress key")] DecryptIngressKey(#[source] crypto::aes_256_cbc::Aes256CbcError), - #[error("invalid ingress key decryption size {0}, expected {1}")] - InvalidIngressKeyDecryptionSize(usize, usize), + #[error("invalid ingress key decryption size {size}, expected {expected_size}")] + InvalidIngressKeyDecryptionSize { size: usize, expected_size: usize }, } /// Hold the hardware-derived keys. @@ -89,7 +89,7 @@ pub trait HardwareKeyProtectorExt: Sized { egress_key: &[u8], ) -> Result; - /// Unseal the `inress_key` with verify-mac-then-decrypt. + /// Unseal the `ingress_key` with verify-mac-then-decrypt. fn unseal_key( &self, hardware_derived_keys: &HardwareDerivedKeys, @@ -115,10 +115,10 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { .and_then(|aes| aes.encrypt()?.cipher(&iv, egress_key)) .map_err(HardwareKeySealingError::EncryptEgressKey)?; if output.len() != vmgs::AES_GCM_KEY_LENGTH { - Err(HardwareKeySealingError::InvalidEgressKeyEncryptionSize( - output.len(), - vmgs::AES_GCM_KEY_LENGTH, - ))? + return Err(HardwareKeySealingError::InvalidEgressKeyEncryptionSize { + size: output.len(), + expected_size: vmgs::AES_GCM_KEY_LENGTH, + }); } encrypted_egress_key.copy_from_slice(&output[..vmgs::AES_GCM_KEY_LENGTH]); @@ -152,7 +152,7 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { .map_err(HardwareKeySealingError::HmacBeforeDecrypt)?; if !constant_time_eq::constant_time_eq_32(&hmac, &self.hmac) { - Err(HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed)? + return Err(HardwareKeySealingError::HardwareKeyProtectorHmacVerificationFailed); } let mut decrypted_ingress_key = [0u8; vmgs::AES_GCM_KEY_LENGTH]; @@ -160,10 +160,10 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { .and_then(|aes| aes.decrypt()?.cipher(&self.iv, &self.ciphertext)) .map_err(HardwareKeySealingError::DecryptIngressKey)?; if output.len() != vmgs::AES_GCM_KEY_LENGTH { - Err(HardwareKeySealingError::InvalidIngressKeyDecryptionSize( - output.len(), - vmgs::AES_GCM_KEY_LENGTH, - ))? + return Err(HardwareKeySealingError::InvalidIngressKeyDecryptionSize { + size: output.len(), + expected_size: vmgs::AES_GCM_KEY_LENGTH, + }); } decrypted_ingress_key.copy_from_slice(&output[..vmgs::AES_GCM_KEY_LENGTH]); @@ -179,7 +179,7 @@ impl HardwareKeyProtectorExt for HardwareKeyProtector { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::MockTeeCall; + use crate::test_helpers::MockTeeCall; use zerocopy::FromBytes; #[test] diff --git a/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs b/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs index 284c423f3a..dc4fe262a1 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/ak_cert.rs @@ -8,23 +8,49 @@ use crate::igvm_attest::parse_response_header; use thiserror::Error; -/// AkCertError is returned by parse_ak_cert_response() in emuplat/tpm.rs +/// Errors returned when parsing an `AK_CERT_REQUEST` response. +/// +/// Returned by [`parse_response`], which is re-exported from the crate root +/// as `parse_ak_cert_response` and used by `emuplat/tpm.rs`. #[derive(Debug, Error)] pub enum AkCertError { + /// The response buffer is shorter than the minimum expected size to fit + /// the response header. #[error( "AK cert response is too small to parse. Found {size} bytes but expected at least {minimum_size}" )] - SizeTooSmall { size: usize, minimum_size: usize }, + SizeTooSmall { + /// Actual length of the response buffer in bytes. + size: usize, + /// Minimum size in bytes required to parse the response header. + minimum_size: usize, + }, + /// The size declared in the response header is larger than the actual + /// length of the response buffer received from the host. #[error( "AK cert response size {specified_size} specified in the header is larger then the actual size {size}" )] - SizeMismatch { size: usize, specified_size: usize }, + SizeMismatch { + /// Actual length of the response buffer in bytes. + size: usize, + /// Length declared inside the response header. + specified_size: usize, + }, + /// The response header version does not match the version expected by + /// this build. #[error( "AK cert response header version {version} does match the expected version {expected_version}" )] - HeaderVersionMismatch { version: u32, expected_version: u32 }, + HeaderVersionMismatch { + /// Header version reported in the response. + version: u32, + /// Header version expected by this build. + expected_version: u32, + }, + /// Parsing the common response header failed. #[error("error in parsing response header")] ParseHeader(#[source] CommonError), + /// The response header version is not a value recognized by this build. #[error("invalid response header version: {0}")] InvalidResponseVersion(u32), } diff --git a/openhcl/underhill_attestation/src/igvm_attest/key_release.rs b/openhcl/underhill_attestation/src/igvm_attest/key_release.rs index c61905aaa2..04176ec2db 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/key_release.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/key_release.rs @@ -60,7 +60,7 @@ pub fn parse_response( let minimum_payload_size = CIPHER_TEXT_KEY.len() + wrapped_key_base64_url_size - 1; if payload.len() < minimum_payload_size { - Err(KeyReleaseError::PayloadSizeTooSmall)? + return Err(KeyReleaseError::PayloadSizeTooSmall); } let data_utf8 = String::from_utf8_lossy(payload); let wrapped_key = match serde_json::from_str::(&data_utf8) { @@ -79,7 +79,7 @@ pub fn parse_response( .verify_signature() .map_err(KeyReleaseError::VerifyAkvJwtSignature)? { - Err(KeyReleaseError::VerifyAkvJwtSignatureFailed)? + return Err(KeyReleaseError::VerifyAkvJwtSignatureFailed); } } get_wrapped_key_blob(result)? diff --git a/openhcl/underhill_attestation/src/igvm_attest/mod.rs b/openhcl/underhill_attestation/src/igvm_attest/mod.rs index bf5b46dea2..6c5ccfeaaa 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/mod.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/mod.rs @@ -28,38 +28,69 @@ pub mod wrapped_key; base64_serde_type!(Base64Url, base64::engine::general_purpose::URL_SAFE_NO_PAD); -#[expect(missing_docs)] // self-explanatory fields +/// Errors returned by IGVM attest request preparation and response parsing. #[derive(Debug, Error)] pub enum Error { + /// The attestation report supplied to the request helper does not match + /// the expected size for the active TEE. #[error( "the size of the attestation report {report_size} is invalid, expected {expected_size}" )] InvalidAttestationReportSize { + /// Actual size of the report blob in bytes. report_size: usize, + /// Expected size of the report blob in bytes. expected_size: usize, }, + /// The attestation response is shorter than the minimum header size. #[error("the size of the attestation response {response_size} is too small to parse")] - ResponseSizeTooSmall { response_size: usize }, + ResponseSizeTooSmall { + /// Length of the response that failed to parse. + response_size: usize, + }, + /// The attestation response header could not be deserialized. #[error( "the header of the attestation response (size {response_size}) is not in correct format" )] - ResponseHeaderInvalidFormat { response_size: usize }, + ResponseHeaderInvalidFormat { + /// Length of the response whose header is malformed. + response_size: usize, + }, + /// The size declared in the response header does not match the actual + /// length of the buffer received from the host. #[error( "response size {specified_size} specified in the header not match the actual size {size}" )] - ResponseSizeMismatch { size: usize, specified_size: usize }, + ResponseSizeMismatch { + /// Actual length of the response buffer. + size: usize, + /// Length declared inside the header. + specified_size: usize, + }, + /// The response header advertises a version newer than this build supports. #[error("response header version {version:?} larger than current version {latest_version:?}")] InvalidResponseHeaderVersion { + /// Version reported in the response header. version: IgvmAttestResponseVersion, + /// Latest version known to this build. latest_version: IgvmAttestResponseVersion, }, + /// The host-side IGVM agent reported an attestation failure. The + /// `retry_signal` and `skip_hw_unsealing_signal` fields convey hints + /// from the agent about how the caller should proceed. #[error( "attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal}), skip hw unsealing recommendation ({skip_hw_unsealing_signal})" )] Attestation { + /// IGVM-specific error code returned by the agent. igvm_error_code: u32, + /// HTTP status code from the underlying call to the attestation + /// service, when applicable. http_status_code: u32, + /// Hint from the agent that the operation may succeed on retry. retry_signal: bool, + /// Hint from the agent that hardware key unsealing should be skipped + /// because the protected secret is no longer recoverable. skip_hw_unsealing_signal: bool, }, } @@ -89,9 +120,16 @@ impl ReportType { } /// Helper struct to create `IgvmAttestRequest` in raw bytes. +/// +/// The helper captures the immutable runtime-claims context (report type, +/// serialized runtime claims, claims hash, and hash type). The specific +/// `IgvmAttestRequestType` for each call is passed to [`create_request`] so +/// that the same helper can be reused across multiple related requests +/// (e.g. the wrapped-key flow issues both a `WRAPPED_KEY_REQUEST` and a +/// `KEY_RELEASE_REQUEST` from the same helper). +/// +/// [`create_request`]: IgvmAttestRequestHelper::create_request pub struct IgvmAttestRequestHelper { - /// The request type. - request_type: IgvmAttestRequestType, /// The report type. report_type: ReportType, /// Raw bytes of `RuntimeClaims`. @@ -130,7 +168,6 @@ impl IgvmAttestRequestHelper { runtime_claims_hash[0..hash.len()].copy_from_slice(&hash); Self { - request_type: IgvmAttestRequestType::KEY_RELEASE_REQUEST, report_type, runtime_claims, runtime_claims_hash, @@ -173,7 +210,6 @@ impl IgvmAttestRequestHelper { runtime_claims_hash[0..hash.len()].copy_from_slice(&hash); Self { - request_type: IgvmAttestRequestType::AK_CERT_REQUEST, report_type, runtime_claims, runtime_claims_hash, @@ -186,20 +222,20 @@ impl IgvmAttestRequestHelper { &self.runtime_claims_hash } - /// Set the `request_type`. - pub fn set_request_type(&mut self, request_type: IgvmAttestRequestType) { - self.request_type = request_type - } - - /// Create the request in raw bytes. + /// Create the request in raw bytes for the given `request_type`. + /// + /// The runtime claims captured by this helper are shared across all + /// requests built from it; only `request_type` and `attestation_report` + /// vary per call. pub fn create_request( &self, version: IgvmAttestRequestVersion, + request_type: IgvmAttestRequestType, attestation_report: &[u8], ) -> Result, Error> { create_request( version, - self.request_type, + request_type, &self.runtime_claims, attestation_report, &self.report_type, @@ -220,16 +256,16 @@ pub fn parse_response_header(response: &[u8]) -> Result response.len() { - Err(Error::ResponseSizeMismatch { + return Err(Error::ResponseSizeMismatch { size: response.len(), specified_size: header.data_size as usize, - })? + }); } if header.version > IGVM_ATTEST_RESPONSE_CURRENT_VERSION { - Err(Error::InvalidResponseHeaderVersion { + return Err(Error::InvalidResponseHeaderVersion { version: header.version, latest_version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION, - })? + }); } // IgvmErrorInfo is added in response header since version 2 @@ -244,12 +280,12 @@ pub fn parse_response_header(response: &[u8]) -> Result Vec { - let runtime_claims = serde_json::to_string(runtime_claims).expect("JSON serialization failed"); - runtime_claims.as_bytes().to_vec() + serde_json::to_vec(runtime_claims).expect("JSON serialization failed") } #[cfg(test)] diff --git a/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs b/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs index 4f4d3bd6d9..475f21d392 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs @@ -59,7 +59,7 @@ pub fn parse_response(response: &[u8]) -> Result JwtHelper { /// or Err indicate an invalid signature or other error. pub fn verify_signature(&self) -> Result { let alg = &self.jwt.header.alg; - let pkey = validate_cert_chain(&self.cert_chain()?)?; + let pkey = validate_cert_chain(&self.cert_chain()?) + .map_err(JwtError::CertificateChainValidation)?; verify_jwt_signature(alg, &pkey, self.payload.as_bytes(), &self.jwt.signature) .map_err(JwtError::JwtSignatureVerification) } @@ -224,7 +223,7 @@ fn verify_jwt_signature( match alg { JwtAlgorithm::RS256 => pkey .pkcs1_verify(payload, signature, crypto::rsa::HashAlgorithm::Sha256) - .map_err(JwtSignatureVerificationError::VerifySignature), + .map_err(JwtSignatureVerificationError), } } @@ -233,7 +232,7 @@ fn validate_cert_chain( cert_chain: &[X509Certificate], ) -> Result { if cert_chain.is_empty() { - Err(CertificateChainValidationError::CertChainIsEmpty)? + return Err(CertificateChainValidationError::CertChainIsEmpty); } // Only validate the subject-issuer pair and signature (without validity) @@ -250,14 +249,14 @@ fn validate_cert_chain( CertificateChainValidationError::VerifyChildSignatureWithParentPublicKey, )?; if !verified { - Err(CertificateChainValidationError::CertChainSignatureMismatch)? + return Err(CertificateChainValidationError::CertChainSignatureMismatch); } let issued = parent .issued(child) .map_err(CertificateChainValidationError::CheckCertificateIssuedByIssuer)?; if !issued { - Err(CertificateChainValidationError::CertChainSubjectIssuerMismatch)? + return Err(CertificateChainValidationError::CertChainSubjectIssuerMismatch); } } } diff --git a/openhcl/underhill_attestation/src/key_protector.rs b/openhcl/underhill_attestation/src/key_protector.rs index 7111a685f4..093a39189f 100644 --- a/openhcl/underhill_attestation/src/key_protector.rs +++ b/openhcl/underhill_attestation/src/key_protector.rs @@ -9,38 +9,39 @@ use crypto::rsa::RsaKeyPair; use cvm_tracing::CVM_ALLOWED; use cvm_tracing::CVM_CONFIDENTIAL; use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH; +use openhcl_attestation_protocol::vmgs::DekKp; use openhcl_attestation_protocol::vmgs::KeyProtector; use thiserror::Error; #[derive(Debug, Error)] pub(crate) enum GetKeysFromKeyProtectorError { #[error( - "The DEK format expects to hold an RSA-WRAPPED AES key, but found an AES-WRAPPED AES key" + "the DEK format expects to hold an RSA-WRAPPED AES key, but found an AES-WRAPPED AES key" )] InvalidDekFormat, - #[error("Ingress RSA KEK size {key_size} was larger than expected {expected_size}")] + #[error("ingress RSA KEK size {key_size} was larger than expected {expected_size}")] InvalidIngressRsaKekSize { key_size: usize, expected_size: usize, }, #[error( - "Wrapped DiskEncryptionSettings key size {key_size} was smaller than expected {expected_size}" + "wrapped DiskEncryptionSettings key size {key_size} was smaller than expected {expected_size}" )] InvalidWrappedDesKeySize { key_size: usize, expected_size: usize, }, - #[error("Invalid RSA unwrap output size {output_size}, expected {expected_size}")] + #[error("invalid RSA unwrap output size {output_size}, expected {expected_size}")] InvalidRsaUnwrapOutputSize { output_size: usize, expected_size: usize, }, - #[error("Invalid AES unwrap output size {output_size}, expected {expected_size}")] + #[error("invalid AES unwrap output size {output_size}, expected {expected_size}")] InvalidAesUnwrapOutputSize { output_size: usize, expected_size: usize, }, - #[error("Wrapped egress key too large - {key_size} > {expected_size}")] + #[error("wrapped egress key too large - {key_size} > {expected_size}")] InvalidWrappedEgressKeySize { key_size: usize, expected_size: usize, @@ -67,6 +68,11 @@ pub const AES_WRAPPED_AES_KEY_LENGTH: usize = 40; /// AES-Wrapped RSA key size (must be at least RSA 2k) pub const RSA_WRAPPED_AES_KEY_LENGTH: usize = 256; +/// Returns `true` if the DEK buffer contains any non-zero bytes. +pub(crate) fn dek_is_present(dek: &DekKp) -> bool { + dek.dek_buffer.iter().any(|&x| x != 0) +} + /// Extension trait of [`KeyProtector`]. pub trait KeyProtectorExt { /// Unwrap the ingress key for decrypting VMGS (if present) in the Key Protector @@ -80,6 +86,185 @@ pub trait KeyProtectorExt { ) -> Result; } +/// RSA-OAEP unwrap the wrapped DiskEncryptionSettings key. The resulting +/// AES key is used for AES-wrapping/unwrapping the DEK entries in the +/// "3-blob" VMGS layout. +fn unwrap_des_key( + ingress_kek: &RsaKeyPair, + wrapped_des_key: &[u8], + modulus_size: usize, +) -> Result, GetKeysFromKeyProtectorError> { + tracing::info!(CVM_ALLOWED, "wrapped key is present"); + + if wrapped_des_key.len() < modulus_size { + return Err(GetKeysFromKeyProtectorError::InvalidWrappedDesKeySize { + key_size: wrapped_des_key.len(), + expected_size: modulus_size, + }); + } + + let key = ingress_kek + .oaep_decrypt(&wrapped_des_key[..modulus_size], HashAlgorithm::Sha256) + .map_err(GetKeysFromKeyProtectorError::DesKeyRsaUnwrap)?; + + if key.len() != AES_GCM_KEY_LENGTH { + return Err(GetKeysFromKeyProtectorError::InvalidRsaUnwrapOutputSize { + output_size: key.len(), + expected_size: AES_GCM_KEY_LENGTH, + }); + } + Ok(key) +} + +/// Unwrap the existing ingress DEK from `kp.dek[ingress_idx]`. +/// +/// When `des_key` is `Some(_)` the DEK is expected to be AES-wrapped using +/// that key (3-blob layout); otherwise the DEK is expected to be +/// RSA-wrapped with `ingress_kek` (2-blob layout). +fn unwrap_ingress_dek( + kp: &KeyProtector, + ingress_kek: &RsaKeyPair, + des_key: Option<&[u8]>, + ingress_idx: usize, + modulus_size: usize, +) -> Result<[u8; AES_GCM_KEY_LENGTH], GetKeysFromKeyProtectorError> { + tracing::info!(CVM_CONFIDENTIAL, "found dek, index {}", ingress_idx); + + let dek_buffer = &kp.dek[ingress_idx].dek_buffer; + let mut ingress_key = [0u8; AES_GCM_KEY_LENGTH]; + + if let Some(des_key) = des_key { + // Validate the DEK format: the bytes following the AES-wrapped key + // must be zero in the 3-blob layout. + if dek_buffer[AES_WRAPPED_AES_KEY_LENGTH..] + .iter() + .any(|&x| x != 0) + { + return Err(GetKeysFromKeyProtectorError::InvalidDekFormat); + } + + tracing::info!( + CVM_CONFIDENTIAL, + "dek[{}] hold an AES-wrapped key", + ingress_idx + ); + + let aes_unwrapped_key = crypto::aes_key_wrap::AesKeyWrap::new(des_key) + .and_then(|kw| { + kw.unwrapper()? + .unwrap(&dek_buffer[..AES_WRAPPED_AES_KEY_LENGTH]) + }) + .map_err(GetKeysFromKeyProtectorError::IngressDekAesUnwrap)?; + + if aes_unwrapped_key.len() != AES_GCM_KEY_LENGTH { + return Err(GetKeysFromKeyProtectorError::InvalidAesUnwrapOutputSize { + output_size: aes_unwrapped_key.len(), + expected_size: AES_GCM_KEY_LENGTH, + }); + } + + ingress_key[..aes_unwrapped_key.len()].copy_from_slice(&aes_unwrapped_key); + } else { + tracing::info!( + CVM_CONFIDENTIAL, + "dek[{}] hold an RSA-wrapped key", + ingress_idx + ); + + let rsa_unwrapped_key = ingress_kek + .oaep_decrypt(&dek_buffer[..modulus_size], HashAlgorithm::Sha256) + .map_err(GetKeysFromKeyProtectorError::IngressDekRsaUnwrap)?; + + if rsa_unwrapped_key.len() != AES_GCM_KEY_LENGTH { + return Err(GetKeysFromKeyProtectorError::InvalidRsaUnwrapOutputSize { + output_size: rsa_unwrapped_key.len(), + expected_size: AES_GCM_KEY_LENGTH, + }); + } + + ingress_key[..rsa_unwrapped_key.len()].copy_from_slice(&rsa_unwrapped_key); + } + + Ok(ingress_key) +} + +/// Unwrap an existing (non-empty) egress DEK left over from a previously +/// incomplete key rotation. +/// +/// The returned key MUST NOT be used to re-encrypt the VMGS — the host +/// controls its value. It is only safe for decrypting an existing VMGS. +fn unwrap_existing_egress_dek( + kp: &KeyProtector, + ingress_kek: &RsaKeyPair, + des_key: Option<&[u8]>, + egress_idx: usize, + modulus_size: usize, +) -> Result<[u8; AES_GCM_KEY_LENGTH], GetKeysFromKeyProtectorError> { + tracing::info!(CVM_ALLOWED, "found egress dek"); + + let dek_buffer = kp.dek[egress_idx].dek_buffer; + let old_egress_key = if let Some(unwrapping_key) = des_key { + // The DEK buffer should contain an AES-wrapped key. + crypto::aes_key_wrap::AesKeyWrap::new(unwrapping_key) + .and_then(|kw| { + kw.unwrapper()? + .unwrap(&dek_buffer[..AES_WRAPPED_AES_KEY_LENGTH]) + }) + .map_err(GetKeysFromKeyProtectorError::EgressDekAesUnwrap)? + } else { + // The DEK buffer should contain an RSA-wrapped key. + ingress_kek + .oaep_decrypt(&dek_buffer[..modulus_size], HashAlgorithm::Sha256) + .map_err(GetKeysFromKeyProtectorError::EgressDekRsaUnwrap)? + }; + let mut key = [0u8; AES_GCM_KEY_LENGTH]; + key[..old_egress_key.len()].copy_from_slice(&old_egress_key); + Ok(key) +} + +/// Wrap the newly-generated random `encrypt_egress_key` (with AES-wrap if +/// `des_key` is provided, otherwise RSA-OAEP) and store the result in +/// `kp.dek[egress_idx]`. +fn wrap_and_store_new_egress_key( + kp: &mut KeyProtector, + ingress_kek: &RsaKeyPair, + des_key: Option<&[u8]>, + egress_idx: usize, + encrypt_egress_key: &[u8; AES_GCM_KEY_LENGTH], +) -> Result<(), GetKeysFromKeyProtectorError> { + use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE; + + let new_egress_key = if let Some(wrapping_key) = des_key { + // Create an AES wrapped key + crypto::aes_key_wrap::AesKeyWrap::new(wrapping_key) + .and_then(|kw| kw.wrapper()?.wrap(encrypt_egress_key)) + .map_err(GetKeysFromKeyProtectorError::EgressKeyAesWrap)? + } else { + // Create an RSA wrapped key + ingress_kek + .oaep_encrypt(encrypt_egress_key, HashAlgorithm::Sha256) + .map_err(GetKeysFromKeyProtectorError::EgressKeyRsaWrap)? + }; + + if new_egress_key.len() > DEK_BUFFER_SIZE { + return Err(GetKeysFromKeyProtectorError::InvalidWrappedEgressKeySize { + key_size: new_egress_key.len(), + expected_size: DEK_BUFFER_SIZE, + }); + } + + kp.dek[egress_idx].dek_buffer[..new_egress_key.len()].copy_from_slice(&new_egress_key); + + tracing::info!( + CVM_CONFIDENTIAL, + egress_idx = egress_idx, + egress_key_len = new_egress_key.len(), + "store new egress key to dek" + ); + + Ok(()) +} + impl KeyProtectorExt for KeyProtector { fn unwrap_and_rotate_keys( &mut self, @@ -90,176 +275,66 @@ impl KeyProtectorExt for KeyProtector { ) -> Result { use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE; - let found_ingress_dek = !self.dek[ingress_idx].dek_buffer.iter().all(|&x| x == 0); - let found_egress_dek = !self.dek[egress_idx].dek_buffer.iter().all(|&x| x == 0); - let mut ingress_key = [0u8; AES_GCM_KEY_LENGTH]; - let mut encrypt_egress_key = [0u8; AES_GCM_KEY_LENGTH]; - let use_des_key = wrapped_des_key.is_some(); // whether the wrapped key from DiskEncryptionSettings payload is used + let found_ingress_dek = dek_is_present(&self.dek[ingress_idx]); + let found_egress_dek = dek_is_present(&self.dek[egress_idx]); let modulus_size = ingress_kek.modulus_size(); - // If the `dek` entry is not empty or `wrapped_des_key` (RSA-wrapped) is present, decrypt the ingress key. - // The use of `wrapped_des_key` from DiskEncryptionSettings implies that VMGS structure is new (3-blob) where - // the `dek` entry contains an AES-wrapped key. The AES-wrapped key can be unwrapped by the - // decrypted `wrapped_key` (using `ingress_kek`). Otherwise, VMGS structure should be old (2-blob) - // where the `dek` is an RSA-wrapped key. The RSA-wrapped key can be unwrapped by the `ingress_kek`. - let des_key = if found_ingress_dek || use_des_key { - if found_ingress_dek && use_des_key { - // Validate the DEK format, which is expected to hold an AES-wrapped key - // when `wrapped_des_key` is `Some`. - if !self.dek[ingress_idx].dek_buffer[AES_WRAPPED_AES_KEY_LENGTH..] - .iter() - .all(|&x| x == 0) - { - Err(GetKeysFromKeyProtectorError::InvalidDekFormat)? - } - } - - if modulus_size > DEK_BUFFER_SIZE { - Err(GetKeysFromKeyProtectorError::InvalidIngressRsaKekSize { - key_size: modulus_size, - expected_size: DEK_BUFFER_SIZE, - })? - } - - let rsa_unwrapped_key = if let Some(wrapped_des_key) = wrapped_des_key { - tracing::info!(CVM_ALLOWED, "wrapped key is present"); - - if wrapped_des_key.len() < modulus_size { - Err(GetKeysFromKeyProtectorError::InvalidWrappedDesKeySize { - key_size: wrapped_des_key.len(), - expected_size: modulus_size, - })? - } - - ingress_kek - .oaep_decrypt(&wrapped_des_key[..modulus_size], HashAlgorithm::Sha256) - .map_err(GetKeysFromKeyProtectorError::DesKeyRsaUnwrap)? - } else { - // The DEK buffer should contain an RSA-wrapped key. - tracing::info!(CVM_CONFIDENTIAL, "found dek, index {}", ingress_idx); - - ingress_kek - .oaep_decrypt( - &self.dek[ingress_idx].dek_buffer[..modulus_size], - HashAlgorithm::Sha256, - ) - .map_err(GetKeysFromKeyProtectorError::IngressDekRsaUnwrap)? - }; - - if rsa_unwrapped_key.len() != AES_GCM_KEY_LENGTH { - Err(GetKeysFromKeyProtectorError::InvalidRsaUnwrapOutputSize { - output_size: rsa_unwrapped_key.len(), - expected_size: AES_GCM_KEY_LENGTH, - })? - } - - if found_ingress_dek { - if use_des_key { - tracing::info!( - CVM_CONFIDENTIAL, - "dek[{}] hold an AES-wrapped key", - ingress_idx - ); - - // The DEK buffer should contain an AES-wrapped key. - let dek_buffer = &self.dek[ingress_idx].dek_buffer; - let aes_unwrapped_key = - crypto::aes_key_wrap::AesKeyWrap::new(&rsa_unwrapped_key) - .and_then(|kw| { - kw.unwrapper()? - .unwrap(&dek_buffer[..AES_WRAPPED_AES_KEY_LENGTH]) - }) - .map_err(GetKeysFromKeyProtectorError::IngressDekAesUnwrap)?; - - if aes_unwrapped_key.len() != AES_GCM_KEY_LENGTH { - Err(GetKeysFromKeyProtectorError::InvalidAesUnwrapOutputSize { - output_size: aes_unwrapped_key.len(), - expected_size: AES_GCM_KEY_LENGTH, - })? - } - - ingress_key[..aes_unwrapped_key.len()].copy_from_slice(&aes_unwrapped_key); - } else { - tracing::info!( - CVM_CONFIDENTIAL, - "dek[{}] hold an RSA-wrapped key", - ingress_idx - ); - - ingress_key[..rsa_unwrapped_key.len()].copy_from_slice(&rsa_unwrapped_key); - } - } - - if use_des_key { - Some(rsa_unwrapped_key) - } else { - None - } + // The RSA modulus indexes into a fixed-size DEK buffer on the + // RSA-unwrap paths; reject keys that wouldn't fit before using + // `modulus_size` as a slice end. + let needs_rsa_unwrap = wrapped_des_key.is_some() || found_ingress_dek || found_egress_dek; + if needs_rsa_unwrap && modulus_size > DEK_BUFFER_SIZE { + return Err(GetKeysFromKeyProtectorError::InvalidIngressRsaKekSize { + key_size: modulus_size, + expected_size: DEK_BUFFER_SIZE, + }); + } + + // Stage A: optionally unwrap the DES key, then unwrap the ingress DEK. + let des_key = match wrapped_des_key { + Some(buf) => Some(unwrap_des_key(ingress_kek, buf, modulus_size)?), + None => None, + }; + let ingress_key = if found_ingress_dek { + unwrap_ingress_dek( + self, + ingress_kek, + des_key.as_deref(), + ingress_idx, + modulus_size, + )? } else { - None + [0u8; AES_GCM_KEY_LENGTH] }; + // Stage B: optionally unwrap any pre-existing egress DEK left + // behind by a previously-failed key rotation. let decrypt_egress_key = if found_egress_dek { - tracing::info!(CVM_ALLOWED, "found egress dek"); - - // Key rolling did not complete successfully last time (normally egress should be empty). - // Any existing egress key can be used to decrypt the VMGS but must not be used to - // re-encrypt the VMGS, as its value can be controlled by the host. - let dek_buffer = self.dek[egress_idx].dek_buffer; - let old_egress_key = if let Some(unwrapping_key) = &des_key { - // The DEK buffer should contain an AES-wrapped key. - crypto::aes_key_wrap::AesKeyWrap::new(unwrapping_key) - .and_then(|kw| { - kw.unwrapper()? - .unwrap(&dek_buffer[..AES_WRAPPED_AES_KEY_LENGTH]) - }) - .map_err(GetKeysFromKeyProtectorError::EgressDekAesUnwrap)? - } else { - // The DEK buffer should contain an RSA-wrapped key. - ingress_kek - .oaep_decrypt(&dek_buffer[..modulus_size], HashAlgorithm::Sha256) - .map_err(GetKeysFromKeyProtectorError::EgressDekRsaUnwrap)? - }; - let mut key = [0u8; AES_GCM_KEY_LENGTH]; - key[..old_egress_key.len()].copy_from_slice(&old_egress_key); - Some(key) + Some(unwrap_existing_egress_dek( + self, + ingress_kek, + des_key.as_deref(), + egress_idx, + modulus_size, + )?) } else { tracing::info!(CVM_ALLOWED, "there is no egress dek"); None }; - // Always generate a new "encrypt egress key". This is generated randomly by - // OpenHCL, and so cannot be controlled by the host, and is safe to use to - // encrypt the VMGS. + // Stage C: generate a fresh random egress key, wrap it, and store + // it in the egress DEK slot. The key is generated by OpenHCL so + // the host cannot influence its value. + let mut encrypt_egress_key = [0u8; AES_GCM_KEY_LENGTH]; getrandom::fill(&mut encrypt_egress_key).expect("rng failure"); - let new_egress_key = if let Some(wrapping_key) = des_key { - // Create an AES wrapped key - crypto::aes_key_wrap::AesKeyWrap::new(&wrapping_key) - .and_then(|kw| kw.wrapper()?.wrap(&encrypt_egress_key)) - .map_err(GetKeysFromKeyProtectorError::EgressKeyAesWrap)? - } else { - // Create an RSA wrapped key - ingress_kek - .oaep_encrypt(&encrypt_egress_key, HashAlgorithm::Sha256) - .map_err(GetKeysFromKeyProtectorError::EgressKeyRsaWrap)? - }; - - if new_egress_key.len() > DEK_BUFFER_SIZE { - Err(GetKeysFromKeyProtectorError::InvalidWrappedEgressKeySize { - key_size: new_egress_key.len(), - expected_size: DEK_BUFFER_SIZE, - })? - } - - self.dek[egress_idx].dek_buffer[..new_egress_key.len()].copy_from_slice(&new_egress_key); - - tracing::info!( - CVM_CONFIDENTIAL, - egress_idx = egress_idx, - egress_key_len = new_egress_key.len(), - "store new egress key to dek" - ); + wrap_and_store_new_egress_key( + self, + ingress_kek, + des_key.as_deref(), + egress_idx, + &encrypt_egress_key, + )?; Ok(Keys { ingress: ingress_key, @@ -545,7 +620,9 @@ mod tests { let result = key_protector.unwrap_and_rotate_keys(&kek, Some(rsa_wrapped_des.as_ref()), 0, 1); - assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "The DEK format expects to hold an RSA-WRAPPED AES key, but found an AES-WRAPPED AES key".to_string()) + assert!(matches!( + result, + Err(GetKeysFromKeyProtectorError::InvalidDekFormat) + )) } } diff --git a/openhcl/underhill_attestation/src/lib.rs b/openhcl/underhill_attestation/src/lib.rs index dbb1205083..49ff042895 100644 --- a/openhcl/underhill_attestation/src/lib.rs +++ b/openhcl/underhill_attestation/src/lib.rs @@ -9,6 +9,7 @@ #![cfg(target_os = "linux")] #![forbid(unsafe_code)] +mod derived_keys; mod hardware_key_sealing; mod igvm_attest; mod jwt; @@ -19,6 +20,9 @@ mod vmgs; #[cfg(test)] mod test_helpers; +#[cfg(test)] +mod tests; + 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; @@ -26,33 +30,24 @@ pub use igvm_attest::ak_cert::parse_response as parse_ak_cert_response; use ::vmgs::EncryptionAlgorithm; use ::vmgs::GspType; use ::vmgs::Vmgs; -use crypto::rsa::RsaKeyPair; use cvm_tracing::CVM_ALLOWED; +use derived_keys::GetDerivedKeysError; use get_protocol::dps_json::GuestStateEncryptionPolicy; use guest_emulation_transport::GuestEmulationTransportClient; use guest_emulation_transport::api::GspExtendedStatusFlags; -use guest_emulation_transport::api::GuestStateProtection; -use guest_emulation_transport::api::GuestStateProtectionById; use guid::Guid; -use hardware_key_sealing::HardwareDerivedKeys; -use hardware_key_sealing::HardwareKeyProtectorExt as _; -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::vmgs::AES_GCM_KEY_LENGTH; use openhcl_attestation_protocol::vmgs::AGENT_DATA_MAX_SIZE; -use openhcl_attestation_protocol::vmgs::HardwareKeyProtector; use openhcl_attestation_protocol::vmgs::KeyProtector; use openhcl_attestation_protocol::vmgs::SecurityProfile; use pal_async::local::LocalDriver; use secure_key_release::VmgsEncryptionKeys; -use static_assertions::const_assert_eq; use std::fmt::Debug; use tee_call::TeeCall; use thiserror::Error; use zerocopy::FromZeros; -use zerocopy::IntoBytes; /// An attestation error. #[derive(Debug, Error)] @@ -67,7 +62,7 @@ impl> From for Error { #[derive(Debug, Error)] enum AttestationErrorInner { - #[error("read security profile from vmgs")] + #[error("failed to read security profile from vmgs")] ReadSecurityProfile(#[source] vmgs::ReadFromVmgsError), #[error("failed to get derived keys")] GetDerivedKeys(#[source] GetDerivedKeysError), @@ -81,60 +76,6 @@ enum AttestationErrorInner { ReadGuestSecretKey(#[source] vmgs::ReadFromVmgsError), } -#[derive(Debug, Error)] -enum GetDerivedKeysError { - #[error("failed to get ingress/egress keys from the the key protector")] - GetKeysFromKeyProtector(#[source] GetKeysFromKeyProtectorError), - #[error("failed to fetch GSP")] - FetchGuestStateProtectionById( - #[source] guest_emulation_transport::error::GuestStateProtectionByIdError, - ), - #[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, - ), - #[error("failed to get an ingress key from key protector")] - GetIngressKeyFromKpFailed, - #[error("failed to get an ingress key from guest state protection")] - GetIngressKeyFromKGspFailed, - #[error("failed to get an ingress key from guest state protection by id")] - GetIngressKeyFromKGspByIdFailed, - #[error("Encryption cannot be disabled if VMGS was previously encrypted")] - DisableVmgsEncryptionFailed, - #[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), - #[error("failed to write to `FileId::HW_KEY_PROTECTOR` in vmgs")] - VmgsWriteHardwareKeyProtector(#[source] vmgs::WriteToVmgsError), - #[error("failed to get derived key by id")] - GetDerivedKeyById(#[source] GetDerivedKeysByIdError), - #[error("failed to derive an ingress key")] - DeriveIngressKey(#[source] crypto::kdf::KdfError), - #[error("failed to derive an egress key")] - DeriveEgressKey(#[source] crypto::kdf::KdfError), -} - -#[derive(Debug, Error)] -enum GetDerivedKeysByIdError { - #[error("failed to derive an egress key based on current vm bios guid")] - DeriveEgressKeyUsingCurrentVmId(#[source] crypto::kdf::KdfError), - #[error("invalid derived egress key size {key_size}, expected {expected_size}")] - InvalidDerivedEgressKeySize { - key_size: usize, - expected_size: usize, - }, - #[error("failed to derive an ingress key based on key protector Id from vmgs")] - DeriveIngressKeyUsingKeyProtectorId(#[source] crypto::kdf::KdfError), - #[error("invalid derived egress key size {key_size}, expected {expected_size}")] - InvalidDerivedIngressKeySize { - key_size: usize, - expected_size: usize, - }, -} - #[derive(Debug, Error)] enum UnlockVmgsDataStoreError { #[error("failed to unlock vmgs with the existing egress key")] @@ -167,19 +108,6 @@ enum LogOpType { ConvertEncryptionType, } -/// Label used by `derive_key` -const VMGS_KEY_DERIVE_LABEL: &[u8; 7] = b"VMGSKEY"; - -/// KBKDF from SP800-108, using HMAC-SHA-256. -fn derive_key( - key: &[u8], - context: &[u8], - label: &[u8], -) -> Result<[u8; AES_GCM_KEY_LENGTH], crypto::kdf::KdfError> { - let output = crypto::kdf::kbkdf_hmac_sha256(key, context, label, AES_GCM_KEY_LENGTH)?; - Ok(output.try_into().unwrap()) -} - #[derive(Debug)] struct Keys { ingress: [u8; AES_GCM_KEY_LENGTH], @@ -187,27 +115,88 @@ struct Keys { encrypt_egress: [u8; AES_GCM_KEY_LENGTH], } -/// Key protector settings -#[derive(Clone, Copy)] -struct KeyProtectorSettings { - /// Whether to update key protector +/// Actions to apply to the on-disk key protector blobs once the unlock +/// strategy has been chosen by [`get_derived_keys`]. +/// +/// [`get_derived_keys`]: derived_keys::get_derived_keys +#[derive(Clone, Copy, Default)] +struct KeyProtectorActions { + /// Write the rotated [`KeyProtector`] back to VMGS. should_write_kp: bool, - /// Whether GSP by id is used + /// Update the per-VM-id key protector entry. use_gsp_by_id: bool, - /// Whether hardware key sealing is used + /// True when the VMGS was unlocked using a hardware-sealed key. The + /// in-memory [`KeyProtector`] must not be altered in this case. use_hardware_unlock: bool, - /// GSP type used for decryption (for logging) - decrypt_gsp_type: GspType, - /// GSP type used for encryption (for logging) - encrypt_gsp_type: GspType, } -/// Helper struct for [`protocol::vmgs::KeyProtectorById`] -struct KeyProtectorById { - /// The instance of [`protocol::vmgs::KeyProtectorById`]. - pub inner: openhcl_attestation_protocol::vmgs::KeyProtectorById, - /// Indicate if the instance is read from the VMGS file. - pub found_id: bool, +/// Records which GSP type was used for each side of the key rotation. +/// Used solely for observability tracing in [`initialize_platform_security`]. +#[derive(Clone, Copy)] +struct GspTypeRecord { + /// GSP type used to decrypt the existing (ingress) VMGS contents. + decrypt: GspType, + /// GSP type used to encrypt the new (egress) VMGS contents. + encrypt: GspType, +} + +impl Default for GspTypeRecord { + fn default() -> Self { + Self { + decrypt: GspType::None, + encrypt: GspType::None, + } + } +} + +/// In-memory representation of the per-VM-id key protector entry stored in +/// VMGS. +/// +/// On first boot (or when the entry has never been written) the entry is +/// absent. The orchestration code distinguishes the two states explicitly +/// rather than relying on "all-zeros means not found" sentinel values. +enum KeyProtectorById { + /// The entry was loaded from VMGS. + Found(openhcl_attestation_protocol::vmgs::KeyProtectorById), + /// No entry was present in VMGS. + NotFound, +} + +impl KeyProtectorById { + /// Returns the on-disk id, or [`Guid::ZERO`] when the entry was not + /// found. Used by callers that need to compare against the current + /// `bios_guid` or detect an unprovisioned slot. + fn id_guid(&self) -> Guid { + match self { + Self::Found(inner) => inner.id_guid, + Self::NotFound => Guid::ZERO, + } + } + + /// Borrow the inner protocol struct mutably, transitioning from + /// [`Self::NotFound`] to [`Self::Found`] (with a freshly-zeroed inner) + /// when needed. Used by call sites that are about to write the entry + /// back to VMGS, since the on-disk write implies the entry now exists. + fn ensure_found_mut(&mut self) -> &mut openhcl_attestation_protocol::vmgs::KeyProtectorById { + if matches!(self, Self::NotFound) { + *self = Self::Found(openhcl_attestation_protocol::vmgs::KeyProtectorById::new_zeroed()); + } + let Self::Found(inner) = self else { + unreachable!("transitioned to Found above") + }; + inner + } + + /// Test helper that extracts the inner protocol struct, panicking on + /// [`Self::NotFound`]. Used by tests that assert the in-memory entry + /// matches what was just written to VMGS. + #[cfg(test)] + fn inner_for_test(&self) -> &openhcl_attestation_protocol::vmgs::KeyProtectorById { + let Self::Found(inner) = self else { + panic!("expected KeyProtectorById::Found"); + }; + inner + } } /// Host attestation settings obtained via the GET GSP call-out. @@ -220,8 +209,11 @@ pub struct HostAttestationSettings { struct DerivedKeyResult { /// Optional derived keys. derived_keys: Option, - /// The instance of [`KeyProtectorSettings`]. - key_protector_settings: KeyProtectorSettings, + /// Actions for the orchestration code to apply to the on-disk key + /// protector blobs. + actions: KeyProtectorActions, + /// Observability record of which GSP types were used (logging only). + gsp_types: GspTypeRecord, /// The instance of [`GspExtendedStatusFlags`] returned by GSP. gsp_extended_status_flags: GspExtendedStatusFlags, } @@ -236,6 +228,35 @@ pub struct PlatformAttestationData { pub guest_secret_key: Option>, } +/// An error paired with a retry hint. +/// +/// Used by attestation flows where some failures are transient (host service +/// unavailable, TDX service VM not yet ready, dynamic firmware update in +/// flight) and the caller should retry, while other failures are fatal and +/// must be propagated immediately. +#[derive(Debug)] +pub(crate) struct Retryable { + /// The underlying error. + pub error: E, + /// Whether the operation can be retried. + pub can_retry: bool, +} + +impl Retryable { + /// Wraps a fatal error that should not be retried. + pub(crate) fn fatal(error: E) -> Self { + Self { + error, + can_retry: false, + } + } + + /// Wraps an error with the given retry hint. + pub(crate) fn with_retry(error: E, can_retry: bool) -> Self { + Self { error, can_retry } + } +} + /// The attestation type to use. // TODO: Support VBS #[derive(Debug, MeshPayload, Copy, Clone, PartialEq, Eq)] @@ -251,9 +272,10 @@ pub enum AttestationType { } /// Request VMGS encryption keys and unlock the VMGS. -/// If successful, return a bool indicating whether igvmagent requested a -/// state refresh. If unsuccessful, return an error and a bool indicating -/// whether to retry. +/// +/// On success, returns a bool indicating whether igvmagent requested a +/// state refresh. On failure, returns a [`Retryable`] carrying the error +/// and whether the caller should retry. async fn try_unlock_vmgs( get: &GuestEmulationTransportClient, bios_guid: Guid, @@ -264,7 +286,7 @@ async fn try_unlock_vmgs( strict_encryption_policy: bool, agent_data: &mut [u8; AGENT_DATA_MAX_SIZE], key_protector_by_id: &mut KeyProtectorById, -) -> Result { +) -> Result> { let skr_response = if let Some(tee_call) = tee_call { tracing::info!(CVM_ALLOWED, "Retrieving key-encryption key"); @@ -286,13 +308,13 @@ async fn try_unlock_vmgs( let retry = match &skr_response { Ok(_) => false, - Err((_, r)) => *r, + Err(r) => r.can_retry, }; let skip_hw_unsealing = matches!( &skr_response, - Err(( - secure_key_release::RequestVmgsEncryptionKeysError::ParseIgvmAttestKeyReleaseResponse( + Err(Retryable { + error: secure_key_release::RequestVmgsEncryptionKeysError::ParseIgvmAttestKeyReleaseResponse( igvm_attest::key_release::KeyReleaseError::ParseHeader( igvm_attest::Error::Attestation { skip_hw_unsealing_signal: true, @@ -300,8 +322,8 @@ async fn try_unlock_vmgs( }, ), ), - _, - )) + .. + }) ); let VmgsEncryptionKeys { @@ -313,11 +335,11 @@ async fn try_unlock_vmgs( tracing::info!(CVM_ALLOWED, "Successfully retrieved key-encryption key"); k } - Err((e, _)) => { + Err(Retryable { error, .. }) => { // Non-fatal, allowing for hardware-based recovery tracing::error!( CVM_ALLOWED, - error = &e as &dyn std::error::Error, + error = &error as &dyn std::error::Error, "Failed to retrieve key-encryption key" ); @@ -340,7 +362,7 @@ async fn try_unlock_vmgs( ); let mut key_protector = vmgs::read_key_protector(vmgs, dek_minimal_size) .await - .map_err(|e| (AttestationErrorInner::ReadKeyProtector(e), false))?; + .map_err(|e| Retryable::fatal(AttestationErrorInner::ReadKeyProtector(e)))?; let start_time = std::time::SystemTime::now(); let vmgs_encrypted = vmgs.encrypted(); @@ -351,7 +373,7 @@ async fn try_unlock_vmgs( "Deriving keys" ); - let derived_keys_result = get_derived_keys( + let derived_keys_result = derived_keys::get_derived_keys( get, tee_call, vmgs, @@ -379,7 +401,7 @@ async fn try_unlock_vmgs( .map_or(0, |d| d.as_millis()), "Failed to derive keys" ); - (AttestationErrorInner::GetDerivedKeys(e), retry) + Retryable::with_retry(AttestationErrorInner::GetDerivedKeys(e), retry) })?; // All Underhill VMs use VMGS encryption @@ -390,7 +412,7 @@ async fn try_unlock_vmgs( &mut key_protector, key_protector_by_id, derived_keys_result.derived_keys, - derived_keys_result.key_protector_settings, + derived_keys_result.actions, bios_guid, ) .await @@ -408,19 +430,18 @@ async fn try_unlock_vmgs( get.event_log_fatal(guest_emulation_transport::api::EventLogId::ATTESTATION_FAILED) .await; - Err((AttestationErrorInner::UnlockVmgsDataStore(e), retry))?; + return Err(Retryable::with_retry( + AttestationErrorInner::UnlockVmgsDataStore(e), + retry, + )); } tracing::info!( CVM_ALLOWED, op_type = ?LogOpType::DecryptVmgs, success = true, - decrypt_gsp_type = ?derived_keys_result - .key_protector_settings - .decrypt_gsp_type, - encrypt_gsp_type = ?derived_keys_result - .key_protector_settings - .encrypt_gsp_type, + decrypt_gsp_type = ?derived_keys_result.gsp_types.decrypt, + encrypt_gsp_type = ?derived_keys_result.gsp_types.encrypt, latency = std::time::SystemTime::now().duration_since(start_time).map_or(0, |d| d.as_millis()), "Unlocked datastore" ); @@ -446,8 +467,14 @@ pub async fn initialize_platform_security( guest_state_encryption_policy: GuestStateEncryptionPolicy, strict_encryption_policy: bool, ) -> Result { - const MAXIMUM_RETRY_COUNT: usize = 10; - const NO_RETRY_COUNT: usize = 1; + // Maximum number of attempts when the VMGS is encrypted and the + // attestation call-out may transiently fail (IGVm agent down for + // servicing, TDX service VM not ready, or a dynamic firmware update + // means the report is not verifiable yet). + const ENCRYPTED_VMGS_MAX_ATTEMPTS: usize = 10; + // When the VMGS is not encrypted there is no benefit to retrying; + // make a single attempt and surface the error. + const UNENCRYPTED_VMGS_MAX_ATTEMPTS: usize = 1; tracing::info!(CVM_ALLOWED, tee_type=?tee_call.map(|tee| tee.tee_type()), @@ -480,22 +507,16 @@ 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)) }?, + Ok(inner) => KeyProtectorById::Found(inner), + Err(vmgs::ReadFromVmgsError::EntryNotFound(_)) => KeyProtectorById::NotFound, + Err(e) => return Err(AttestationErrorInner::ReadKeyProtectorById(e).into()), }; // 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; + let vm_id_changed = if let KeyProtectorById::Found(inner) = &key_protector_by_id { + let changed = inner.id_guid != bios_guid; if changed { - tracing::info!("VM Id has changed since last boot"); + tracing::info!(CVM_ALLOWED, "VM Id has changed since last boot"); }; changed } else { @@ -509,9 +530,9 @@ pub async fn initialize_platform_security( // update could mean that the report was not verifiable. let vmgs_encrypted: bool = vmgs.encrypted(); let max_retry = if vmgs_encrypted { - MAXIMUM_RETRY_COUNT + ENCRYPTED_VMGS_MAX_ATTEMPTS } else { - NO_RETRY_COUNT + UNENCRYPTED_VMGS_MAX_ATTEMPTS }; let mut timer = pal_async::timer::PolledTimer::new(&driver); @@ -535,10 +556,16 @@ pub async fn initialize_platform_security( match response { Ok(b) => break b, - Err((e, false)) => Err(e)?, - Err((e, true)) => { + Err(Retryable { + error, + can_retry: false, + }) => return Err(error.into()), + Err(Retryable { + error, + can_retry: true, + }) => { if i >= max_retry - 1 { - Err(e)? + return Err(error.into()); } } } @@ -549,7 +576,7 @@ pub async fn initialize_platform_security( }; let host_attestation_settings = HostAttestationSettings { - refresh_tpm_seeds: { state_refresh_request_from_gsp | vm_id_changed }, + refresh_tpm_seeds: state_refresh_request_from_gsp || vm_id_changed, }; tracing::info!( @@ -585,7 +612,7 @@ async fn unlock_vmgs_data_store( key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, derived_keys: Option, - key_protector_settings: KeyProtectorSettings, + actions: KeyProtectorActions, bios_guid: Guid, ) -> Result<(), UnlockVmgsDataStoreError> { let mut new_key = false; // Indicate if we need to add a new key after unlock @@ -621,9 +648,9 @@ async fn unlock_vmgs_data_store( .await .map_err(UnlockVmgsDataStoreError::VmgsUnlockUsingExistingEgressKey)?; } else { - Err(UnlockVmgsDataStoreError::VmgsUnlockUsingExistingIngressKey( + return Err(UnlockVmgsDataStoreError::VmgsUnlockUsingExistingIngressKey( e, - ))? + )); } } } else { @@ -637,22 +664,27 @@ async fn unlock_vmgs_data_store( tracing::info!( CVM_ALLOWED, - should_write_kp = key_protector_settings.should_write_kp, - use_gsp_by_id = key_protector_settings.use_gsp_by_id, - use_hardware_unlock = key_protector_settings.use_hardware_unlock, + should_write_kp = actions.should_write_kp, + use_gsp_by_id = actions.use_gsp_by_id, + use_hardware_unlock = actions.use_hardware_unlock, "key protector settings" ); - if key_protector_settings.should_write_kp { + if actions.should_write_kp { // Update on disk KP with all seeds used, to allow for disaster recovery vmgs::write_key_protector(key_protector, vmgs) .await .map_err(UnlockVmgsDataStoreError::WriteKeyProtector)?; - if key_protector_settings.use_gsp_by_id { - vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, false, bios_guid) - .await - .map_err(UnlockVmgsDataStoreError::WriteKeyProtectorById)?; + if actions.use_gsp_by_id { + vmgs::write_key_protector_by_id( + key_protector_by_id.ensure_found_mut(), + vmgs, + false, + bios_guid, + ) + .await + .map_err(UnlockVmgsDataStoreError::WriteKeyProtectorById)?; } } @@ -666,701 +698,9 @@ async fn unlock_vmgs_data_store( } // Persist KP to VMGS - persist_all_key_protectors( - vmgs, - key_protector, - key_protector_by_id, - bios_guid, - key_protector_settings, - ) - .await - .map_err(UnlockVmgsDataStoreError::PersistAllKeyProtectors) -} - -/// Update data store keys with key protectors. -/// VMGS encryption can come from combinations of three sources, -/// a Tenant Key (KEK), GSP, and GSP By Id. -/// There is an Ingress Key (previously used to lock the VMGS), -/// and an Egress Key (new key for locking the VMGS), and these -/// keys can be derived differently, where KEK is -/// always used if available, and GSP is preferred to GSP By Id. -/// Ingress Possible Egress in order of preference [Ingress] -/// - No Encryption - All -/// - GSP By Id - KEK + GSP, KEK + GSP By Id, GSP, [GSP By Id] -/// - GSP (v10 VM and later) - KEK + GSP, [GSP] -/// - KEK (IVM only) - KEK + GSP, KEK + GSP By Id, [KEK] -/// - KEK + GSP By Id - KEK + GSP, [KEK + GSP By Id] -/// - KEK + GSP - [KEK + GSP] -/// -/// NOTE: for TVM parity, only None, Gsp By Id v9.1, and Gsp By Id / Gsp v10.0 are used. -async fn get_derived_keys( - get: &GuestEmulationTransportClient, - tee_call: Option<&dyn TeeCall>, - vmgs: &mut Vmgs, - key_protector: &mut KeyProtector, - key_protector_by_id: &mut KeyProtectorById, - bios_guid: Guid, - attestation_vm_config: &AttestationVmConfig, - is_encrypted: bool, - ingress_rsa_kek: Option<&RsaKeyPair>, - wrapped_des_key: Option<&[u8]>, - tcb_version: Option, - guest_state_encryption_policy: GuestStateEncryptionPolicy, - strict_encryption_policy: bool, - skip_hw_unsealing: bool, -) -> Result { - tracing::info!( - CVM_ALLOWED, - ?guest_state_encryption_policy, - strict_encryption_policy, - "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, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::None, - encrypt_gsp_type: GspType::None, - }; - - let mut derived_keys = Keys { - ingress: [0u8; AES_GCM_KEY_LENGTH], - decrypt_egress: None, - encrypt_egress: [0u8; AES_GCM_KEY_LENGTH], - }; - - // Ingress / Egress seed values depend on what happened previously to the datastore - let ingress_idx = (key_protector.active_kp % 2) as usize; - let egress_idx = if ingress_idx == 0 { 1 } else { 0 } as usize; - - let found_dek = !key_protector.dek[ingress_idx] - .dek_buffer - .iter() - .all(|&x| x == 0); - - // Handle key released via attestation process (tenant key) to get keys from KeyProtector - let (ingress_key, mut decrypt_egress_key, encrypt_egress_key, no_kek) = - if let Some(ingress_kek) = ingress_rsa_kek { - let keys = match key_protector.unwrap_and_rotate_keys( - ingress_kek, - wrapped_des_key, - ingress_idx, - egress_idx, - ) { - Ok(keys) => keys, - Err(e) - if matches!( - e, - GetKeysFromKeyProtectorError::DesKeyRsaUnwrap(_) - | GetKeysFromKeyProtectorError::IngressDekRsaUnwrap(_) - ) => - { - get.event_log_fatal( - guest_emulation_transport::api::EventLogId::DEK_DECRYPTION_FAILED, - ) - .await; - - return Err(GetDerivedKeysError::GetKeysFromKeyProtector(e)); - } - Err(e) => return Err(GetDerivedKeysError::GetKeysFromKeyProtector(e)), - }; - ( - keys.ingress, - keys.decrypt_egress, - keys.encrypt_egress, - false, - ) - } else { - ( - [0u8; AES_GCM_KEY_LENGTH], - None, - [0u8; AES_GCM_KEY_LENGTH], - true, - ) - }; - - // Handle various sources of Guest State Protection - let existing_unencrypted = !vmgs.encrypted() && !vmgs.was_provisioned_this_boot(); - let is_gsp_by_id = key_protector_by_id.found_id && key_protector_by_id.inner.ported != 1; - let is_gsp = key_protector.gsp[ingress_idx].gsp_length != 0; - tracing::info!( - CVM_ALLOWED, - is_encrypted, - is_gsp_by_id, - is_gsp, - found_dek, - "initial vmgs encryption state" - ); - let mut requires_gsp_by_id = is_gsp_by_id; - - // Attempt GSP - let (gsp_response, gsp_available, no_gsp, requires_gsp) = { - tracing::info!(CVM_ALLOWED, "attempting GSP"); - - let response = get_gsp_data(get, key_protector).await; - - tracing::info!( - CVM_ALLOWED, - request_data_length_in_vmgs = key_protector.gsp[ingress_idx].gsp_length, - no_rpc_server = response.extended_status_flags.no_rpc_server(), - requires_rpc_server = response.extended_status_flags.requires_rpc_server(), - encrypted_gsp_length = response.encrypted_gsp.length, - "GSP response" - ); - - let no_gsp_available = - response.extended_status_flags.no_rpc_server() || response.encrypted_gsp.length == 0; - - let no_gsp = no_gsp_available - // disable if auto and pre-existing guest state is not encrypted or - // encrypted using GspById to prevent encryption changes without - // explicit intent - || (matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::Auto - ) && (is_gsp_by_id || existing_unencrypted)) - // disable per encryption policy (first boot only, unless strict) - || (matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::GspById | GuestStateEncryptionPolicy::None - ) && (!is_gsp || strict_encryption_policy)); - - let requires_gsp = is_gsp - || response.extended_status_flags.requires_rpc_server() - || (matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::GspKey - ) && strict_encryption_policy); - - // If the VMGS is encrypted, but no key protection data is found, - // assume GspById encryption is enabled, but no ID file was written. - if is_encrypted && !requires_gsp_by_id && !requires_gsp && !found_dek { - requires_gsp_by_id = true; - } - - (response, !no_gsp_available, no_gsp, requires_gsp) - }; - - // Attempt GSP By Id protection if GSP is not available, when changing - // schemes, or as requested - let (gsp_response_by_id, gsp_by_id_available, no_gsp_by_id) = if no_gsp || requires_gsp_by_id { - tracing::info!(CVM_ALLOWED, "attempting GSP By Id"); - - let gsp_response_by_id = get - .guest_state_protection_data_by_id() - .await - .map_err(GetDerivedKeysError::FetchGuestStateProtectionById)?; - - let no_gsp_by_id_available = gsp_response_by_id.extended_status_flags.no_registry_file(); - - let no_gsp_by_id = no_gsp_by_id_available - // disable if auto and pre-existing guest state is unencrypted - // to prevent encryption changes without explicit intent - || (matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::Auto - ) && existing_unencrypted) - // disable per encryption policy (first boot only, unless strict) - || (matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::None - ) && (!requires_gsp_by_id || strict_encryption_policy)); - - if no_gsp_by_id && requires_gsp_by_id { - Err(GetDerivedKeysError::GspByIdRequiredButNotFound)? - } - - ( - gsp_response_by_id, - Some(!no_gsp_by_id_available), - no_gsp_by_id, - ) - } else { - (GuestStateProtectionById::new_zeroed(), None, true) - }; - - // If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key - if (no_kek && found_dek) || (no_gsp && requires_gsp) || (no_gsp_by_id && requires_gsp_by_id) { - // If possible, get ingressKey from hardware sealed data - let (hardware_key_protector, hardware_derived_keys) = if let Some(tee_call) = tee_call { - let hardware_key_protector = match vmgs::read_hardware_key_protector(vmgs).await { - Ok(hardware_key_protector) => Some(hardware_key_protector), - Err(e) => { - // non-fatal - tracing::warn!( - CVM_ALLOWED, - error = &e as &dyn std::error::Error, - "failed to read HW_KEY_PROTECTOR from Vmgs" - ); - None - } - }; - - 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, - ) { - Ok(hardware_derived_key) => Some(hardware_derived_key), - Err(e) => { - // non-fatal - tracing::warn!( - CVM_ALLOWED, - error = &e as &dyn std::error::Error, - "failed to derive hardware keys using HW_KEY_PROTECTOR", - ); - None - } - } - } else { - None - } - }); - - // When the IGVM agent signals skip_hw_unsealing, set both - // hardware_key_protector and hardware_derived_keys to None - // so the code falls through to the scheme-specific error below. - // When hardware sealing keys were actually available, additionally - // emit a warning and a host event that make the skip visible. - if skip_hw_unsealing { - if hardware_key_protector.is_some() && hardware_derived_keys.is_some() { - 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; - - (None, None) - } else { - tracing::info!( - CVM_ALLOWED, - hardware_key_protector = hardware_key_protector.is_some(), - hardware_derived_keys = hardware_derived_keys.is_some(), - "skip_hw_unsealing signaled but hardware key data not available, \ - falling through to scheme-specific error" - ); - (None, None) - } - } else { - (hardware_key_protector, hardware_derived_keys) - } - } else { - (None, None) - }; - - 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)?; - 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; - - tracing::warn!( - CVM_ALLOWED, - "Using hardware-derived key to recover VMGS DEK" - ); - - return Ok(DerivedKeyResult { - derived_keys: Some(derived_keys), - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - }); - } else { - if no_kek && found_dek { - return Err(GetDerivedKeysError::GetIngressKeyFromKpFailed); - } else if no_gsp && requires_gsp { - return Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed); - } else { - // no_gsp_by_id && requires_gsp_by_id - return Err(GetDerivedKeysError::GetIngressKeyFromKGspByIdFailed); - } - } - } - - tracing::info!( - CVM_ALLOWED, - kek = !no_kek, - gsp_available, - gsp = !no_gsp, - gsp_by_id_available = ?gsp_by_id_available, - gsp_by_id = !no_gsp_by_id, - "Encryption sources" - ); - - // Check if sources of encryption are available - if no_kek && no_gsp && no_gsp_by_id { - if is_encrypted { - Err(GetDerivedKeysError::DisableVmgsEncryptionFailed)? - } - match guest_state_encryption_policy { - // fail if some minimum level of encryption was required - GuestStateEncryptionPolicy::GspById - | GuestStateEncryptionPolicy::GspKey - | GuestStateEncryptionPolicy::HardwareSealing => { - Err(GetDerivedKeysError::EncryptionRequiredButNotFound)? - } - GuestStateEncryptionPolicy::Auto | GuestStateEncryptionPolicy::None => { - tracing::info!(CVM_ALLOWED, "No VMGS encryption used."); - - return Ok(DerivedKeyResult { - derived_keys: None, - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - }); - } - } - } - - // 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"); - - derived_keys.ingress = ingress_key; - derived_keys.decrypt_egress = decrypt_egress_key; - derived_keys.encrypt_egress = encrypt_egress_key; - - if let Some(hardware_derived_keys) = hardware_derived_keys { - let hardware_key_protector = HardwareKeyProtector::seal_key( - &hardware_derived_keys, - &derived_keys.encrypt_egress, - ) - .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; - vmgs::write_hardware_key_protector(&hardware_key_protector, vmgs) - .await - .map_err(GetDerivedKeysError::VmgsWriteHardwareKeyProtector)?; - - tracing::info!(CVM_ALLOWED, "hardware key protector updated (no GSP used)"); - } - - return Ok(DerivedKeyResult { - derived_keys: Some(derived_keys), - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - }); - } - - // GSP By Id derives keys differently, - // because key is shared across VMs different context must be used (Id GUID) - if (no_kek && no_gsp) || requires_gsp_by_id { - let derived_keys_by_id = - get_derived_keys_by_id(key_protector_by_id, bios_guid, gsp_response_by_id) - .map_err(GetDerivedKeysError::GetDerivedKeyById)?; - - if no_kek && no_gsp { - if matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::GspById | GuestStateEncryptionPolicy::Auto - ) { - tracing::info!(CVM_ALLOWED, "Using GspById"); - } else { - // Log a warning here to indicate that the VMGS state is out of - // sync with the VM's configuration. - // - // This should only happen if strict encryption policy is - // disabled and one of the following is true: - // - The VM is configured to have no encryption, but it already - // has GspById encryption. - // - The VM is configured to use GspKey, but GspKey is not - // available and GspById is. - tracing::warn!(CVM_ALLOWED, "Allowing GspById"); - }; - - // Not required for Id protection - key_protector_settings.should_write_kp = false; - key_protector_settings.use_gsp_by_id = true; - key_protector_settings.decrypt_gsp_type = GspType::GspById; - key_protector_settings.encrypt_gsp_type = GspType::GspById; - - return Ok(DerivedKeyResult { - derived_keys: Some(derived_keys_by_id), - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - }); - } - - derived_keys.ingress = derived_keys_by_id.ingress; - - tracing::info!( - CVM_ALLOWED, - op_type = ?LogOpType::ConvertEncryptionType, - "Converting GSP method." - ); - } - - let egress_seed; - let mut ingress_seed = None; - - // To get to this point, either KEK or GSP must be available - // Mix tenant key with GSP key to create data store encryption keys - // Covers possible egress combinations: - // GSP, GSP + KEK, GSP By Id + KEK - - if requires_gsp_by_id || no_gsp { - // If DEK exists, ingress is either KEK or KEK + GSP By Id - // If no DEK, then ingress was Gsp By Id (derived above) - if found_dek { - if requires_gsp_by_id { - ingress_seed = Some( - gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize] - .to_vec(), - ); - key_protector_settings.decrypt_gsp_type = GspType::GspById; - } else { - derived_keys.ingress = ingress_key; - } - } else { - key_protector_settings.decrypt_gsp_type = GspType::GspById; - } - - // Choose best available egress seed - if no_gsp { - egress_seed = - gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize].to_vec(); - key_protector_settings.use_gsp_by_id = true; - key_protector_settings.encrypt_gsp_type = GspType::GspById; - } else { - egress_seed = - gsp_response.new_gsp.buffer[..gsp_response.new_gsp.length as usize].to_vec(); - key_protector_settings.encrypt_gsp_type = GspType::GspKey; - } - } else { - // `no_gsp` is false, using `gsp_response` - - if gsp_response.decrypted_gsp[ingress_idx].length == 0 - && gsp_response.decrypted_gsp[egress_idx].length == 0 - { - tracing::info!(CVM_ALLOWED, "Applying GSP."); - - // VMGS has never had any GSP applied. - // Leave ingress key untouched, derive egress key with new seed. - egress_seed = - gsp_response.new_gsp.buffer[..gsp_response.new_gsp.length as usize].to_vec(); - - // Ingress key is either zero or tenant only. - // Only copy in the case where a tenant key was released. - if !no_kek { - derived_keys.ingress = ingress_key; - } - - key_protector_settings.encrypt_gsp_type = GspType::GspKey; - } else { - tracing::info!(CVM_ALLOWED, "Using existing GSP."); - - ingress_seed = Some( - gsp_response.decrypted_gsp[ingress_idx].buffer - [..gsp_response.decrypted_gsp[ingress_idx].length as usize] - .to_vec(), - ); - - if gsp_response.decrypted_gsp[egress_idx].length == 0 { - // Derive ingress with saved seed, derive egress with new seed. - egress_seed = - gsp_response.new_gsp.buffer[..gsp_response.new_gsp.length as usize].to_vec(); - } else { - // System failed during data store unlock, and is in indeterminate state. - // The egress key might have been applied, or the ingress key might be valid. - // Use saved KP, derive ingress/egress keys to attempt recovery. - // Do not update the saved KP with new seed value. - egress_seed = gsp_response.decrypted_gsp[egress_idx].buffer - [..gsp_response.decrypted_gsp[egress_idx].length as usize] - .to_vec(); - key_protector_settings.should_write_kp = false; - decrypt_egress_key = Some(encrypt_egress_key); - } - - key_protector_settings.decrypt_gsp_type = GspType::GspKey; - key_protector_settings.encrypt_gsp_type = GspType::GspKey; - } - } - - // Derive key used to lock data store previously - if let Some(seed) = ingress_seed { - derived_keys.ingress = derive_key(&ingress_key, &seed, VMGS_KEY_DERIVE_LABEL) - .map_err(GetDerivedKeysError::DeriveIngressKey)?; - } - - // Always derive a new egress key using best available seed - derived_keys.decrypt_egress = decrypt_egress_key - .map(|key| derive_key(&key, &egress_seed, VMGS_KEY_DERIVE_LABEL)) - .transpose() - .map_err(GetDerivedKeysError::DeriveEgressKey)?; - - derived_keys.encrypt_egress = - derive_key(&encrypt_egress_key, &egress_seed, VMGS_KEY_DERIVE_LABEL) - .map_err(GetDerivedKeysError::DeriveEgressKey)?; - - if key_protector_settings.should_write_kp { - // Update with all seeds used, but do not write until data store is unlocked - key_protector.gsp[egress_idx] - .gsp_buffer - .copy_from_slice(&gsp_response.encrypted_gsp.buffer); - key_protector.gsp[egress_idx].gsp_length = gsp_response.encrypted_gsp.length; - - if let Some(hardware_derived_keys) = hardware_derived_keys { - let hardware_key_protector = HardwareKeyProtector::seal_key( - &hardware_derived_keys, - &derived_keys.encrypt_egress, - ) - .map_err(GetDerivedKeysError::SealEgressKeyUsingHardwareDerivedKeys)?; - - vmgs::write_hardware_key_protector(&hardware_key_protector, vmgs) - .await - .map_err(GetDerivedKeysError::VmgsWriteHardwareKeyProtector)?; - - tracing::info!(CVM_ALLOWED, "hardware key protector updated"); - } - } - - if matches!( - guest_state_encryption_policy, - GuestStateEncryptionPolicy::GspKey | GuestStateEncryptionPolicy::Auto - ) { - tracing::info!(CVM_ALLOWED, "Using Gsp"); - } else { - // Log a warning here to indicate that the VMGS state is out of - // sync with the VM's configuration. - // - // This should only happen if the VM is configured to have no - // encryption or GspById encryption, but it already has GspKey - // encryption and strict encryption policy is disabled. - tracing::warn!(CVM_ALLOWED, "Allowing Gsp"); - } - - Ok(DerivedKeyResult { - derived_keys: Some(derived_keys), - key_protector_settings, - gsp_extended_status_flags: gsp_response.extended_status_flags, - }) -} - -/// Update data store keys with key protectors based on VmUniqueId & host seed. -fn get_derived_keys_by_id( - key_protector_by_id: &mut KeyProtectorById, - bios_guid: Guid, - gsp_response_by_id: GuestStateProtectionById, -) -> Result { - // This does not handle tenant encrypted VMGS files or Isolated VM, - // or the case where an unlock/relock fails and a snapshot is - // made from that file (the Id cannot change in that failure path). - // When converted to a later scheme, Egress Key will be overwritten. - - // Always derive a new egress key from current VmUniqueId - let new_egress_key = derive_key( - &gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize], - bios_guid.as_bytes(), - VMGS_KEY_DERIVE_LABEL, - ) - .map_err(GetDerivedKeysByIdError::DeriveEgressKeyUsingCurrentVmId)?; - - if new_egress_key.len() != AES_GCM_KEY_LENGTH { - Err(GetDerivedKeysByIdError::InvalidDerivedEgressKeySize { - key_size: new_egress_key.len(), - expected_size: AES_GCM_KEY_LENGTH, - })? - } - - // Ingress values depend on what happened previously to the datastore. - // If not previously encrypted (no saved Id), then Ingress Key not required. - let new_ingress_key = if key_protector_by_id.inner.id_guid != Guid::default() { - // Derive key used to lock data store previously - derive_key( - &gsp_response_by_id.seed.buffer[..gsp_response_by_id.seed.length as usize], - key_protector_by_id.inner.id_guid.as_bytes(), - VMGS_KEY_DERIVE_LABEL, - ) - .map_err(GetDerivedKeysByIdError::DeriveIngressKeyUsingKeyProtectorId)? - } else { - // If data store is not encrypted, Ingress should equal Egress - new_egress_key - }; - - if new_ingress_key.len() != AES_GCM_KEY_LENGTH { - Err(GetDerivedKeysByIdError::InvalidDerivedIngressKeySize { - key_size: new_ingress_key.len(), - expected_size: AES_GCM_KEY_LENGTH, - })? - } - - Ok(Keys { - ingress: new_ingress_key, - decrypt_egress: None, - encrypt_egress: new_egress_key, - }) -} - -/// Prepare the request payload and request GSP from the host via GET. -async fn get_gsp_data( - get: &GuestEmulationTransportClient, - key_protector: &mut KeyProtector, -) -> GuestStateProtection { - use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; - use openhcl_attestation_protocol::vmgs::NUMBER_KP; - - const_assert_eq!(guest_emulation_transport::api::NUMBER_GSP, NUMBER_KP as u32); - const_assert_eq!( - guest_emulation_transport::api::GSP_CIPHERTEXT_MAX, - GSP_BUFFER_SIZE as u32 - ); - - let mut encrypted_gsp = - [guest_emulation_transport::api::GspCiphertextContent::new_zeroed(); NUMBER_KP]; - - for (i, gsp) in encrypted_gsp.iter_mut().enumerate().take(NUMBER_KP) { - if key_protector.gsp[i].gsp_length == 0 { - continue; - } - - gsp.buffer[..key_protector.gsp[i].gsp_length as usize].copy_from_slice( - &key_protector.gsp[i].gsp_buffer[..key_protector.gsp[i].gsp_length as usize], - ); - - gsp.length = key_protector.gsp[i].gsp_length; - } - - get.guest_state_protection_data(encrypted_gsp, GspExtendedStatusFlags::new()) + persist_all_key_protectors(vmgs, key_protector, key_protector_by_id, bios_guid, actions) .await + .map_err(UnlockVmgsDataStoreError::PersistAllKeyProtectors) } /// Update Key Protector to remove 2nd protector, and write to VMGS @@ -1369,17 +709,22 @@ async fn persist_all_key_protectors( key_protector: &mut KeyProtector, key_protector_by_id: &mut KeyProtectorById, bios_guid: Guid, - key_protector_settings: KeyProtectorSettings, + actions: KeyProtectorActions, ) -> Result<(), PersistAllKeyProtectorsError> { use openhcl_attestation_protocol::vmgs::NUMBER_KP; - 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)?; + if actions.use_gsp_by_id && !actions.should_write_kp { + vmgs::write_key_protector_by_id( + key_protector_by_id.ensure_found_mut(), + vmgs, + false, + bios_guid, + ) + .await + .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; } else { // If HW Key unlocked VMGS, do not alter KP - if !key_protector_settings.use_hardware_unlock { + if !actions.use_hardware_unlock { // Remove ingress KP & DEK, no longer applies to data store key_protector.dek[key_protector.active_kp as usize % NUMBER_KP] .dek_buffer @@ -1393,12 +738,12 @@ async fn persist_all_key_protectors( } // Update Id data to indicate this scheme is no longer in use - if !key_protector_settings.use_gsp_by_id - && key_protector_by_id.found_id - && key_protector_by_id.inner.ported == 0 + if !actions.use_gsp_by_id + && let KeyProtectorById::Found(inner) = key_protector_by_id + && inner.ported == 0 { - key_protector_by_id.inner.ported = 1; - vmgs::write_key_protector_by_id(&mut key_protector_by_id.inner, vmgs, true, bios_guid) + inner.ported = 1; + vmgs::write_key_protector_by_id(inner, vmgs, true, bios_guid) .await .map_err(PersistAllKeyProtectorsError::WriteKeyProtectorById)?; } @@ -1406,1386 +751,3 @@ async fn persist_all_key_protectors( Ok(()) } - -/// Module that implements the mock [`TeeCall`] for testing purposes -#[cfg(test)] -pub mod test_utils { - use tee_call::GetAttestationReportResult; - use tee_call::HW_DERIVED_KEY_LENGTH; - use tee_call::REPORT_DATA_SIZE; - use tee_call::TeeCall; - use tee_call::TeeCallGetDerivedKey; - use tee_call::TeeType; - - /// Mock implementation of [`TeeCall`] with get derived key support for testing purposes - pub struct MockTeeCall { - /// Mock TCB version to return from get_attestation_report - pub tcb_version: u64, - } - - impl MockTeeCall { - /// Create a new instance of [`MockTeeCall`]. - pub fn new(tcb_version: u64) -> Self { - Self { tcb_version } - } - } - - impl TeeCall for MockTeeCall { - fn get_attestation_report( - &self, - report_data: &[u8; REPORT_DATA_SIZE], - ) -> Result { - let mut report = - [0x6c; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE]; - report[..REPORT_DATA_SIZE].copy_from_slice(report_data); - - Ok(GetAttestationReportResult { - report: report.to_vec(), - tcb_version: Some(self.tcb_version), - }) - } - - fn supports_get_derived_key(&self) -> Option<&dyn TeeCallGetDerivedKey> { - Some(self) - } - - fn tee_type(&self) -> TeeType { - // Use Snp for testing - TeeType::Snp - } - } - - impl TeeCallGetDerivedKey for MockTeeCall { - fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; 32], tee_call::Error> { - // Base test key; mix in policy so different policies yield different derived secrets - let mut key: [u8; HW_DERIVED_KEY_LENGTH] = [0xab; HW_DERIVED_KEY_LENGTH]; - - // Use mutation to simulate the policy - let tcb = tcb_version.to_le_bytes(); - for (i, b) in key.iter_mut().enumerate() { - *b ^= tcb[i % tcb.len()]; - } - - Ok(key) - } - } - - /// Mock implementation of [`TeeCall`] without get derived key support for testing purposes - pub struct MockTeeCallNoGetDerivedKey; - - impl TeeCall for MockTeeCallNoGetDerivedKey { - fn get_attestation_report( - &self, - report_data: &[u8; REPORT_DATA_SIZE], - ) -> Result { - let mut report = - [0x6c; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE]; - report[..REPORT_DATA_SIZE].copy_from_slice(report_data); - - Ok(GetAttestationReportResult { - report: report.to_vec(), - tcb_version: None, - }) - } - - fn supports_get_derived_key(&self) -> Option<&dyn TeeCallGetDerivedKey> { - None - } - - fn tee_type(&self) -> TeeType { - // Use Snp for testing - TeeType::Snp - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::MockTeeCallNoGetDerivedKey; - use disk_backend::Disk; - use disklayer_ram::ram_disk; - use get_protocol::GSP_CLEARTEXT_MAX; - use get_protocol::GspExtendedStatusFlags; - use guest_emulation_device::IgvmAgentAction; - use guest_emulation_device::IgvmAgentTestPlan; - use guest_emulation_transport::test_utilities::TestGet; - use key_protector::AES_WRAPPED_AES_KEY_LENGTH; - use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestType; - use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE; - use openhcl_attestation_protocol::vmgs::DekKp; - use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; - use openhcl_attestation_protocol::vmgs::GspKp; - use openhcl_attestation_protocol::vmgs::NUMBER_KP; - use pal_async::DefaultDriver; - use pal_async::async_test; - use pal_async::task::Spawn; - use std::collections::VecDeque; - use test_utils::MockTeeCall; - use test_with_tracing::test; - use vmgs_format::EncryptionAlgorithm; - use vmgs_format::FileId; - - const ONE_MEGA_BYTE: u64 = 1024 * 1024; - - fn new_test_file() -> Disk { - ram_disk(4 * ONE_MEGA_BYTE, false).unwrap() - } - - async fn new_formatted_vmgs() -> Vmgs { - let disk = new_test_file(); - - let mut vmgs = Vmgs::format_new(disk, None).await.unwrap(); - - assert!( - key_protector_is_empty(&mut vmgs).await, - "Newly formatted VMGS should have an empty key protector" - ); - assert!( - key_protector_by_id_is_empty(&mut vmgs).await, - "Newly formatted VMGS should have an empty key protector by id" - ); - - vmgs - } - - async fn key_protector_is_empty(vmgs: &mut Vmgs) -> bool { - let key_protector = vmgs::read_key_protector(vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - - key_protector.as_bytes().iter().all(|&b| b == 0) - } - - async fn key_protector_by_id_is_empty(vmgs: &mut Vmgs) -> bool { - vmgs::read_key_protector_by_id(vmgs) - .await - .is_err_and(|err| { - matches!( - err, - vmgs::ReadFromVmgsError::EntryNotFound(FileId::VM_UNIQUE_ID) - ) - }) - } - - async fn hardware_key_protector_is_empty(vmgs: &mut Vmgs) -> bool { - vmgs::read_hardware_key_protector(vmgs) - .await - .is_err_and(|err| { - matches!( - err, - vmgs::ReadFromVmgsError::EntryNotFound(FileId::HW_KEY_PROTECTOR) - ) - }) - } - - fn new_key_protector() -> KeyProtector { - // Ingress and egress KPs are assumed to be the only two KPs, therefore `NUMBER_KP` should be 2 - assert_eq!(NUMBER_KP, 2); - - let ingress_dek = DekKp { - dek_buffer: [1; DEK_BUFFER_SIZE], - }; - let egress_dek = DekKp { - dek_buffer: [2; DEK_BUFFER_SIZE], - }; - let ingress_gsp = GspKp { - gsp_length: GSP_BUFFER_SIZE as u32, - gsp_buffer: [3; GSP_BUFFER_SIZE], - }; - let egress_gsp = GspKp { - gsp_length: GSP_BUFFER_SIZE as u32, - gsp_buffer: [4; GSP_BUFFER_SIZE], - }; - KeyProtector { - dek: [ingress_dek, egress_dek], - gsp: [ingress_gsp, egress_gsp], - active_kp: 0, - } - } - - fn new_key_protector_by_id( - id_guid: Option, - ported: Option, - found_id: bool, - ) -> KeyProtectorById { - let key_protector_by_id = openhcl_attestation_protocol::vmgs::KeyProtectorById { - id_guid: id_guid.unwrap_or_else(Guid::new_random), - ported: ported.unwrap_or(0), - pad: [0; 3], - }; - - KeyProtectorById { - inner: key_protector_by_id, - found_id, - } - } - - async fn new_test_get( - spawn: impl Spawn, - enable_igvm_attest: bool, - plan: Option, - ) -> TestGet { - if enable_igvm_attest { - const TEST_DEVICE_MEMORY_SIZE: u64 = 64; - // Use `DeviceTestMemory` to set up shared memory required by the IGVM_ATTEST GET calls. - let dev_test_mem = user_driver_emulated_mock::DeviceTestMemory::new( - TEST_DEVICE_MEMORY_SIZE, - true, - "test-attest", - ); - - let mut test_get = guest_emulation_transport::test_utilities::new_transport_pair( - spawn, - None, - get_protocol::ProtocolVersion::NICKEL_REV2, - Some(dev_test_mem.guest_memory()), - plan, - ) - .await; - - test_get.client.set_gpa_allocator(dev_test_mem.dma_client()); - - test_get - } else { - guest_emulation_transport::test_utilities::new_transport_pair( - spawn, - None, - get_protocol::ProtocolVersion::NICKEL_REV2, - None, - None, - ) - .await - } - } - - fn new_attestation_vm_config() -> AttestationVmConfig { - AttestationVmConfig { - current_time: None, - root_cert_thumbprint: String::new(), - console_enabled: false, - interactive_console_enabled: false, - secure_boot: false, - tpm_enabled: true, - tpm_persisted: true, - filtered_vpci_devices_allowed: false, - vm_unique_id: String::new(), - } - } - - #[async_test] - async fn do_nothing_without_derived_keys() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: false, - use_gsp_by_id: false, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::None, - encrypt_gsp_type: GspType::None, - }; - - let bios_guid = Guid::new_random(); - - unlock_vmgs_data_store( - &mut vmgs, - false, - &mut key_protector, - &mut key_protector_by_id, - None, - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(key_protector_by_id_is_empty(&mut vmgs).await); - - // Create another instance as the previous `unlock_vmgs_data_store` took ownership of the last one - let key_protector_settings = KeyProtectorSettings { - should_write_kp: false, - use_gsp_by_id: false, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::None, - encrypt_gsp_type: GspType::None, - }; - - // Even if the VMGS is encrypted, if no derived keys are provided, nothing should happen - unlock_vmgs_data_store( - &mut vmgs, - true, - &mut key_protector, - &mut key_protector_by_id, - None, - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(key_protector_by_id_is_empty(&mut vmgs).await); - } - - #[async_test] - async fn provision_vmgs_and_rotate_keys() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let ingress = [1; AES_GCM_KEY_LENGTH]; - let egress = [2; AES_GCM_KEY_LENGTH]; - let derived_keys = Keys { - ingress, - decrypt_egress: None, - encrypt_egress: egress, - }; - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - let bios_guid = Guid::new_random(); - - // Without encryption implies the provision path - // The VMGS will be locked using the egress key - unlock_vmgs_data_store( - &mut vmgs, - false, - &mut key_protector, - &mut key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - // The ingress key is essentially ignored since the VMGS wasn't previously encrypted - vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); - - // The egress key was used to lock the VMGS after provisioning - vmgs.unlock_with_encryption_key(&egress).await.unwrap(); - // Since this is a new VMGS, the egress key is the first and only key - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); - - // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated - assert!(!key_protector_is_empty(&mut vmgs).await); - assert!(!key_protector_by_id_is_empty(&mut vmgs).await); - - let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); - - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!( - found_key_protector_by_id.as_bytes(), - key_protector_by_id.inner.as_bytes() - ); - - // Now that the VMGS has been provisioned, simulate the rotation of keys - let new_egress = [3; AES_GCM_KEY_LENGTH]; - - let mut new_key_protector = new_key_protector(); - let mut new_key_protector_by_id = new_key_protector_by_id(None, None, false); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - // Ingress is now the old egress, and we provide a new new egress key - let derived_keys = Keys { - ingress: egress, - decrypt_egress: None, - encrypt_egress: new_egress, - }; - - unlock_vmgs_data_store( - &mut vmgs, - true, - &mut new_key_protector, - &mut new_key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - // We should still fail to unlock the VMGS with the original ingress key - vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); - // The old egress key should no longer be able to unlock the VMGS - vmgs.unlock_with_encryption_key(&egress).await.unwrap_err(); - - // The new egress key should be able to unlock the VMGS - vmgs.unlock_with_encryption_key(&new_egress).await.unwrap(); - // The old egress key was removed, but not before the new egress key was added in the 1th slot - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); - - let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - assert_eq!(found_key_protector.as_bytes(), new_key_protector.as_bytes()); - - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!( - found_key_protector_by_id.as_bytes(), - new_key_protector_by_id.inner.as_bytes() - ); - } - - #[async_test] - async fn unlock_previously_encrypted_vmgs_with_ingress_key() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let ingress = [1; AES_GCM_KEY_LENGTH]; - let egress = [2; AES_GCM_KEY_LENGTH]; - - let derived_keys = Keys { - ingress, - decrypt_egress: None, - encrypt_egress: egress, - }; - - vmgs.update_encryption_key(&ingress, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - - // Initially, the VMGS can be unlocked using the ingress key - vmgs.unlock_with_encryption_key(&ingress).await.unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - let bios_guid = Guid::new_random(); - - unlock_vmgs_data_store( - &mut vmgs, - true, - &mut key_protector, - &mut key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - // After the VMGS has been unlocked, the VMGS encryption key should be rotated from ingress to egress - vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); - vmgs.unlock_with_encryption_key(&egress).await.unwrap(); - // The ingress key was removed, but not before the egress key was added in the 0th slot - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); - - // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated - let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); - - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!( - found_key_protector_by_id.as_bytes(), - key_protector_by_id.inner.as_bytes() - ); - } - - #[async_test] - async fn failed_to_persist_ingress_key_so_use_egress_key_to_unlock_vmgs() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let ingress = [1; AES_GCM_KEY_LENGTH]; - let decrypt_egress = [2; AES_GCM_KEY_LENGTH]; - let encrypt_egress = [3; AES_GCM_KEY_LENGTH]; - - let derived_keys = Keys { - ingress, - decrypt_egress: Some(decrypt_egress), - encrypt_egress, - }; - - // Add only the egress key to the VMGS to simulate a failure to persist the ingress key - vmgs.test_add_new_encryption_key(&decrypt_egress, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - let egress_key_index = vmgs.test_get_active_datastore_key_index().unwrap(); - assert_eq!(egress_key_index, 0); - - vmgs.unlock_with_encryption_key(&decrypt_egress) - .await - .unwrap(); - let found_egress_key_index = vmgs.test_get_active_datastore_key_index().unwrap(); - assert_eq!(found_egress_key_index, egress_key_index); - - // Confirm that the ingress key cannot be used to unlock the VMGS - vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - let bios_guid = Guid::new_random(); - - unlock_vmgs_data_store( - &mut vmgs, - true, - &mut key_protector, - &mut key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await - .unwrap(); - - // Confirm that the ingress key was not added - vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); - - // Confirm that the decrypt egress key no longer works - vmgs.unlock_with_encryption_key(&decrypt_egress) - .await - .unwrap_err(); - - // The encrypt_egress key can unlock the VMGS and was added as a new key - vmgs.unlock_with_encryption_key(&encrypt_egress) - .await - .unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); - - // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated - let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); - - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!( - found_key_protector_by_id.as_bytes(), - key_protector_by_id.inner.as_bytes() - ); - } - - #[async_test] - async fn fail_to_unlock_vmgs_with_existing_ingress_key() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let ingress = [1; AES_GCM_KEY_LENGTH]; - - // Ingress and egress keys are the same - let derived_keys = Keys { - ingress, - decrypt_egress: None, - encrypt_egress: ingress, - }; - - // Add two random keys to the VMGS to simulate unlock failure when ingress and egress keys are the same - let additional_key = [2; AES_GCM_KEY_LENGTH]; - let yet_another_key = [3; AES_GCM_KEY_LENGTH]; - - vmgs.test_add_new_encryption_key(&additional_key, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); - - vmgs.test_add_new_encryption_key(&yet_another_key, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - let bios_guid = Guid::new_random(); - - let unlock_result = unlock_vmgs_data_store( - &mut vmgs, - true, - &mut key_protector, - &mut key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await; - assert!(unlock_result.is_err()); - assert_eq!( - unlock_result.unwrap_err().to_string(), - "failed to unlock vmgs with the existing ingress key".to_string() - ); - } - - #[async_test] - async fn fail_to_unlock_vmgs_with_new_ingress_key() { - let mut vmgs = new_formatted_vmgs().await; - - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - - let derived_keys = Keys { - ingress: [1; AES_GCM_KEY_LENGTH], - decrypt_egress: None, - encrypt_egress: [2; AES_GCM_KEY_LENGTH], - }; - - // Add two random keys to the VMGS to simulate unlock failure when ingress and egress keys are *not* the same - let additional_key = [3; AES_GCM_KEY_LENGTH]; - let yet_another_key = [4; AES_GCM_KEY_LENGTH]; - - vmgs.test_add_new_encryption_key(&additional_key, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); - - vmgs.test_add_new_encryption_key(&yet_another_key, EncryptionAlgorithm::AES_GCM) - .await - .unwrap(); - assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); - - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - - let bios_guid = Guid::new_random(); - - let unlock_result = unlock_vmgs_data_store( - &mut vmgs, - true, - &mut key_protector, - &mut key_protector_by_id, - Some(derived_keys), - key_protector_settings, - bios_guid, - ) - .await; - assert!(unlock_result.is_err()); - assert_eq!( - unlock_result.unwrap_err().to_string(), - "failed to unlock vmgs with the existing ingress key".to_string() - ); - } - - #[async_test] - async fn get_derived_keys_using_id() { - let bios_guid = Guid::new_random(); - - let gsp_response_by_id = GuestStateProtectionById { - seed: guest_emulation_transport::api::GspCleartextContent { - length: GSP_CLEARTEXT_MAX, - buffer: [1; GSP_CLEARTEXT_MAX as usize * 2], - }, - extended_status_flags: GspExtendedStatusFlags::from_bits(0), - }; - - // When the key protector by id inner `id_guid` is all zeroes, the derived ingress and egress keys - // should be identical. - let mut key_protector_by_id = - new_key_protector_by_id(Some(Guid::new_zeroed()), None, false); - let derived_keys = - get_derived_keys_by_id(&mut key_protector_by_id, bios_guid, gsp_response_by_id) - .unwrap(); - - assert_eq!(derived_keys.ingress, derived_keys.encrypt_egress); - - // When the key protector by id inner `id_guid` is not all zeroes, the derived ingress and egress keys - // should be different. - let mut key_protector_by_id = new_key_protector_by_id(None, None, false); - let derived_keys = - get_derived_keys_by_id(&mut key_protector_by_id, bios_guid, gsp_response_by_id) - .unwrap(); - - assert_ne!(derived_keys.ingress, derived_keys.encrypt_egress); - - // When the `gsp_response_by_id` seed length is 0, deriving a key will fail. - let gsp_response_by_id_with_0_length_seed = GuestStateProtectionById { - seed: guest_emulation_transport::api::GspCleartextContent { - length: 0, - buffer: [1; GSP_CLEARTEXT_MAX as usize * 2], - }, - extended_status_flags: GspExtendedStatusFlags::from_bits(0), - }; - - let derived_keys_response = get_derived_keys_by_id( - &mut key_protector_by_id, - bios_guid, - gsp_response_by_id_with_0_length_seed, - ); - assert!(derived_keys_response.is_err()); - assert_eq!( - derived_keys_response.unwrap_err().to_string(), - "failed to derive an egress key based on current vm bios guid".to_string() - ); - } - - #[async_test] - async fn pass_through_persist_all_key_protectors() { - let mut vmgs = new_formatted_vmgs().await; - 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(); - - // Copied/cloned bits used for comparison later - let kp_copy = key_protector.as_bytes().to_vec(); - let active_kp_copy = key_protector.active_kp; - - // When all key protector settings are true, no actions will be taken on the key protectors or VMGS - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: true, - use_hardware_unlock: true, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - persist_all_key_protectors( - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - key_protector_settings, - ) - .await - .unwrap(); - - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(key_protector_by_id_is_empty(&mut vmgs).await); - - // The key protector should remain unchanged - assert_eq!(active_kp_copy, key_protector.active_kp); - assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); - } - - #[async_test] - async fn persist_all_key_protectors_write_key_protector_by_id() { - let mut vmgs = new_formatted_vmgs().await; - 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(); - - // Copied/cloned bits used for comparison later - let kp_copy = key_protector.as_bytes().to_vec(); - let active_kp_copy = key_protector.active_kp; - - // When `use_gsp_by_id` is true and `should_write_kp` is false, the key protector by id should be written to the VMGS - let key_protector_settings = KeyProtectorSettings { - should_write_kp: false, - use_gsp_by_id: true, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::GspById, - encrypt_gsp_type: GspType::GspById, - }; - persist_all_key_protectors( - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - key_protector_settings, - ) - .await - .unwrap(); - - // The previously empty VMGS now holds the key protector by id but not the key protector - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(!key_protector_by_id_is_empty(&mut vmgs).await); - - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!( - found_key_protector_by_id.as_bytes(), - key_protector_by_id.inner.as_bytes() - ); - - // The key protector should remain unchanged - assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); - assert_eq!(active_kp_copy, key_protector.active_kp); - } - - #[async_test] - async fn persist_all_key_protectors_remove_ingress_kp() { - let mut vmgs = new_formatted_vmgs().await; - 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(); - - // Copied active KP for later use - let active_kp_copy = key_protector.active_kp; - - // When `use_gsp_by_id` is false, `should_write_kp` is true, and `use_hardware_unlock` is false, the active key protector's - // active kp's dek should be zeroed, the active kp's gsp length should be set to 0, and the active kp should be incremented - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: false, - use_hardware_unlock: false, - decrypt_gsp_type: GspType::None, - encrypt_gsp_type: GspType::None, - }; - persist_all_key_protectors( - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - key_protector_settings, - ) - .await - .unwrap(); - - assert!(!key_protector_is_empty(&mut vmgs).await); - assert!(key_protector_by_id_is_empty(&mut vmgs).await); - - // The previously empty VMGS's key protector should now be overwritten - let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) - .await - .unwrap(); - - assert!( - found_key_protector.dek[active_kp_copy as usize] - .dek_buffer - .iter() - .all(|&b| b == 0), - ); - assert_eq!( - found_key_protector.gsp[active_kp_copy as usize].gsp_length, - 0 - ); - assert_eq!(found_key_protector.active_kp, active_kp_copy + 1); - } - - #[async_test] - async fn persist_all_key_protectors_mark_key_protector_by_id_as_not_in_use() { - let mut vmgs = new_formatted_vmgs().await; - let mut key_protector = new_key_protector(); - let mut key_protector_by_id = new_key_protector_by_id(None, None, true); - let bios_guid = Guid::new_random(); - - // When `use_gsp_by_id` is false, `should_write_kp` is true, `use_hardware_unlock` is true, and - // the key protector by id is found and not ported, the key protector by id should be marked as ported - let key_protector_settings = KeyProtectorSettings { - should_write_kp: true, - use_gsp_by_id: false, - use_hardware_unlock: true, - decrypt_gsp_type: GspType::None, - encrypt_gsp_type: GspType::None, - }; - - persist_all_key_protectors( - &mut vmgs, - &mut key_protector, - &mut key_protector_by_id, - bios_guid, - key_protector_settings, - ) - .await - .unwrap(); - - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(!key_protector_by_id_is_empty(&mut vmgs).await); - - // The previously empty VMGS's key protector by id should now be overwritten - let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); - assert_eq!(found_key_protector_by_id.ported, 1); - assert_eq!( - found_key_protector_by_id.id_guid, - key_protector_by_id.inner.id_guid - ); - } - - // --- initialize_platform_security tests --- - - #[async_test] - async fn init_sec_suppress_attestation(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // Write non-zero agent data to VMGS so we can verify it is returned. - let agent = SecurityProfile { - agent_data: [0xAA; AGENT_DATA_MAX_SIZE], - }; - vmgs.write_file(FileId::ATTEST, agent.as_bytes()) - .await - .unwrap(); - - // Ensure no IGVM attest call out - let get_pair = new_test_get(driver, false, None).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - None, // no TEE when suppressed - true, // suppress_attestation - ldriver, - GuestStateEncryptionPolicy::None, - true, - ) - .await - .unwrap(); - - // VMGS remains unencrypted and KP/HWKP not written. - assert!(!vmgs.encrypted()); - assert!(key_protector_is_empty(&mut vmgs).await); - assert!(hardware_key_protector_is_empty(&mut vmgs).await); - // Agent data passed through - assert_eq!(res.agent_data.unwrap(), agent.agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - } - - #[async_test] - async fn init_sec_secure_key_release_with_wrapped_key_request(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // IGVM attest is required - let get_pair = new_test_get(driver, true, None).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new(0x1234); - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver.clone(), - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS is now encrypted and HWKP is updated. - assert!(vmgs.encrypted()); - assert!(!hardware_key_protector_is_empty(&mut vmgs).await); - - // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. - // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. - let key_reference = serde_json::json!({ - "key_info": { - "host": "name" - }, - "attestation_info": { - "host": "attestation_name" - } - }); - let key_reference = serde_json::to_string(&key_reference).unwrap(); - let key_reference = key_reference.as_bytes(); - let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; - expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); - assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - - // Second call: VMGS unlock via SKR should succeed - initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver, - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS should remain encrypted - assert!(vmgs.encrypted()); - } - - #[async_test] - async fn init_sec_secure_key_release_without_wrapped_key_request(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // Write non-zero agent data to workaround the WRAPPED_KEY_REQUEST requirement. - let agent = SecurityProfile { - agent_data: [0xAA; AGENT_DATA_MAX_SIZE], - }; - vmgs.write_file(FileId::ATTEST, agent.as_bytes()) - .await - .unwrap(); - - // Skip WRAPPED_KEY_REQUEST for both boots - let mut plan = IgvmAgentTestPlan::default(); - plan.insert( - IgvmAttestRequestType::WRAPPED_KEY_REQUEST, - VecDeque::from([IgvmAgentAction::NoResponse, IgvmAgentAction::NoResponse]), - ); - - // IGVM attest is required - let get_pair = new_test_get(driver, true, Some(plan)).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - let tee = MockTeeCall::new(0x1234); - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver.clone(), - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS is now encrypted and HWKP is updated. - assert!(vmgs.encrypted()); - assert!(!hardware_key_protector_is_empty(&mut vmgs).await); - // Agent data passed through - assert_eq!(res.agent_data.clone().unwrap(), agent.agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - - // Second call: VMGS unlock via SKR should succeed - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver, - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS should remain encrypted - assert!(vmgs.encrypted()); - // Agent data passed through - assert_eq!(res.agent_data.clone().unwrap(), agent.agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - } - - #[async_test] - async fn init_sec_secure_key_release_hw_sealing_backup(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // IGVM attest is required - let mut plan = IgvmAgentTestPlan::default(); - plan.insert( - IgvmAttestRequestType::WRAPPED_KEY_REQUEST, - VecDeque::from([ - IgvmAgentAction::RespondSuccess, - // initialize_platform_security will attempt SKR/unlock 10 times - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - ]), - ); - - let get_pair = new_test_get(driver, true, Some(plan)).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new(0x1234); - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver.clone(), - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS is now encrypted and HWKP is updated. - assert!(vmgs.encrypted()); - assert!(!hardware_key_protector_is_empty(&mut vmgs).await); - // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. - // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. - let key_reference = serde_json::json!({ - "key_info": { - "host": "name" - }, - "attestation_info": { - "host": "attestation_name" - } - }); - let key_reference = serde_json::to_string(&key_reference).unwrap(); - let key_reference = key_reference.as_bytes(); - let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; - expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); - assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - - // Second call: VMGS unlock via key recovered with hardware sealing - // NOTE: The test relies on the test GED to return failing WRAPPED_KEY response - // with retry recommendation as false to skip the retry loop in - // secure_key_release::request_vmgs_encryption_keys. Otherwise, the test will stuck - // on the timer.sleep() as the the driver is not progressed. - initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver, - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS should remain encrypted - assert!(vmgs.encrypted()); - } - - #[async_test] - async fn init_sec_secure_key_release_skip_hw_unsealing(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // IGVM attest is required - // KEY_RELEASE succeeds on first boot, fails with skip_hw_unsealing on second boot. - // WRAPPED_KEY is not in the plan, so it falls back to default (success) every time. - let mut plan = IgvmAgentTestPlan::default(); - plan.insert( - IgvmAttestRequestType::KEY_RELEASE_REQUEST, - VecDeque::from([ - IgvmAgentAction::RespondSuccess, - IgvmAgentAction::RespondFailureSkipHwUnsealing, - ]), - ); - - let get_pair = new_test_get(driver, true, Some(plan)).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let tee = MockTeeCall::new(0x1234); - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver.clone(), - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS is now encrypted and HWKP is updated. - assert!(vmgs.encrypted()); - assert!(!hardware_key_protector_is_empty(&mut vmgs).await); - // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. - let key_reference = serde_json::json!({ - "key_info": { - "host": "name" - }, - "attestation_info": { - "host": "attestation_name" - } - }); - let key_reference = serde_json::to_string(&key_reference).unwrap(); - let key_reference = key_reference.as_bytes(); - let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; - expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); - assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - - // Second call: KEY_RELEASE fails with skip_hw_unsealing signal. - // The skip_hw_unsealing signal causes the hardware unsealing fallback to be - // skipped, so VMGS unlock should fail. - // NOTE: The test relies on the test GED to return failing KEY_RELEASE response - // with retry recommendation as false so the retry loop terminates immediately. - // Otherwise, the test will get stuck on timer.sleep() as the driver is not - // progressed. - let result = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver, - GuestStateEncryptionPolicy::Auto, - true, - ) - .await; - - assert!(result.is_err()); - } - - #[async_test] - async fn init_sec_secure_key_release_no_hw_sealing_backup(driver: DefaultDriver) { - let mut vmgs = new_formatted_vmgs().await; - - // IGVM attest is required - let mut plan = IgvmAgentTestPlan::default(); - plan.insert( - IgvmAttestRequestType::WRAPPED_KEY_REQUEST, - VecDeque::from([ - IgvmAgentAction::RespondSuccess, - // initialize_platform_security will attempt SKR/unlock 10 times - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - IgvmAgentAction::RespondFailure, - ]), - ); - - let get_pair = new_test_get(driver, true, Some(plan)).await; - - let bios_guid = Guid::new_random(); - let att_cfg = new_attestation_vm_config(); - // Without hardware sealing support - let tee = MockTeeCallNoGetDerivedKey {}; - - // Ensure VMGS is not encrypted and agent data is empty before the call - assert!(!vmgs.encrypted()); - - // Obtain a LocalDriver briefly, then run the async flow under the pool executor - let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); - let res = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver.clone(), - GuestStateEncryptionPolicy::Auto, - true, - ) - .await - .unwrap(); - - // VMGS is now encrypted but HWKP remains empty. - assert!(vmgs.encrypted()); - assert!(hardware_key_protector_is_empty(&mut vmgs).await); - // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. - // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. - let key_reference = serde_json::json!({ - "key_info": { - "host": "name" - }, - "attestation_info": { - "host": "attestation_name" - } - }); - let key_reference = serde_json::to_string(&key_reference).unwrap(); - let key_reference = key_reference.as_bytes(); - let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; - expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); - assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); - // Secure key should be None without pre-provisioning - assert!(res.guest_secret_key.is_none()); - - // Second call: VMGS unlock should fail without hardware sealing support - let result = initialize_platform_security( - &get_pair.client, - bios_guid, - &att_cfg, - &mut vmgs, - Some(&tee), - false, - ldriver, - GuestStateEncryptionPolicy::Auto, - true, - ) - .await; - - assert!(result.is_err()); - } -} diff --git a/openhcl/underhill_attestation/src/secure_key_release.rs b/openhcl/underhill_attestation/src/secure_key_release.rs index f639499b8b..c0709c3da5 100644 --- a/openhcl/underhill_attestation/src/secure_key_release.rs +++ b/openhcl/underhill_attestation/src/secure_key_release.rs @@ -5,6 +5,7 @@ //! encryption keys. use crate::IgvmAttestRequestHelper; +use crate::Retryable; use crate::igvm_attest; use crypto::rsa::RsaKeyPair; use cvm_tracing::CVM_ALLOWED; @@ -59,8 +60,13 @@ pub(crate) enum RequestVmgsEncryptionKeysError { #[derive(Debug, thiserror::Error)] pub(crate) enum Pkcs11RsaAesKeyUnwrapError { - #[error("expected wrapped AES key blob to be {0} bytes, but found {1} bytes")] - UndersizedWrappedAesKey(usize, usize), + #[error( + "expected wrapped AES key blob to be {expected_size} bytes, but found {actual_size} bytes" + )] + UndersizedWrappedAesKey { + expected_size: usize, + actual_size: usize, + }, #[error("wrapped RSA key blob cannot be empty")] EmptyWrappedRsaKey, #[error("RSA unwrap failed")] @@ -82,10 +88,10 @@ fn pkcs11_rsa_aes_key_unwrap( let (wrapped_aes_key, wrapped_rsa_key) = wrapped_key_blob .split_at_checked(modulus_size) - .ok_or(Pkcs11RsaAesKeyUnwrapError::UndersizedWrappedAesKey( - modulus_size, - wrapped_key_blob.len(), - ))?; + .ok_or(Pkcs11RsaAesKeyUnwrapError::UndersizedWrappedAesKey { + expected_size: modulus_size, + actual_size: wrapped_key_blob.len(), + })?; if wrapped_rsa_key.is_empty() { return Err(Pkcs11RsaAesKeyUnwrapError::EmptyWrappedRsaKey); @@ -130,16 +136,12 @@ pub async fn request_vmgs_encryption_keys( vmgs: &Vmgs, attestation_vm_config: &AttestationVmConfig, agent_data: &mut [u8; AGENT_DATA_MAX_SIZE], -) -> Result { +) -> Result> { const TRANSFER_RSA_KEY_BITS: u32 = 2048; // Generate an ephemeral transfer key - let transfer_key = RsaKeyPair::generate(TRANSFER_RSA_KEY_BITS).map_err(|e| { - ( - RequestVmgsEncryptionKeysError::GenerateTransferKey(e), - false, - ) - })?; + let transfer_key = RsaKeyPair::generate(TRANSFER_RSA_KEY_BITS) + .map_err(|e| Retryable::fatal(RequestVmgsEncryptionKeysError::GenerateTransferKey(e)))?; let exponent = transfer_key.public_exponent(); let modulus = transfer_key.modulus(); @@ -160,12 +162,7 @@ pub async fn request_vmgs_encryption_keys( // Get attestation report each time this function is called. Failures here are fatal. let result = tee_call .get_attestation_report(igvm_attest_request_helper.get_runtime_claims_hash()) - .map_err(|e| { - ( - RequestVmgsEncryptionKeysError::GetAttestationReport(e), - false, - ) - })?; + .map_err(|e| Retryable::fatal(RequestVmgsEncryptionKeysError::GetAttestationReport(e)))?; // Get tenant keys based on attestation results, this might fail. match make_igvm_attest_requests( @@ -183,7 +180,9 @@ pub async fn request_vmgs_encryption_keys( wrapped_des_key, }) => { let ingress_rsa_kek = pkcs11_rsa_aes_key_unwrap(&transfer_key, &rsa_aes_wrapped_key) - .map_err(|e| (RequestVmgsEncryptionKeysError::Pkcs11RsaAesKeyUnwrap(e), false))?; + .map_err(|e| { + Retryable::fatal(RequestVmgsEncryptionKeysError::Pkcs11RsaAesKeyUnwrap(e)) + })?; Ok(VmgsEncryptionKeys { ingress_rsa_kek: Some(ingress_rsa_kek), @@ -211,7 +210,7 @@ pub async fn request_vmgs_encryption_keys( error = &wrapped_key_attest_error as &dyn std::error::Error, "VMGS key-encryption failed due to igvm attest error" ); - Err((wrapped_key_attest_error, retry_signal)) + Err(Retryable::with_retry(wrapped_key_attest_error, retry_signal)) } Err( key_release_attest_error @ RequestVmgsEncryptionKeysError::ParseIgvmAttestKeyReleaseResponse( @@ -234,7 +233,7 @@ pub async fn request_vmgs_encryption_keys( error = &key_release_attest_error as &dyn std::error::Error, "VMGS key-encryption failed due to igvm attest error" ); - Err((key_release_attest_error, retry_signal)) + Err(Retryable::with_retry(key_release_attest_error, retry_signal)) } Err(e) => { tracing::error!( @@ -242,11 +241,20 @@ pub async fn request_vmgs_encryption_keys( error = &e as &dyn std::error::Error, "VMGS key-encryption key request failed due to error", ); - Err((e, true)) + Err(Retryable::with_retry(e, true)) } } } +/// Notify the host that the WRAPPED_KEY response was required for diagnosis but +/// could not be obtained or parsed. No-op if `required` is false. +async fn notify_wrapped_key_required(get: &GuestEmulationTransportClient, required: bool) { + if required { + get.event_log_fatal(EventLogId::WRAPPED_KEY_REQUIRED_BUT_INVALID) + .await; + } +} + /// Make the `IGVM_ATTEST` request to GET. async fn make_igvm_attest_requests( get: &GuestEmulationTransportClient, @@ -261,9 +269,12 @@ async fn make_igvm_attest_requests( let wrapped_key_required = vmgs_encrypted && agent_data.iter().all(|&x| x == 0); // Attempt to get wrapped DiskEncryptionSettings key - igvm_attest_request_helper.set_request_type(IgvmAttestRequestType::WRAPPED_KEY_REQUEST); let request = igvm_attest_request_helper - .create_request(IGVM_ATTEST_REQUEST_CURRENT_VERSION, attestation_report) + .create_request( + IGVM_ATTEST_REQUEST_CURRENT_VERSION, + IgvmAttestRequestType::WRAPPED_KEY_REQUEST, + attestation_report, + ) .map_err(RequestVmgsEncryptionKeysError::CreateIgvmAttestWrappedKeyRequest)?; let response = match get @@ -272,11 +283,7 @@ async fn make_igvm_attest_requests( { Ok(response) => response, Err(e) => { - if wrapped_key_required { - // Notify host if WrappedKey is required for diagnosis. - get.event_log_fatal(EventLogId::WRAPPED_KEY_REQUIRED_BUT_INVALID) - .await; - } + notify_wrapped_key_required(get, wrapped_key_required).await; return Err(RequestVmgsEncryptionKeysError::SendIgvmAttestWrappedKeyRequest(e)); } @@ -285,19 +292,19 @@ async fn make_igvm_attest_requests( let wrapped_des_key = match igvm_attest::wrapped_key::parse_response(&response.response) { Ok(parsed_response) => { if parsed_response.wrapped_key.is_empty() { - Err(RequestVmgsEncryptionKeysError::EmptyWrappedKey)? + return Err(RequestVmgsEncryptionKeysError::EmptyWrappedKey); } // Update the key reference data to the response contents if parsed_response.key_reference.is_empty() { - Err(RequestVmgsEncryptionKeysError::EmptyKeyReference)? + return Err(RequestVmgsEncryptionKeysError::EmptyKeyReference); } if parsed_response.key_reference.len() > AGENT_DATA_MAX_SIZE { - Err(RequestVmgsEncryptionKeysError::InvalidKeyReferenceSize { + return Err(RequestVmgsEncryptionKeysError::InvalidKeyReferenceSize { key_reference_size: parsed_response.key_reference.len(), expected_size: AGENT_DATA_MAX_SIZE, - })? + }); } // Make sure rewriting the whole `agent_data` buffer @@ -322,9 +329,7 @@ async fn make_igvm_attest_requests( // The request does not succeed. // Return an error if WrappedKey is required, otherwise ignore the error and set the `wrapped_des_key` to None. if wrapped_key_required { - // Notify host if WrappedKey is required for diagnosis. - get.event_log_fatal(EventLogId::WRAPPED_KEY_REQUIRED_BUT_INVALID) - .await; + notify_wrapped_key_required(get, true).await; return Err( RequestVmgsEncryptionKeysError::RequiredButInvalidIgvmAttestWrappedKeyResponse, @@ -334,19 +339,18 @@ async fn make_igvm_attest_requests( } } Err(e) => { - if wrapped_key_required { - // Notify host if WrappedKey is required for diagnosis. - get.event_log_fatal(EventLogId::WRAPPED_KEY_REQUIRED_BUT_INVALID) - .await; - } + notify_wrapped_key_required(get, wrapped_key_required).await; return Err(RequestVmgsEncryptionKeysError::ParseIgvmAttestWrappedKeyResponse(e)); } }; - igvm_attest_request_helper.set_request_type(IgvmAttestRequestType::KEY_RELEASE_REQUEST); let request = igvm_attest_request_helper - .create_request(IGVM_ATTEST_REQUEST_CURRENT_VERSION, attestation_report) + .create_request( + IGVM_ATTEST_REQUEST_CURRENT_VERSION, + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + attestation_report, + ) .map_err(RequestVmgsEncryptionKeysError::CreateIgvmAttestKeyReleaseRequest)?; // Get tenant keys based on attestation results @@ -395,9 +399,10 @@ mod tests { let result = pkcs11_rsa_aes_key_unwrap(&rsa, &wrapped_key_blob); assert!(matches!( result, - Err(Pkcs11RsaAesKeyUnwrapError::UndersizedWrappedAesKey( - 256, 255 - )) + Err(Pkcs11RsaAesKeyUnwrapError::UndersizedWrappedAesKey { + expected_size: 256, + actual_size: 255, + }) )); // empty rsa key blob diff --git a/openhcl/underhill_attestation/src/test_helpers.rs b/openhcl/underhill_attestation/src/test_helpers.rs index 19b5fb5fa8..62ed23c607 100644 --- a/openhcl/underhill_attestation/src/test_helpers.rs +++ b/openhcl/underhill_attestation/src/test_helpers.rs @@ -10,10 +10,82 @@ use crate::jwt::JwtHeader; use base64::Engine; use crypto::rsa::RsaKeyPair; use crypto::x509::X509Certificate; +use guid::Guid; use openhcl_attestation_protocol::igvm_attest::akv; +use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE; +use openhcl_attestation_protocol::vmgs::DekKp; +use openhcl_attestation_protocol::vmgs::GSP_BUFFER_SIZE; +use openhcl_attestation_protocol::vmgs::GspKp; +use openhcl_attestation_protocol::vmgs::KeyProtector; +use openhcl_attestation_protocol::vmgs::NUMBER_KP; +use tee_call::GetAttestationReportResult; +use tee_call::HW_DERIVED_KEY_LENGTH; +use tee_call::REPORT_DATA_SIZE; +use tee_call::TeeCall; +use tee_call::TeeCallGetDerivedKey; +use tee_call::TeeType; pub const CIPHERTEXT: &str = "test"; +/// Construct a [`KeyProtector`] populated with deterministic, easily +/// distinguishable byte patterns for each KP slot. +/// +/// The caller supplies `active_kp` because different tests want different +/// starting states: the `lib.rs` orchestration tests start from a fresh +/// slot 0, while the `vmgs.rs` round-trip tests use `u32::MAX` to verify +/// that the field is preserved verbatim across serialization. +pub fn new_key_protector(active_kp: u32) -> KeyProtector { + // Ingress and egress KPs are assumed to be the only two KPs, therefore `NUMBER_KP` should be 2 + assert_eq!(NUMBER_KP, 2); + + let ingress_dek = DekKp { + dek_buffer: [1; DEK_BUFFER_SIZE], + }; + let egress_dek = DekKp { + dek_buffer: [2; DEK_BUFFER_SIZE], + }; + let ingress_gsp = GspKp { + gsp_length: GSP_BUFFER_SIZE as u32, + gsp_buffer: [3; GSP_BUFFER_SIZE], + }; + let egress_gsp = GspKp { + gsp_length: GSP_BUFFER_SIZE as u32, + gsp_buffer: [4; GSP_BUFFER_SIZE], + }; + KeyProtector { + dek: [ingress_dek, egress_dek], + gsp: [ingress_gsp, egress_gsp], + active_kp, + } +} + +/// Construct a [`KeyProtectorById`] for tests. +/// +/// When `found_id` is true, returns [`KeyProtectorById::Found`] populated +/// with the supplied `id_guid`/`ported` (or sensible defaults). When +/// `found_id` is false, returns [`KeyProtectorById::NotFound`] regardless +/// of the other arguments — modelling a VMGS that has never had a per-VM +/// key protector entry written. +/// +/// [`KeyProtectorById`]: crate::KeyProtectorById +/// [`KeyProtectorById::Found`]: crate::KeyProtectorById::Found +/// [`KeyProtectorById::NotFound`]: crate::KeyProtectorById::NotFound +pub fn new_key_protector_by_id( + id_guid: Option, + ported: Option, + found_id: bool, +) -> crate::KeyProtectorById { + if found_id { + crate::KeyProtectorById::Found(openhcl_attestation_protocol::vmgs::KeyProtectorById { + id_guid: id_guid.unwrap_or_else(Guid::new_random), + ported: ported.unwrap_or(0), + pad: [0; 3], + }) + } else { + crate::KeyProtectorById::NotFound + } +} + /// Generate a self-signed X.509 certificate for testing. pub fn generate_x509(key_pair: &RsaKeyPair) -> X509Certificate { X509Certificate::build_self_signed( @@ -79,3 +151,82 @@ pub fn generate_base64_encoded_jwt_components(key_pair: &RsaKeyPair) -> (String, (base64_header, base64_body, base64_signature) } + +/// Mock implementation of [`TeeCall`] with get derived key support for testing purposes +pub struct MockTeeCall { + /// Mock TCB version to return from get_attestation_report + pub tcb_version: u64, +} + +impl MockTeeCall { + /// Create a new instance of [`MockTeeCall`]. + pub fn new(tcb_version: u64) -> Self { + Self { tcb_version } + } +} + +impl TeeCall for MockTeeCall { + fn get_attestation_report( + &self, + report_data: &[u8; REPORT_DATA_SIZE], + ) -> Result { + let mut report = [0x6c; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE]; + report[..REPORT_DATA_SIZE].copy_from_slice(report_data); + + Ok(GetAttestationReportResult { + report: report.to_vec(), + tcb_version: Some(self.tcb_version), + }) + } + + fn supports_get_derived_key(&self) -> Option<&dyn TeeCallGetDerivedKey> { + Some(self) + } + + fn tee_type(&self) -> TeeType { + // Use Snp for testing + TeeType::Snp + } +} + +impl TeeCallGetDerivedKey for MockTeeCall { + fn get_derived_key(&self, tcb_version: u64) -> Result<[u8; 32], tee_call::Error> { + // Base test key; mix in policy so different policies yield different derived secrets + let mut key: [u8; HW_DERIVED_KEY_LENGTH] = [0xab; HW_DERIVED_KEY_LENGTH]; + + // Use mutation to simulate the policy + let tcb = tcb_version.to_le_bytes(); + for (i, b) in key.iter_mut().enumerate() { + *b ^= tcb[i % tcb.len()]; + } + + Ok(key) + } +} + +/// Mock implementation of [`TeeCall`] without get derived key support for testing purposes +pub struct MockTeeCallNoGetDerivedKey; + +impl TeeCall for MockTeeCallNoGetDerivedKey { + fn get_attestation_report( + &self, + report_data: &[u8; REPORT_DATA_SIZE], + ) -> Result { + let mut report = [0x6c; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE]; + report[..REPORT_DATA_SIZE].copy_from_slice(report_data); + + Ok(GetAttestationReportResult { + report: report.to_vec(), + tcb_version: None, + }) + } + + fn supports_get_derived_key(&self) -> Option<&dyn TeeCallGetDerivedKey> { + None + } + + fn tee_type(&self) -> TeeType { + // Use Snp for testing + TeeType::Snp + } +} diff --git a/openhcl/underhill_attestation/src/tests.rs b/openhcl/underhill_attestation/src/tests.rs new file mode 100644 index 0000000000..e62af311a3 --- /dev/null +++ b/openhcl/underhill_attestation/src/tests.rs @@ -0,0 +1,1173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::*; +use crate::test_helpers::MockTeeCall; +use crate::test_helpers::MockTeeCallNoGetDerivedKey; +use crate::test_helpers::new_key_protector_by_id; +use disk_backend::Disk; +use disklayer_ram::ram_disk; +use guest_emulation_device::IgvmAgentAction; +use guest_emulation_device::IgvmAgentTestPlan; +use guest_emulation_transport::test_utilities::TestGet; +use key_protector::AES_WRAPPED_AES_KEY_LENGTH; +use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestType; +use pal_async::DefaultDriver; +use pal_async::async_test; +use pal_async::task::Spawn; +use std::collections::VecDeque; +use test_with_tracing::test; +use vmgs_format::EncryptionAlgorithm; +use vmgs_format::FileId; +use zerocopy::IntoBytes; + +const ONE_MEGA_BYTE: u64 = 1024 * 1024; + +fn new_test_file() -> Disk { + ram_disk(4 * ONE_MEGA_BYTE, false).unwrap() +} + +async fn new_formatted_vmgs() -> Vmgs { + let disk = new_test_file(); + + let mut vmgs = Vmgs::format_new(disk, None).await.unwrap(); + + assert!( + key_protector_is_empty(&mut vmgs).await, + "Newly formatted VMGS should have an empty key protector" + ); + assert!( + key_protector_by_id_is_empty(&mut vmgs).await, + "Newly formatted VMGS should have an empty key protector by id" + ); + + vmgs +} + +async fn key_protector_is_empty(vmgs: &mut Vmgs) -> bool { + let key_protector = vmgs::read_key_protector(vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + + key_protector.as_bytes().iter().all(|&b| b == 0) +} + +async fn key_protector_by_id_is_empty(vmgs: &mut Vmgs) -> bool { + vmgs::read_key_protector_by_id(vmgs) + .await + .is_err_and(|err| { + matches!( + err, + vmgs::ReadFromVmgsError::EntryNotFound(FileId::VM_UNIQUE_ID) + ) + }) +} + +async fn hardware_key_protector_is_empty(vmgs: &mut Vmgs) -> bool { + vmgs::read_hardware_key_protector(vmgs) + .await + .is_err_and(|err| { + matches!( + err, + vmgs::ReadFromVmgsError::EntryNotFound(FileId::HW_KEY_PROTECTOR) + ) + }) +} + +fn new_key_protector() -> KeyProtector { + test_helpers::new_key_protector(0) +} + +async fn new_test_get( + spawn: impl Spawn, + enable_igvm_attest: bool, + plan: Option, +) -> TestGet { + if enable_igvm_attest { + const TEST_DEVICE_MEMORY_SIZE: u64 = 64; + // Use `DeviceTestMemory` to set up shared memory required by the IGVM_ATTEST GET calls. + let dev_test_mem = user_driver_emulated_mock::DeviceTestMemory::new( + TEST_DEVICE_MEMORY_SIZE, + true, + "test-attest", + ); + + let mut test_get = guest_emulation_transport::test_utilities::new_transport_pair( + spawn, + None, + get_protocol::ProtocolVersion::NICKEL_REV2, + Some(dev_test_mem.guest_memory()), + plan, + ) + .await; + + test_get.client.set_gpa_allocator(dev_test_mem.dma_client()); + + test_get + } else { + guest_emulation_transport::test_utilities::new_transport_pair( + spawn, + None, + get_protocol::ProtocolVersion::NICKEL_REV2, + None, + None, + ) + .await + } +} + +fn new_attestation_vm_config() -> AttestationVmConfig { + AttestationVmConfig { + current_time: None, + root_cert_thumbprint: String::new(), + console_enabled: false, + interactive_console_enabled: false, + secure_boot: false, + tpm_enabled: true, + tpm_persisted: true, + filtered_vpci_devices_allowed: false, + vm_unique_id: String::new(), + } +} + +#[async_test] +async fn do_nothing_without_derived_keys() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: false, + use_gsp_by_id: false, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + unlock_vmgs_data_store( + &mut vmgs, + false, + &mut key_protector, + &mut key_protector_by_id, + None, + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(key_protector_by_id_is_empty(&mut vmgs).await); + + // Create another instance as the previous `unlock_vmgs_data_store` took ownership of the last one + let key_protector_settings = KeyProtectorActions { + should_write_kp: false, + use_gsp_by_id: false, + use_hardware_unlock: false, + }; + + // Even if the VMGS is encrypted, if no derived keys are provided, nothing should happen + unlock_vmgs_data_store( + &mut vmgs, + true, + &mut key_protector, + &mut key_protector_by_id, + None, + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(key_protector_by_id_is_empty(&mut vmgs).await); +} + +#[async_test] +async fn provision_vmgs_and_rotate_keys() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let ingress = [1; AES_GCM_KEY_LENGTH]; + let egress = [2; AES_GCM_KEY_LENGTH]; + let derived_keys = Keys { + ingress, + decrypt_egress: None, + encrypt_egress: egress, + }; + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + // Without encryption implies the provision path + // The VMGS will be locked using the egress key + unlock_vmgs_data_store( + &mut vmgs, + false, + &mut key_protector, + &mut key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + // The ingress key is essentially ignored since the VMGS wasn't previously encrypted + vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); + + // The egress key was used to lock the VMGS after provisioning + vmgs.unlock_with_encryption_key(&egress).await.unwrap(); + // Since this is a new VMGS, the egress key is the first and only key + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); + + // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated + assert!(!key_protector_is_empty(&mut vmgs).await); + assert!(!key_protector_by_id_is_empty(&mut vmgs).await); + + let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); + + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!( + found_key_protector_by_id.as_bytes(), + key_protector_by_id.inner_for_test().as_bytes() + ); + + // Now that the VMGS has been provisioned, simulate the rotation of keys + let new_egress = [3; AES_GCM_KEY_LENGTH]; + + let mut new_key_protector = new_key_protector(); + let mut new_key_protector_by_id = new_key_protector_by_id(None, None, false); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + // Ingress is now the old egress, and we provide a new new egress key + let derived_keys = Keys { + ingress: egress, + decrypt_egress: None, + encrypt_egress: new_egress, + }; + + unlock_vmgs_data_store( + &mut vmgs, + true, + &mut new_key_protector, + &mut new_key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + // We should still fail to unlock the VMGS with the original ingress key + vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); + // The old egress key should no longer be able to unlock the VMGS + vmgs.unlock_with_encryption_key(&egress).await.unwrap_err(); + + // The new egress key should be able to unlock the VMGS + vmgs.unlock_with_encryption_key(&new_egress).await.unwrap(); + // The old egress key was removed, but not before the new egress key was added in the 1th slot + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); + + let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + assert_eq!(found_key_protector.as_bytes(), new_key_protector.as_bytes()); + + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!( + found_key_protector_by_id.as_bytes(), + new_key_protector_by_id.inner_for_test().as_bytes() + ); +} + +#[async_test] +async fn unlock_previously_encrypted_vmgs_with_ingress_key() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let ingress = [1; AES_GCM_KEY_LENGTH]; + let egress = [2; AES_GCM_KEY_LENGTH]; + + let derived_keys = Keys { + ingress, + decrypt_egress: None, + encrypt_egress: egress, + }; + + vmgs.update_encryption_key(&ingress, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + + // Initially, the VMGS can be unlocked using the ingress key + vmgs.unlock_with_encryption_key(&ingress).await.unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + unlock_vmgs_data_store( + &mut vmgs, + true, + &mut key_protector, + &mut key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + // After the VMGS has been unlocked, the VMGS encryption key should be rotated from ingress to egress + vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); + vmgs.unlock_with_encryption_key(&egress).await.unwrap(); + // The ingress key was removed, but not before the egress key was added in the 0th slot + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); + + // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated + let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); + + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!( + found_key_protector_by_id.as_bytes(), + key_protector_by_id.inner_for_test().as_bytes() + ); +} + +#[async_test] +async fn failed_to_persist_ingress_key_so_use_egress_key_to_unlock_vmgs() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let ingress = [1; AES_GCM_KEY_LENGTH]; + let decrypt_egress = [2; AES_GCM_KEY_LENGTH]; + let encrypt_egress = [3; AES_GCM_KEY_LENGTH]; + + let derived_keys = Keys { + ingress, + decrypt_egress: Some(decrypt_egress), + encrypt_egress, + }; + + // Add only the egress key to the VMGS to simulate a failure to persist the ingress key + vmgs.test_add_new_encryption_key(&decrypt_egress, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + let egress_key_index = vmgs.test_get_active_datastore_key_index().unwrap(); + assert_eq!(egress_key_index, 0); + + vmgs.unlock_with_encryption_key(&decrypt_egress) + .await + .unwrap(); + let found_egress_key_index = vmgs.test_get_active_datastore_key_index().unwrap(); + assert_eq!(found_egress_key_index, egress_key_index); + + // Confirm that the ingress key cannot be used to unlock the VMGS + vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + unlock_vmgs_data_store( + &mut vmgs, + true, + &mut key_protector, + &mut key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await + .unwrap(); + + // Confirm that the ingress key was not added + vmgs.unlock_with_encryption_key(&ingress).await.unwrap_err(); + + // Confirm that the decrypt egress key no longer works + vmgs.unlock_with_encryption_key(&decrypt_egress) + .await + .unwrap_err(); + + // The encrypt_egress key can unlock the VMGS and was added as a new key + vmgs.unlock_with_encryption_key(&encrypt_egress) + .await + .unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); + + // Since both `should_write_kp` and `use_gsp_by_id` are true, both key protectors should be updated + let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + assert_eq!(found_key_protector.as_bytes(), key_protector.as_bytes()); + + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!( + found_key_protector_by_id.as_bytes(), + key_protector_by_id.inner_for_test().as_bytes() + ); +} + +#[async_test] +async fn fail_to_unlock_vmgs_with_existing_ingress_key() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let ingress = [1; AES_GCM_KEY_LENGTH]; + + // Ingress and egress keys are the same + let derived_keys = Keys { + ingress, + decrypt_egress: None, + encrypt_egress: ingress, + }; + + // Add two random keys to the VMGS to simulate unlock failure when ingress and egress keys are the same + let additional_key = [2; AES_GCM_KEY_LENGTH]; + let yet_another_key = [3; AES_GCM_KEY_LENGTH]; + + vmgs.test_add_new_encryption_key(&additional_key, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); + + vmgs.test_add_new_encryption_key(&yet_another_key, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + let unlock_result = unlock_vmgs_data_store( + &mut vmgs, + true, + &mut key_protector, + &mut key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await; + assert!(matches!( + unlock_result, + Err(UnlockVmgsDataStoreError::VmgsUnlockUsingExistingIngressKey( + _ + )) + )); +} + +#[async_test] +async fn fail_to_unlock_vmgs_with_new_ingress_key() { + let mut vmgs = new_formatted_vmgs().await; + + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, false); + + let derived_keys = Keys { + ingress: [1; AES_GCM_KEY_LENGTH], + decrypt_egress: None, + encrypt_egress: [2; AES_GCM_KEY_LENGTH], + }; + + // Add two random keys to the VMGS to simulate unlock failure when ingress and egress keys are *not* the same + let additional_key = [3; AES_GCM_KEY_LENGTH]; + let yet_another_key = [4; AES_GCM_KEY_LENGTH]; + + vmgs.test_add_new_encryption_key(&additional_key, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(0)); + + vmgs.test_add_new_encryption_key(&yet_another_key, EncryptionAlgorithm::AES_GCM) + .await + .unwrap(); + assert_eq!(vmgs.test_get_active_datastore_key_index(), Some(1)); + + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + + let bios_guid = Guid::new_random(); + + let unlock_result = unlock_vmgs_data_store( + &mut vmgs, + true, + &mut key_protector, + &mut key_protector_by_id, + Some(derived_keys), + key_protector_settings, + bios_guid, + ) + .await; + assert!(matches!( + unlock_result, + Err(UnlockVmgsDataStoreError::VmgsUnlockUsingExistingIngressKey( + _ + )) + )); +} + +#[async_test] +async fn pass_through_persist_all_key_protectors() { + let mut vmgs = new_formatted_vmgs().await; + 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(); + + // Copied/cloned bits used for comparison later + let kp_copy = key_protector.as_bytes().to_vec(); + let active_kp_copy = key_protector.active_kp; + + // When all key protector settings are true, no actions will be taken on the key protectors or VMGS + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: true, + use_hardware_unlock: true, + }; + persist_all_key_protectors( + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + key_protector_settings, + ) + .await + .unwrap(); + + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(key_protector_by_id_is_empty(&mut vmgs).await); + + // The key protector should remain unchanged + assert_eq!(active_kp_copy, key_protector.active_kp); + assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); +} + +#[async_test] +async fn persist_all_key_protectors_write_key_protector_by_id() { + let mut vmgs = new_formatted_vmgs().await; + 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(); + + // Copied/cloned bits used for comparison later + let kp_copy = key_protector.as_bytes().to_vec(); + let active_kp_copy = key_protector.active_kp; + + // When `use_gsp_by_id` is true and `should_write_kp` is false, the key protector by id should be written to the VMGS + let key_protector_settings = KeyProtectorActions { + should_write_kp: false, + use_gsp_by_id: true, + use_hardware_unlock: false, + }; + persist_all_key_protectors( + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + key_protector_settings, + ) + .await + .unwrap(); + + // The previously empty VMGS now holds the key protector by id but not the key protector + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(!key_protector_by_id_is_empty(&mut vmgs).await); + + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!( + found_key_protector_by_id.as_bytes(), + key_protector_by_id.inner_for_test().as_bytes() + ); + + // The key protector should remain unchanged + assert_eq!(kp_copy.as_slice(), key_protector.as_bytes()); + assert_eq!(active_kp_copy, key_protector.active_kp); +} + +#[async_test] +async fn persist_all_key_protectors_remove_ingress_kp() { + let mut vmgs = new_formatted_vmgs().await; + 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(); + + // Copied active KP for later use + let active_kp_copy = key_protector.active_kp; + + // When `use_gsp_by_id` is false, `should_write_kp` is true, and `use_hardware_unlock` is false, the active key protector's + // active kp's dek should be zeroed, the active kp's gsp length should be set to 0, and the active kp should be incremented + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: false, + use_hardware_unlock: false, + }; + persist_all_key_protectors( + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + key_protector_settings, + ) + .await + .unwrap(); + + assert!(!key_protector_is_empty(&mut vmgs).await); + assert!(key_protector_by_id_is_empty(&mut vmgs).await); + + // The previously empty VMGS's key protector should now be overwritten + let found_key_protector = vmgs::read_key_protector(&mut vmgs, AES_WRAPPED_AES_KEY_LENGTH) + .await + .unwrap(); + + assert!( + found_key_protector.dek[active_kp_copy as usize] + .dek_buffer + .iter() + .all(|&b| b == 0), + ); + assert_eq!( + found_key_protector.gsp[active_kp_copy as usize].gsp_length, + 0 + ); + assert_eq!(found_key_protector.active_kp, active_kp_copy + 1); +} + +#[async_test] +async fn persist_all_key_protectors_mark_key_protector_by_id_as_not_in_use() { + let mut vmgs = new_formatted_vmgs().await; + let mut key_protector = new_key_protector(); + let mut key_protector_by_id = new_key_protector_by_id(None, None, true); + let bios_guid = Guid::new_random(); + + // When `use_gsp_by_id` is false, `should_write_kp` is true, `use_hardware_unlock` is true, and + // the key protector by id is found and not ported, the key protector by id should be marked as ported + let key_protector_settings = KeyProtectorActions { + should_write_kp: true, + use_gsp_by_id: false, + use_hardware_unlock: true, + }; + + persist_all_key_protectors( + &mut vmgs, + &mut key_protector, + &mut key_protector_by_id, + bios_guid, + key_protector_settings, + ) + .await + .unwrap(); + + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(!key_protector_by_id_is_empty(&mut vmgs).await); + + // The previously empty VMGS's key protector by id should now be overwritten + let found_key_protector_by_id = vmgs::read_key_protector_by_id(&mut vmgs).await.unwrap(); + assert_eq!(found_key_protector_by_id.ported, 1); + assert_eq!( + found_key_protector_by_id.id_guid, + key_protector_by_id.inner_for_test().id_guid + ); +} + +// --- initialize_platform_security tests --- + +#[async_test] +async fn init_sec_suppress_attestation(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // Write non-zero agent data to VMGS so we can verify it is returned. + let agent = SecurityProfile { + agent_data: [0xAA; AGENT_DATA_MAX_SIZE], + }; + vmgs.write_file(FileId::ATTEST, agent.as_bytes()) + .await + .unwrap(); + + // Ensure no IGVM attest call out + let get_pair = new_test_get(driver, false, None).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + None, // no TEE when suppressed + true, // suppress_attestation + ldriver, + GuestStateEncryptionPolicy::None, + true, + ) + .await + .unwrap(); + + // VMGS remains unencrypted and KP/HWKP not written. + assert!(!vmgs.encrypted()); + assert!(key_protector_is_empty(&mut vmgs).await); + assert!(hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data passed through + assert_eq!(res.agent_data.unwrap(), agent.agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); +} + +#[async_test] +async fn init_sec_secure_key_release_with_wrapped_key_request(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // IGVM attest is required + let get_pair = new_test_get(driver, true, None).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + let tee = MockTeeCall::new(0x1234); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted and HWKP is updated. + assert!(vmgs.encrypted()); + assert!(!hardware_key_protector_is_empty(&mut vmgs).await); + + // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. + // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. + let key_reference = serde_json::json!({ + "key_info": { + "host": "name" + }, + "attestation_info": { + "host": "attestation_name" + } + }); + let key_reference = serde_json::to_string(&key_reference).unwrap(); + let key_reference = key_reference.as_bytes(); + let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; + expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); + assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: VMGS unlock via SKR should succeed + initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS should remain encrypted + assert!(vmgs.encrypted()); +} + +#[async_test] +async fn init_sec_secure_key_release_without_wrapped_key_request(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // Write non-zero agent data to workaround the WRAPPED_KEY_REQUEST requirement. + let agent = SecurityProfile { + agent_data: [0xAA; AGENT_DATA_MAX_SIZE], + }; + vmgs.write_file(FileId::ATTEST, agent.as_bytes()) + .await + .unwrap(); + + // Skip WRAPPED_KEY_REQUEST for both boots + let mut plan = IgvmAgentTestPlan::default(); + plan.insert( + IgvmAttestRequestType::WRAPPED_KEY_REQUEST, + VecDeque::from([IgvmAgentAction::NoResponse, IgvmAgentAction::NoResponse]), + ); + + // IGVM attest is required + let get_pair = new_test_get(driver, true, Some(plan)).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + let tee = MockTeeCall::new(0x1234); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted and HWKP is updated. + assert!(vmgs.encrypted()); + assert!(!hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data passed through + assert_eq!(res.agent_data.clone().unwrap(), agent.agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: VMGS unlock via SKR should succeed + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS should remain encrypted + assert!(vmgs.encrypted()); + // Agent data passed through + assert_eq!(res.agent_data.clone().unwrap(), agent.agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); +} + +#[async_test] +async fn init_sec_secure_key_release_hw_sealing_backup(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // IGVM attest is required + let mut plan = IgvmAgentTestPlan::default(); + plan.insert( + IgvmAttestRequestType::WRAPPED_KEY_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + // initialize_platform_security will attempt SKR/unlock 10 times + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + ]), + ); + + let get_pair = new_test_get(driver, true, Some(plan)).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let tee = MockTeeCall::new(0x1234); + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted and HWKP is updated. + assert!(vmgs.encrypted()); + assert!(!hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. + // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. + let key_reference = serde_json::json!({ + "key_info": { + "host": "name" + }, + "attestation_info": { + "host": "attestation_name" + } + }); + let key_reference = serde_json::to_string(&key_reference).unwrap(); + let key_reference = key_reference.as_bytes(); + let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; + expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); + assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: VMGS unlock via key recovered with hardware sealing + // NOTE: The test relies on the test GED to return failing WRAPPED_KEY response + // with retry recommendation as false to skip the retry loop in + // secure_key_release::request_vmgs_encryption_keys. Otherwise, the test will stuck + // on the timer.sleep() as the the driver is not progressed. + initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS should remain encrypted + assert!(vmgs.encrypted()); +} + +#[async_test] +async fn init_sec_secure_key_release_skip_hw_unsealing(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // IGVM attest is required + // KEY_RELEASE succeeds on first boot, fails with skip_hw_unsealing on second boot. + // WRAPPED_KEY is not in the plan, so it falls back to default (success) every time. + let mut plan = IgvmAgentTestPlan::default(); + plan.insert( + IgvmAttestRequestType::KEY_RELEASE_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + IgvmAgentAction::RespondFailureSkipHwUnsealing, + ]), + ); + + let get_pair = new_test_get(driver, true, Some(plan)).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let tee = MockTeeCall::new(0x1234); + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted and HWKP is updated. + assert!(vmgs.encrypted()); + assert!(!hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. + let key_reference = serde_json::json!({ + "key_info": { + "host": "name" + }, + "attestation_info": { + "host": "attestation_name" + } + }); + let key_reference = serde_json::to_string(&key_reference).unwrap(); + let key_reference = key_reference.as_bytes(); + let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; + expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); + assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: KEY_RELEASE fails with skip_hw_unsealing signal. + // The skip_hw_unsealing signal causes the hardware unsealing fallback to be + // skipped, so VMGS unlock should fail. + // NOTE: The test relies on the test GED to return failing KEY_RELEASE response + // with retry recommendation as false so the retry loop terminates immediately. + // Otherwise, the test will get stuck on timer.sleep() as the driver is not + // progressed. + let result = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await; + + assert!(result.is_err()); +} + +#[async_test] +async fn init_sec_secure_key_release_no_hw_sealing_backup(driver: DefaultDriver) { + let mut vmgs = new_formatted_vmgs().await; + + // IGVM attest is required + let mut plan = IgvmAgentTestPlan::default(); + plan.insert( + IgvmAttestRequestType::WRAPPED_KEY_REQUEST, + VecDeque::from([ + IgvmAgentAction::RespondSuccess, + // initialize_platform_security will attempt SKR/unlock 10 times + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + IgvmAgentAction::RespondFailure, + ]), + ); + + let get_pair = new_test_get(driver, true, Some(plan)).await; + + let bios_guid = Guid::new_random(); + let att_cfg = new_attestation_vm_config(); + // Without hardware sealing support + let tee = MockTeeCallNoGetDerivedKey {}; + + // Ensure VMGS is not encrypted and agent data is empty before the call + assert!(!vmgs.encrypted()); + + // Obtain a LocalDriver briefly, then run the async flow under the pool executor + let ldriver = pal_async::local::block_with_io(|ld| async move { ld }); + let res = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver.clone(), + GuestStateEncryptionPolicy::Auto, + true, + ) + .await + .unwrap(); + + // VMGS is now encrypted but HWKP remains empty. + assert!(vmgs.encrypted()); + assert!(hardware_key_protector_is_empty(&mut vmgs).await); + // Agent data should be the same as `key_reference` in the WRAPPED_KEY response. + // See vm/devices/get/guest_emulation_device/src/test_igvm_agent.rs for the expected response. + let key_reference = serde_json::json!({ + "key_info": { + "host": "name" + }, + "attestation_info": { + "host": "attestation_name" + } + }); + let key_reference = serde_json::to_string(&key_reference).unwrap(); + let key_reference = key_reference.as_bytes(); + let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE]; + expected_agent_data[..key_reference.len()].copy_from_slice(key_reference); + assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec()); + // Secure key should be None without pre-provisioning + assert!(res.guest_secret_key.is_none()); + + // Second call: VMGS unlock should fail without hardware sealing support + let result = initialize_platform_security( + &get_pair.client, + bios_guid, + &att_cfg, + &mut vmgs, + Some(&tee), + false, + ldriver, + GuestStateEncryptionPolicy::Auto, + true, + ) + .await; + + assert!(result.is_err()); +} diff --git a/openhcl/underhill_attestation/src/vmgs.rs b/openhcl/underhill_attestation/src/vmgs.rs index 58bc14c02e..b54e779ede 100644 --- a/openhcl/underhill_attestation/src/vmgs.rs +++ b/openhcl/underhill_attestation/src/vmgs.rs @@ -61,7 +61,7 @@ pub(crate) struct WriteToVmgsError { /// Read Key Protector data from the VMGS file. If [`FileId::KEY_PROTECTOR`] doesn't exist yet, /// locally initialize a key_protector instance that can be written to. -pub async fn read_key_protector( +pub(crate) async fn read_key_protector( vmgs: &mut Vmgs, dek_minimal_size: usize, ) -> Result { @@ -71,19 +71,19 @@ pub async fn read_key_protector( match vmgs.read_file(file_id).await { Ok(data) => { if data.len() < dek_minimal_size { - Err(ReadFromVmgsError::EntrySizeTooSmall { + return Err(ReadFromVmgsError::EntrySizeTooSmall { file_id, size: data.len(), minimal_size: dek_minimal_size, - })? + }); } if data.len() > KEY_PROTECTOR_SIZE { - Err(ReadFromVmgsError::EntrySizeTooLarge { + return Err(ReadFromVmgsError::EntrySizeTooLarge { file_id, size: data.len(), maximum_size: KEY_PROTECTOR_SIZE, - })? + }); } let data = if data.len() < KEY_PROTECTOR_SIZE { @@ -106,7 +106,7 @@ pub async fn read_key_protector( } /// Write Key Protector data to the VMGS file. -pub async fn write_key_protector( +pub(crate) async fn write_key_protector( key_protector: &KeyProtector, vmgs: &mut Vmgs, ) -> Result<(), WriteToVmgsError> { @@ -117,7 +117,7 @@ pub async fn write_key_protector( } /// Read Key Protector ID from the VMGS file. -pub async fn read_key_protector_by_id( +pub(crate) async fn read_key_protector_by_id( vmgs: &mut Vmgs, ) -> Result { // This file could include state data following the GUID. @@ -152,7 +152,7 @@ pub async fn read_key_protector_by_id( /// /// Write if `bios_guid` is different from the one held in `key_protector_by_id` (which /// will be set to `bios_guid` before write) or `force_write` is `true`. -pub async fn write_key_protector_by_id( +pub(crate) async fn write_key_protector_by_id( key_protector_by_id: &mut KeyProtectorById, vmgs: &mut Vmgs, force_write: bool, @@ -171,16 +171,18 @@ pub async fn write_key_protector_by_id( /// Read the security profile from the VMGS file. If [`FileId::ATTEST`] doesn't exist yet, /// return an empty vector. -pub async fn read_security_profile(vmgs: &mut Vmgs) -> Result { +pub(crate) async fn read_security_profile( + vmgs: &mut Vmgs, +) -> Result { let file_id = FileId::ATTEST; match vmgs.read_file(file_id).await { Ok(data) => { if data.len() > AGENT_DATA_MAX_SIZE { - Err(ReadFromVmgsError::EntrySizeTooLarge { + return Err(ReadFromVmgsError::EntrySizeTooLarge { file_id, size: data.len(), maximum_size: AGENT_DATA_MAX_SIZE, - })? + }); } let data = if data.len() < AGENT_DATA_MAX_SIZE { @@ -198,12 +200,12 @@ pub async fn read_security_profile(vmgs: &mut Vmgs) -> Result Ok(SecurityProfile::new_zeroed()), - Err(vmgs_err) => Err(ReadFromVmgsError::ReadFromVmgs { file_id, vmgs_err })?, + Err(vmgs_err) => Err(ReadFromVmgsError::ReadFromVmgs { file_id, vmgs_err }), } } /// Read the hardware key protector from the VMGS file. -pub async fn read_hardware_key_protector( +pub(crate) async fn read_hardware_key_protector( vmgs: &mut Vmgs, ) -> Result { use openhcl_attestation_protocol::vmgs::HW_KEY_PROTECTOR_SIZE; @@ -220,11 +222,11 @@ pub async fn read_hardware_key_protector( }; if data.len() != HW_KEY_PROTECTOR_SIZE { - Err(ReadFromVmgsError::EntrySizeUnexpected { + return Err(ReadFromVmgsError::EntrySizeUnexpected { file_id, size: data.len(), expected_size: HW_KEY_PROTECTOR_SIZE, - })? + }); } HardwareKeyProtector::read_from_prefix(&data) @@ -233,7 +235,7 @@ pub async fn read_hardware_key_protector( } /// Write Key Protector Id (current Id) to the VMGS file. -pub async fn write_hardware_key_protector( +pub(crate) async fn write_hardware_key_protector( hardware_key_protector: &HardwareKeyProtector, vmgs: &mut Vmgs, ) -> Result<(), WriteToVmgsError> { @@ -244,16 +246,18 @@ pub async fn write_hardware_key_protector( } /// Read the guest secret key from VMGS file. -pub async fn read_guest_secret_key(vmgs: &mut Vmgs) -> Result { +pub(crate) async fn read_guest_secret_key( + vmgs: &mut Vmgs, +) -> Result { let file_id = FileId::GUEST_SECRET_KEY; match vmgs.read_file(file_id).await { Ok(data) => { if data.len() > GUEST_SECRET_KEY_MAX_SIZE { - Err(ReadFromVmgsError::EntrySizeTooLarge { + return Err(ReadFromVmgsError::EntrySizeTooLarge { file_id, size: data.len(), maximum_size: GUEST_SECRET_KEY_MAX_SIZE, - })? + }); } let data = if data.len() < GUEST_SECRET_KEY_MAX_SIZE { @@ -282,17 +286,13 @@ mod tests { use disklayer_ram::ram_disk; use openhcl_attestation_protocol::vmgs::AES_CBC_IV_LENGTH; use openhcl_attestation_protocol::vmgs::AES_GCM_KEY_LENGTH; - use openhcl_attestation_protocol::vmgs::DEK_BUFFER_SIZE; - use openhcl_attestation_protocol::vmgs::DekKp; 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_SIZE; use openhcl_attestation_protocol::vmgs::HardwareKeyProtectorHeader; use openhcl_attestation_protocol::vmgs::KEY_PROTECTOR_SIZE; use openhcl_attestation_protocol::vmgs::KeyProtector; use openhcl_attestation_protocol::vmgs::KeyProtectorById; - use openhcl_attestation_protocol::vmgs::NUMBER_KP; use pal_async::async_test; const ONE_MEGA_BYTE: u64 = 1024 * 1024; @@ -322,28 +322,7 @@ mod tests { } fn new_key_protector() -> KeyProtector { - // Ingress and egress KPs are assumed to be the only two KPs, therefore `NUMBER_KP` should be 2 - assert_eq!(NUMBER_KP, 2); - - let ingress_dek = DekKp { - dek_buffer: [1; DEK_BUFFER_SIZE], - }; - let egress_dek = DekKp { - dek_buffer: [2; DEK_BUFFER_SIZE], - }; - let ingress_gsp = GspKp { - gsp_length: GSP_BUFFER_SIZE as u32, - gsp_buffer: [3; GSP_BUFFER_SIZE], - }; - let egress_gsp = GspKp { - gsp_length: GSP_BUFFER_SIZE as u32, - gsp_buffer: [4; GSP_BUFFER_SIZE], - }; - KeyProtector { - dek: [ingress_dek, egress_dek], - gsp: [ingress_gsp, egress_gsp], - active_kp: u32::MAX, - } + crate::test_helpers::new_key_protector(u32::MAX) } #[async_test] @@ -379,22 +358,28 @@ mod tests { .unwrap(); let found_key_protector_result = read_key_protector(&mut vmgs, key_protector_bytes.len()).await; - assert!(found_key_protector_result.is_err()); - assert_eq!( - found_key_protector_result.unwrap_err().to_string(), - "KEY_PROTECTOR valid bytes 2059 smaller than the minimal size 2060" - ); + assert!(matches!( + found_key_protector_result, + Err(ReadFromVmgsError::EntrySizeTooSmall { + file_id: FileId::KEY_PROTECTOR, + size: 2059, + minimal_size: 2060, + }) + )); // Read an oversized key protector vmgs.write_file(FileId::KEY_PROTECTOR, &[1; KEY_PROTECTOR_SIZE + 1]) .await .unwrap(); let found_key_protector_result = read_key_protector(&mut vmgs, KEY_PROTECTOR_SIZE).await; - assert!(found_key_protector_result.is_err()); - assert_eq!( - found_key_protector_result.unwrap_err().to_string(), - "KEY_PROTECTOR valid bytes 2061 larger than the maximum size 2060" - ); + assert!(matches!( + found_key_protector_result, + Err(ReadFromVmgsError::EntrySizeTooLarge { + file_id: FileId::KEY_PROTECTOR, + size: 2061, + maximum_size: 2060, + }) + )); // Read a key protector that is equal to the `dek_minimal_size` and smaller than the `KEY_PROTECTOR_SIZE` // so that padding is added @@ -430,11 +415,10 @@ mod tests { // Try to read the `key_protector_by_id` from the VMGS file which doesn't have a `key_protector_by_id` entry let found_key_protector_by_id_result = read_key_protector_by_id(&mut vmgs).await; - assert!(found_key_protector_by_id_result.is_err()); - assert_eq!( - found_key_protector_by_id_result.unwrap_err().to_string(), - "entry does not exist, file id: VM_UNIQUE_ID" - ); + assert!(matches!( + found_key_protector_by_id_result, + Err(ReadFromVmgsError::EntryNotFound(FileId::VM_UNIQUE_ID)) + )); // Populate the VMGS file with `key_protector_by_id` write_key_protector_by_id(&mut key_protector_by_id, &mut vmgs, true, kp_guid) @@ -506,11 +490,14 @@ mod tests { .await .unwrap(); let found_security_profile_result = read_security_profile(&mut vmgs).await; - assert!(found_security_profile_result.is_err()); - assert_eq!( - found_security_profile_result.unwrap_err().to_string(), - "ATTEST valid bytes 2049 larger than the maximum size 2048" - ); + assert!(matches!( + found_security_profile_result, + Err(ReadFromVmgsError::EntrySizeTooLarge { + file_id: FileId::ATTEST, + size: 2049, + maximum_size: AGENT_DATA_MAX_SIZE, + }) + )); // Write a security profile smaller than the maximum size to the VMGS and observe that it is padded with zeros let undersized_security_profile = [7u8; AGENT_DATA_MAX_SIZE - 10]; @@ -558,11 +545,14 @@ mod tests { .await .unwrap(); let found_hardware_key_protector_result = read_hardware_key_protector(&mut vmgs).await; - assert!(found_hardware_key_protector_result.is_err()); - assert_eq!( - found_hardware_key_protector_result.unwrap_err().to_string(), - "HW_KEY_PROTECTOR valid bytes 105, expected 104" - ); + assert!(matches!( + found_hardware_key_protector_result, + Err(ReadFromVmgsError::EntrySizeUnexpected { + file_id: FileId::HW_KEY_PROTECTOR, + size: 105, + expected_size: HW_KEY_PROTECTOR_SIZE, + }) + )); } #[async_test] @@ -571,11 +561,10 @@ mod tests { // When no guest secret key exists, an error should be returned let found_guest_secret_key_result = read_guest_secret_key(&mut vmgs).await; - assert!(found_guest_secret_key_result.is_err()); - assert_eq!( - found_guest_secret_key_result.unwrap_err().to_string(), - "entry does not exist, file id: GUEST_SECRET_KEY" - ); + assert!(matches!( + found_guest_secret_key_result, + Err(ReadFromVmgsError::EntryNotFound(FileId::GUEST_SECRET_KEY)) + )); // Write a guest secret key to the VMGS let guest_secret_key = GuestSecretKey { @@ -596,11 +585,14 @@ mod tests { .await .unwrap(); let found_guest_secret_key_result = read_guest_secret_key(&mut vmgs).await; - assert!(found_guest_secret_key_result.is_err()); - assert_eq!( - found_guest_secret_key_result.unwrap_err().to_string(), - "GUEST_SECRET_KEY valid bytes 2049 larger than the maximum size 2048" - ); + assert!(matches!( + found_guest_secret_key_result, + Err(ReadFromVmgsError::EntrySizeTooLarge { + file_id: FileId::GUEST_SECRET_KEY, + size: 2049, + maximum_size: GUEST_SECRET_KEY_MAX_SIZE, + }) + )); // Write a guest secret smaller than the maximum size to the VMGS and observe that it is padded with zeros let undersized_guest_secret_key = [7u8; GUEST_SECRET_KEY_MAX_SIZE - 10]; diff --git a/openhcl/underhill_core/src/emuplat/tpm.rs b/openhcl/underhill_core/src/emuplat/tpm.rs index 23ff174470..58879b96d8 100644 --- a/openhcl/underhill_core/src/emuplat/tpm.rs +++ b/openhcl/underhill_core/src/emuplat/tpm.rs @@ -5,6 +5,7 @@ use guest_emulation_transport::GuestEmulationTransportClient; use guest_emulation_transport::api::EventLogId; use openhcl_attestation_protocol::igvm_attest::get::AK_CERT_RESPONSE_BUFFER_SIZE; use openhcl_attestation_protocol::igvm_attest::get::IGVM_ATTEST_REQUEST_CURRENT_VERSION; +use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestType; use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestVersion; use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig; use std::sync::Arc; @@ -96,7 +97,11 @@ impl RequestAkCert for TpmRequestAkCertHelper { }; let request = ak_cert_request_helper - .create_request(version, &attestation_report) + .create_request( + version, + IgvmAttestRequestType::AK_CERT_REQUEST, + &attestation_report, + ) .map_err(TpmAttestationError::CreateAkCertRequest)?; Ok(request) From 47d2987043331d14d2e9fbe16895d196c947450d Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Thu, 21 May 2026 17:02:08 +0000 Subject: [PATCH 2/2] underhill_attestation: validate host-controlled lengths before slicing - wrapped_key::parse_response: check header.data_size >= header_size before slicing payload to avoid panic on malformed response. - key_protector::unwrap_existing_egress_dek: validate AES/RSA unwrap output length equals AES_GCM_KEY_LENGTH before copying into the fixed-size key buffer. --- .../src/igvm_attest/wrapped_key.rs | 14 +++++++++- .../src/key_protector.rs | 26 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs b/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs index 475f21d392..5e33e49a05 100644 --- a/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs +++ b/openhcl/underhill_attestation/src/igvm_attest/wrapped_key.rs @@ -23,6 +23,11 @@ pub(crate) enum WrappedKeyError { ParseHeader(#[source] CommonError), #[error("invalid response header version: {0}")] InvalidResponseVersion(u32), + #[error("response data_size {data_size} is smaller than header size {header_size}")] + InvalidDataSize { + data_size: usize, + header_size: usize, + }, } /// Return value of the [`parse_response`]. @@ -56,7 +61,14 @@ pub fn parse_response(response: &[u8]) -> Result size_of::(), invalid_version => return Err(WrappedKeyError::InvalidResponseVersion(invalid_version.0)), }; - let payload = &response[header_size..header.data_size as usize]; + let data_size = header.data_size as usize; + if data_size < header_size { + return Err(WrappedKeyError::InvalidDataSize { + data_size, + header_size, + }); + } + let payload = &response[header_size..data_size]; if payload.len() < MINIMUM_PAYLOAD_SIZE { return Err(WrappedKeyError::PayloadSizeTooSmall); diff --git a/openhcl/underhill_attestation/src/key_protector.rs b/openhcl/underhill_attestation/src/key_protector.rs index cbc8f175d5..8f1f53eab8 100644 --- a/openhcl/underhill_attestation/src/key_protector.rs +++ b/openhcl/underhill_attestation/src/key_protector.rs @@ -205,17 +205,35 @@ fn unwrap_existing_egress_dek( let dek_buffer = kp.dek[egress_idx].dek_buffer; let old_egress_key = if let Some(unwrapping_key) = des_key { // The DEK buffer should contain an AES-wrapped key. - crypto::aes_kwp::AesKeyWrap::new(unwrapping_key) + let aes_unwrapped_key = crypto::aes_kwp::AesKeyWrap::new(unwrapping_key) .and_then(|kw| { kw.unwrapper()? .unwrap(&dek_buffer[..AES_WRAPPED_AES_KEY_LENGTH]) }) - .map_err(GetKeysFromKeyProtectorError::EgressDekAesUnwrap)? + .map_err(GetKeysFromKeyProtectorError::EgressDekAesUnwrap)?; + + if aes_unwrapped_key.len() != AES_GCM_KEY_LENGTH { + return Err(GetKeysFromKeyProtectorError::InvalidAesUnwrapOutputSize { + output_size: aes_unwrapped_key.len(), + expected_size: AES_GCM_KEY_LENGTH, + }); + } + + aes_unwrapped_key } else { // The DEK buffer should contain an RSA-wrapped key. - ingress_kek + let rsa_unwrapped_key = ingress_kek .oaep_decrypt(&dek_buffer[..modulus_size], HashAlgorithm::Sha256) - .map_err(GetKeysFromKeyProtectorError::EgressDekRsaUnwrap)? + .map_err(GetKeysFromKeyProtectorError::EgressDekRsaUnwrap)?; + + if rsa_unwrapped_key.len() != AES_GCM_KEY_LENGTH { + return Err(GetKeysFromKeyProtectorError::InvalidRsaUnwrapOutputSize { + output_size: rsa_unwrapped_key.len(), + expected_size: AES_GCM_KEY_LENGTH, + }); + } + + rsa_unwrapped_key }; let mut key = [0u8; AES_GCM_KEY_LENGTH]; key[..old_egress_key.len()].copy_from_slice(&old_egress_key);