Skip to content
8 changes: 6 additions & 2 deletions openhcl/openhcl_attestation_protocol/src/igvm_attest/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,14 @@ impl IgvmAttestRequestHeader {
/// Bitmap of additional Igvm request attributes.
/// 0 - error_code: Requesting IGVM Agent Error code
/// 1 - retry: Retry preference
/// 2 - skip_hw_unsealing: Skip hardware unsealing in case key release request fails
#[bitfield(u32)]
#[derive(IntoBytes, FromBytes, Immutable, KnownLayout)]
pub struct IgvmCapabilityBitMap {
pub error_code: bool,
pub retry: bool,
#[bits(30)]
pub skip_hw_unsealing: bool,
#[bits(29)]
_reserved: u32,
}

Expand Down Expand Up @@ -229,11 +231,13 @@ impl IgvmAttestRequestDataExt {

/// Bitmap indicates a signal to requestor
/// 0 - IGVM_SIGNAL_RETRY_RCOMMENDED_BIT: Retry recommendation
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing typo in the doc comment: IGVM_SIGNAL_RETRY_RCOMMENDED_BIT should be IGVM_SIGNAL_RETRY_RECOMMENDED_BIT. Since you're modifying the doc block (adding line 234), it would be good to fix this while you're here.

Suggested change
/// 0 - IGVM_SIGNAL_RETRY_RCOMMENDED_BIT: Retry recommendation
/// 0 - IGVM_SIGNAL_RETRY_RECOMMENDED_BIT: Retry recommendation

Copilot uses AI. Check for mistakes.
/// 1 - IGVM_SIGNAL_SKIP_HW_UNSEALING_RECOMMENDED_BIT: Skip hardware unsealing
#[bitfield(u32)]
#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
pub struct IgvmSignal {
pub retry: bool,
#[bits(31)]
pub skip_hw_unsealing: bool,
#[bits(30)]
_reserved: u32,
}

Expand Down
62 changes: 58 additions & 4 deletions openhcl/underhill_attestation/src/igvm_attest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ pub enum Error {
latest_version: IgvmAttestResponseVersion,
},
#[error(
"attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal})"
"attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal}), skip hw unsealing ({skip_hw_unsealing})"
)]
Attestation {
igvm_error_code: u32,
http_status_code: u32,
retry_signal: bool,
skip_hw_unsealing: bool,
},
}

Expand Down Expand Up @@ -247,6 +248,7 @@ pub fn parse_response_header(response: &[u8]) -> Result<IgvmAttestCommonResponse
igvm_error_code: igvm_error_info.error_code,
http_status_code: igvm_error_info.http_status_code,
retry_signal: igvm_error_info.igvm_signal.retry(),
skip_hw_unsealing: igvm_error_info.igvm_signal.skip_hw_unsealing(),
})?
}
}
Expand Down Expand Up @@ -310,7 +312,8 @@ fn create_request(
if include_extension {
let capability_bitmap = IgvmCapabilityBitMap::new()
.with_error_code(true)
.with_retry(true);
.with_retry(true)
.with_skip_hw_unsealing(true);
let ext = IgvmAttestRequestDataExt::new(capability_bitmap);
buffer.extend_from_slice(ext.as_bytes());
}
Expand Down Expand Up @@ -458,6 +461,7 @@ mod tests {
.expect("parse IgvmAttestRequestDataExt");
assert!(ext.capability_bitmap.error_code());
assert!(ext.capability_bitmap.retry());
assert!(ext.capability_bitmap.skip_hw_unsealing());

assert_eq!(
buffer.len(),
Expand Down Expand Up @@ -702,7 +706,8 @@ mod tests {
Error::Attestation {
igvm_error_code: 1103,
http_status_code: 403,
retry_signal: true
retry_signal: true,
skip_hw_unsealing: false
}
.to_string()
);
Expand All @@ -724,7 +729,56 @@ mod tests {
Error::Attestation {
igvm_error_code: 1103,
http_status_code: 503,
retry_signal: false
retry_signal: false,
skip_hw_unsealing: false
}
.to_string()
);
}

#[test]
fn test_failed_response_with_skip_hw_unsealing_signal() {
// error_code: 1103 (0x44f), http_status_code: 400 (0x190),
// igvm_signal: retry=true, skip_hw_unsealing=true (0x03 = bits 0 and 1 set)
const INVALID_RESPONSE: [u8; 42] = [
0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x90, 0x01,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
];

let result = parse_response_header(&INVALID_RESPONSE);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
Error::Attestation {
igvm_error_code: 1103,
http_status_code: 400,
retry_signal: true,
skip_hw_unsealing: true
}
.to_string()
);
}

#[test]
fn test_failed_response_with_skip_hw_unsealing_only() {
// error_code: 1103 (0x44f), http_status_code: 400 (0x190),
// igvm_signal: retry=false, skip_hw_unsealing=true (0x02 = bit 1 set)
const INVALID_RESPONSE: [u8; 42] = [
0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x90, 0x01,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
];

let result = parse_response_header(&INVALID_RESPONSE);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
Error::Attestation {
igvm_error_code: 1103,
http_status_code: 400,
retry_signal: false,
skip_hw_unsealing: true
}
.to_string()
);
Expand Down
126 changes: 120 additions & 6 deletions openhcl/underhill_attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ enum GetDerivedKeysError {
DeriveIngressKey(#[source] crypto::KbkdfError),
#[error("failed to derive an egress key")]
DeriveEgressKey(#[source] crypto::KbkdfError),
#[error("skipped hardware unsealing for VMGS DEK as signaled by IGVM agent")]
HardwareUnsealingSkipped,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -276,11 +278,26 @@ async fn try_unlock_vmgs(
Ok(VmgsEncryptionKeys::default())
};

let retry = match skr_response {
let retry = match &skr_response {
Ok(_) => false,
Err((_, r)) => r,
Err((_, r)) => *r,
};

let skip_hw_unsealing = matches!(
&skr_response,
Err((
secure_key_release::RequestVmgsEncryptionKeysError::ParseIgvmAttestKeyReleaseResponse(
igvm_attest::key_release::KeyReleaseError::ParseHeader(
igvm_attest::Error::Attestation {
skip_hw_unsealing: true,
..
},
),
),
_,
))
);

let VmgsEncryptionKeys {
ingress_rsa_kek,
wrapped_des_key,
Expand Down Expand Up @@ -342,6 +359,7 @@ async fn try_unlock_vmgs(
tcb_version,
guest_state_encryption_policy,
strict_encryption_policy,
skip_hw_unsealing,
)
.await
.map_err(|e| {
Expand Down Expand Up @@ -683,6 +701,7 @@ async fn get_derived_keys(
tcb_version: Option<u64>,
guest_state_encryption_policy: GuestStateEncryptionPolicy,
strict_encryption_policy: bool,
skip_hw_unsealing: bool,
) -> Result<DerivedKeyResult, GetDerivedKeysError> {
tracing::info!(
CVM_ALLOWED,
Expand Down Expand Up @@ -866,7 +885,18 @@ async fn get_derived_keys(
// If sources of encryption used last are missing, attempt to unseal VMGS key with hardware key
if (no_kek && found_dek) || (no_gsp && requires_gsp) || (no_gsp_by_id && requires_gsp_by_id) {
// If possible, get ingressKey from hardware sealed data
let (hardware_key_protector, hardware_derived_keys) = if let Some(tee_call) = tee_call {
let (hardware_key_protector, hardware_derived_keys) = if skip_hw_unsealing {
tracing::warn!(
CVM_ALLOWED,
"Skipping hardware unsealing of VMGS DEK as signaled by IGVM agent"
);
get.event_log_fatal(
guest_emulation_transport::api::EventLogId::DEK_HARDWARE_UNSEALING_SKIPPED,
)
.await;

return Err(GetDerivedKeysError::HardwareUnsealingSkipped);
} else if let Some(tee_call) = tee_call {
let hardware_key_protector = match vmgs::read_hardware_key_protector(vmgs).await {
Ok(hardware_key_protector) => Some(hardware_key_protector),
Err(e) => {
Expand Down Expand Up @@ -932,12 +962,12 @@ async fn get_derived_keys(
});
} else {
if no_kek && found_dek {
Err(GetDerivedKeysError::GetIngressKeyFromKpFailed)?
return Err(GetDerivedKeysError::GetIngressKeyFromKpFailed);
} else if no_gsp && requires_gsp {
Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed)?
return Err(GetDerivedKeysError::GetIngressKeyFromKGspFailed);
} else {
// no_gsp_by_id && requires_gsp_by_id
Err(GetDerivedKeysError::GetIngressKeyFromKGspByIdFailed)?
return Err(GetDerivedKeysError::GetIngressKeyFromKGspByIdFailed);
}
}
}
Expand Down Expand Up @@ -2563,6 +2593,90 @@ mod tests {
assert!(vmgs.encrypted());
}

#[async_test]
async fn init_sec_secure_key_release_skip_hw_unsealing(driver: DefaultDriver) {
let mut vmgs = new_formatted_vmgs().await;

// IGVM attest is required
// KEY_RELEASE succeeds on first boot, fails with skip_hw_unsealing on second boot.
// WRAPPED_KEY is not in the plan, so it falls back to default (success) every time.
let mut plan = IgvmAgentTestPlan::default();
plan.insert(
IgvmAttestRequestType::KEY_RELEASE_REQUEST,
VecDeque::from([
IgvmAgentAction::RespondSuccess,
IgvmAgentAction::RespondFailureSkipHwUnsealing,
]),
);

let get_pair = new_test_get(driver, true, Some(plan)).await;

let bios_guid = Guid::new_random();
let att_cfg = new_attestation_vm_config();

// Ensure VMGS is not encrypted and agent data is empty before the call
assert!(!vmgs.encrypted());

// Obtain a LocalDriver briefly, then run the async flow under the pool executor
let tee = MockTeeCall::new(0x1234);
let ldriver = pal_async::local::block_with_io(|ld| async move { ld });
let res = initialize_platform_security(
&get_pair.client,
bios_guid,
&att_cfg,
&mut vmgs,
Some(&tee),
false,
ldriver.clone(),
GuestStateEncryptionPolicy::Auto,
true,
)
.await
.unwrap();

// VMGS is now encrypted and HWKP is updated.
assert!(vmgs.encrypted());
assert!(!hardware_key_protector_is_empty(&mut vmgs).await);
// Agent data should be the same as `key_reference` in the WRAPPED_KEY response.
let key_reference = serde_json::json!({
"key_info": {
"host": "name"
},
"attestation_info": {
"host": "attestation_name"
}
});
let key_reference = serde_json::to_string(&key_reference).unwrap();
let key_reference = key_reference.as_bytes();
let mut expected_agent_data = [0u8; AGENT_DATA_MAX_SIZE];
expected_agent_data[..key_reference.len()].copy_from_slice(key_reference);
assert_eq!(res.agent_data.unwrap(), expected_agent_data.to_vec());
// Secure key should be None without pre-provisioning
assert!(res.guest_secret_key.is_none());

// Second call: KEY_RELEASE fails with skip_hw_unsealing signal.
// The skip_hw_unsealing signal causes the hardware unsealing fallback to be
// skipped, so VMGS unlock should fail.
// NOTE: The test relies on the test GED to return failing KEY_RELEASE response
// with retry recommendation as false so the retry loop terminates immediately.
// Otherwise, the test will get stuck on timer.sleep() as the driver is not
// progressed.
let result = initialize_platform_security(
&get_pair.client,
bios_guid,
&att_cfg,
&mut vmgs,
Some(&tee),
false,
ldriver,
GuestStateEncryptionPolicy::Auto,
true,
)
.await;

assert!(result.is_err());
}

#[async_test]
async fn init_sec_secure_key_release_no_hw_sealing_backup(driver: DefaultDriver) {
let mut vmgs = new_formatted_vmgs().await;
Expand Down
3 changes: 3 additions & 0 deletions openhcl/underhill_attestation/src/secure_key_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ pub async fn request_vmgs_encryption_keys(
igvm_error_code,
http_status_code,
retry_signal,
..
},
),
),
Expand All @@ -176,6 +177,7 @@ pub async fn request_vmgs_encryption_keys(
igvm_error_code,
http_status_code,
retry_signal,
skip_hw_unsealing,
},
),
),
Expand All @@ -185,6 +187,7 @@ pub async fn request_vmgs_encryption_keys(
igvm_error_code = &igvm_error_code,
igvm_http_status_code = &http_status_code,
retry_signal = &retry_signal,
skip_hw_unsealing = &skip_hw_unsealing,
error = &key_release_attest_error as &dyn std::error::Error,
"VMGS key-encryption failed due to igvm attest error"
);
Expand Down
1 change: 1 addition & 0 deletions vm/devices/get/get_protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ open_enum! {
TPM_INVALID_STATE = 17,
TPM_IDENTITY_CHANGE_FAILED = 18,
WRAPPED_KEY_REQUIRED_BUT_INVALID = 19,
DEK_HARDWARE_UNSEALING_SKIPPED = 23,
}
}

Expand Down
13 changes: 13 additions & 0 deletions vm/devices/get/get_resources/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,20 @@ pub mod ged {
pub enum IgvmAttestTestConfig {
/// Config for testing AK cert retry after failure.
AkCertRequestFailureAndRetry,
/// Config for testing AK cert retry after failure — extended plan.
///
/// Windows guests with CVM isolation (SNP, TDX) or VBS generate
/// additional boot-time AK cert requests (background retries
/// during the initial boot and the initial_reboot) that consume
/// plan actions before the test code runs. This plan has extra
/// failure actions to absorb those requests.
AkCertRequestFailureAndRetryExtended,
Comment on lines +253 to +260
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this extended plan is for "Windows guests with CVM isolation (SNP, TDX) or VBS", but config_for_vm_name also maps ubuntu_2504_server_x64_tdx_ak_cert_retry (a Linux TDX guest) to AkCertRequestFailureAndRetryExtended. Either the doc comment should be broadened (e.g., "CVM guests (SNP, TDX) or Windows VBS guests") or the mapping is incorrect.

Copilot uses AI. Check for mistakes.
/// Config for testing AK cert persistency across boots.
AkCertPersistentAcrossBoot,
/// Config for testing skip hardware unsealing signal from IGVMAgent.
KeyReleaseFailureSkipHwUnsealing,
/// Config for testing key release failure without skip_hw_unsealing
/// signal — hardware unsealing fallback should be attempted.
KeyReleaseFailure,
}
}
2 changes: 1 addition & 1 deletion vm/devices/get/guest_emulation_device/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ impl GuestEmulationDevice {
waiting_for_vtl0_start: Vec::new(),
last_save_restore_buf_len: 0,
igvm_agent_setting,
igvm_agent: TestIgvmAgent::new(),
igvm_agent: TestIgvmAgent::new("openvmm"),
test_gsp_by_id,
}
}
Expand Down
Loading
Loading