From 97847abdad3102baa75abe5b94484443034c66c3 Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Sun, 8 Mar 2026 23:35:33 -0700 Subject: [PATCH 1/3] MigTD: add retry for quote generation Add new quote module (src/migtd/src/quote.rs) that centralizes TD quote generation with exponential backoff retry (5s initial, up to 5 retries). This handles the race where an impactless security update invalidates a TD REPORT generated before the update then sent for quote generation. Replace direct attestation::get_quote + tdcall_report calls with quote::get_quote_with_retry in three call sites: - mig_policy.rs: local TCB info initialization - ratls/server_client.rs: RA-TLS quote generation - spdm/mod.rs: SPDM quote generation, also changed error return to MigrationAttestationError for failed quote generation, consistent with RA-TLS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- src/migtd/src/lib.rs | 1 + src/migtd/src/mig_policy.rs | 4 +- src/migtd/src/quote.rs | 114 +++++++++++++++++++++++++++ src/migtd/src/ratls/server_client.rs | 18 +++-- src/migtd/src/spdm/mod.rs | 10 ++- 5 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 src/migtd/src/quote.rs diff --git a/src/migtd/src/lib.rs b/src/migtd/src/lib.rs index e7a3a894..1aaad0c0 100644 --- a/src/migtd/src/lib.rs +++ b/src/migtd/src/lib.rs @@ -39,6 +39,7 @@ pub mod driver; pub mod event_log; pub mod mig_policy; pub mod migration; +pub mod quote; pub mod ratls; pub mod spdm; diff --git a/src/migtd/src/mig_policy.rs b/src/migtd/src/mig_policy.rs index f542bdc9..5c6458bf 100644 --- a/src/migtd/src/mig_policy.rs +++ b/src/migtd/src/mig_policy.rs @@ -119,9 +119,7 @@ mod v2 { LOCAL_TCB_INFO .try_call_once(|| { let policy = get_verified_policy().ok_or(PolicyError::InvalidParameter)?; - let tdx_report = tdx_tdcall::tdreport::tdcall_report(&[0u8; 64]) - .map_err(|_| PolicyError::GetTdxReport)?; - let quote = attestation::get_quote(tdx_report.as_bytes()) + let (quote, _report) = crate::quote::get_quote_with_retry(&[0u8; 64]) .map_err(|_| PolicyError::QuoteGeneration)?; let (fmspc, suppl_data) = verify_quote("e, policy.get_collaterals())?; setup_evaluation_data(fmspc, &suppl_data, policy, policy.get_collaterals()) diff --git a/src/migtd/src/quote.rs b/src/migtd/src/quote.rs new file mode 100644 index 00000000..12dce2e9 --- /dev/null +++ b/src/migtd/src/quote.rs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation +// +// SPDX-License-Identifier: BSD-2-Clause-Patent + +//! Quote generation with retry logic for handling security updates +//! +//! This module provides a resilient GetQuote flow that can handle impactless security +//! updates. If an update happens after the REPORT is retrieved but before the QUOTE +//! is generated, the Quoting Enclave may reject the REPORT. This module handles +//! such scenarios with simple exponential backoff retry. + +#![cfg(feature = "attestation")] + +use alloc::vec::Vec; + +#[cfg(not(feature = "AzCVMEmu"))] +use tdx_tdcall::tdreport::tdcall_report; + +#[cfg(feature = "AzCVMEmu")] +use tdx_tdcall_emu::tdreport::tdcall_report; + +/// Initial retry delay in milliseconds (5 seconds) +const INITIAL_DELAY_MS: u64 = 5000; + +/// Maximum number of attempts before giving up +const MAX_ATTEMPTS: u32 = 6; // Total wait time up to ~2.5 minutes with 5s initial delay + +/// Error type for quote generation with retry +#[derive(Debug)] +pub enum QuoteError { + /// Failed to generate TD report + ReportGenerationFailed, + /// Quote generation failed after all retry attempts + QuoteGenerationFailed, +} + +/// Get a quote with retry logic to handle potential security updates +/// +/// On quote failure, fetches a new TD REPORT and retries with exponential backoff. +/// +/// # Arguments +/// * `additional_data` - The 64-byte additional data to include in the TD REPORT +/// +/// # Returns +/// * `Ok((quote, report))` - The generated quote and the TD REPORT used +/// * `Err(QuoteError)` - If TD report/quote generation fails +pub fn get_quote_with_retry(additional_data: &[u8; 64]) -> Result<(Vec, Vec), QuoteError> { + let mut delay_ms = INITIAL_DELAY_MS; + + for attempt in 1..=MAX_ATTEMPTS { + // Get TD REPORT + let current_report = tdcall_report(additional_data).map_err(|e| { + log::error!("Failed to get TD report: {:?}\n", e); + QuoteError::ReportGenerationFailed + })?; + + let report_bytes = current_report.as_bytes(); + + // Attempt to get quote + match attestation::get_quote(report_bytes) { + Ok(quote) => { + log::info!("Quote generated successfully\n"); + return Ok((quote, report_bytes.to_vec())); + } + Err(e) => { + if attempt < MAX_ATTEMPTS { + log::warn!( + "GetQuote failed (attempt {}/{}): {:?}, retrying with delay of {}ms\n", + attempt, + MAX_ATTEMPTS, + e, + delay_ms + ); + delay_milliseconds(delay_ms); + delay_ms *= 2; + } else { + log::error!("GetQuote failed after {} attempts: {:?}\n", MAX_ATTEMPTS, e); + return Err(QuoteError::QuoteGenerationFailed); + } + } + } + } + + // Should be unreachable because the final attempt returns above on failure. + Err(QuoteError::QuoteGenerationFailed) +} + +/// Delay for the specified number of milliseconds +#[cfg(feature = "AzCVMEmu")] +fn delay_milliseconds(ms: u64) { + std::thread::sleep(std::time::Duration::from_millis(ms)); +} + +#[cfg(not(feature = "AzCVMEmu"))] +fn delay_milliseconds(ms: u64) { + use crate::driver::ticks::Timer; + use core::future::Future; + use core::pin::Pin; + use core::task::{Context, Poll, Waker}; + use core::time::Duration; + use td_payload::arch::apic::{disable, enable_and_hlt}; + + let mut timer = Timer::after(Duration::from_millis(ms)); + let waker = Waker::noop(); + let mut cx = Context::from_waker(&waker); + + loop { + if let Poll::Ready(()) = Pin::new(&mut timer).poll(&mut cx) { + break; + } + enable_and_hlt(); + disable(); + } +} diff --git a/src/migtd/src/ratls/server_client.rs b/src/migtd/src/ratls/server_client.rs index b805c167..ee36c435 100644 --- a/src/migtd/src/ratls/server_client.rs +++ b/src/migtd/src/ratls/server_client.rs @@ -223,12 +223,20 @@ pub fn client_rebinding( } fn gen_quote(public_key: &[u8]) -> Result> { - let td_report = gen_tdreport(public_key)?; + let hash = digest_sha384(public_key).map_err(|e| { + log::error!("Failed to compute SHA384 digest: {:?}\n", e); + e + })?; + + let mut additional_data = [0u8; 64]; + additional_data[..hash.len()].copy_from_slice(hash.as_ref()); - attestation::get_quote(td_report.as_bytes()).map_err(|e| { - log::error!("Failed to get quote from TD report. Error: {:?}\n", e); + let (quote, _report) = crate::quote::get_quote_with_retry(&additional_data).map_err(|e| { + log::error!("get_quote_with_retry failed: {:?}\n", e); RatlsError::GetQuote - }) + })?; + + Ok(quote) } pub fn gen_tdreport(public_key: &[u8]) -> Result { @@ -819,7 +827,6 @@ mod verify { use crypto::ecdsa::ecdsa_verify; use crypto::{Error as CryptoError, Result as CryptoResult}; use policy::PolicyError; - use tdx_tdcall::tdreport::TdxReport; #[cfg(not(feature = "policy_v2"))] pub fn verify_peer_cert( @@ -1227,6 +1234,7 @@ mod verify { #[cfg(feature = "policy_v2")] fn verify_public_key_with_tdreport(tdreport: &[u8], public_key: &[u8]) -> CryptoResult<()> { + use tdx_tdcall::tdreport::TdxReport; if cfg!(feature = "AzCVMEmu") { // In AzCVMEmu mode, REPORTDATA is constructed differently. // Bypass public key hash check in this development environment. diff --git a/src/migtd/src/spdm/mod.rs b/src/migtd/src/spdm/mod.rs index 84c47c6e..e9dd2041 100644 --- a/src/migtd/src/spdm/mod.rs +++ b/src/migtd/src/spdm/mod.rs @@ -115,11 +115,13 @@ pub fn gen_quote_spdm(report_data: &[u8]) -> Result, MigrationResult> { // Generate the TD Report that contains the public key hash as nonce let mut additional_data = [0u8; 64]; additional_data[..hash.len()].copy_from_slice(hash.as_ref()); - let td_report = tdx_tdcall::tdreport::tdcall_report(&additional_data)?; - let res = - attestation::get_quote(td_report.as_bytes()).map_err(|_| MigrationResult::Unsupported)?; - Ok(res) + let (quote, _report) = crate::quote::get_quote_with_retry(&additional_data).map_err(|e| { + log::error!("get_quote_with_retry failed: {:?}\n", e); + MigrationResult::MutualAttestationError + })?; + + Ok(quote) } /// Verify that the peer's quote contains the expected REPORTDATA. From 923129e8f45a5120d35f87661a9e74ab25ea7f6a Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Wed, 18 Mar 2026 15:06:36 -0700 Subject: [PATCH 2/3] AzCVMEmu: add mock tests for quote retry Also flush log area in more VMCALL handlers so all logs read in time Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml | 1 + .../tdx-tdcall/src/tdreport_emu.rs | 25 +++++++++++++++++++ .../tdx-tdcall/src/tdx_emu.rs | 4 +++ migtdemu.sh | 10 ++++++++ src/migtd/Cargo.toml | 1 + src/migtd/src/quote.rs | 5 ++++ 6 files changed, 46 insertions(+) diff --git a/deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml b/deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml index 41db8e7e..f3ad79c2 100644 --- a/deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml +++ b/deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml @@ -32,6 +32,7 @@ interrupt-emu = { path = "../interrupt-emu", package = "interrupt-emu" } default = [] test_mock_report = ["tdx-mock-data"] mock_report_tools = ["tdx-mock-data"] +mock_quote_retry = ["test_mock_report"] [lib] name = "tdx_tdcall_emu" diff --git a/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdreport_emu.rs b/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdreport_emu.rs index c0995f41..3b94486e 100644 --- a/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdreport_emu.rs +++ b/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdreport_emu.rs @@ -136,6 +136,31 @@ pub fn tdcall_report_emulated(additional_data: &[u8; 64]) -> Result Result, QuoteError> { + // When mock_quote_retry is enabled, fail the first N calls to exercise retry logic + #[cfg(feature = "mock_quote_retry")] + { + use core::sync::atomic::{AtomicU32, Ordering}; + + /// Number of times get_quote should fail before succeeding. + const FAIL_COUNT: u32 = 8; + + static CALL_COUNT: AtomicU32 = AtomicU32::new(0); + + let count = CALL_COUNT.fetch_add(1, Ordering::SeqCst); + if count < FAIL_COUNT { + log::warn!( + "mock_quote_retry: Simulating quote failure ({}/{})", + count + 1, + FAIL_COUNT + ); + return Err(QuoteError::ImdsError); + } + log::info!( + "mock_quote_retry: Returning mock quote on attempt {}", + count + 1 + ); + } + debug!("Using mock quote for test_mock_report feature"); Ok(create_mock_quote(td_report_data)) } diff --git a/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs b/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs index ac496901..4cb88c25 100644 --- a/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs +++ b/deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs @@ -560,6 +560,8 @@ pub fn tdvmcall_migtd_waitforrequest( data_buffer: &mut [u8], interrupt: u8, ) -> Result<(), TdVmcallError> { + crate::logging_emu::read_log_entries(); + // data_buffer uses the GHCI 1.5 buffer format: // Bytes 0-7: status (u64) - filled by VMM/emulation // byte[0] = 1 (TDX_VMCALL_VMM_SUCCESS) @@ -1108,6 +1110,7 @@ pub fn tdcall_servtd_rebind_approve( pub fn tdvmcall_get_quote(buffer: &mut [u8]) -> Result<(), original_tdx_tdcall::TdVmcallError> { use original_tdx_tdcall::TdVmcallError; + crate::logging_emu::read_log_entries(); log::info!("AzCVMEmu: tdvmcall_get_quote emulated"); // TDX GHCI GetQuote buffer format: @@ -1604,6 +1607,7 @@ pub fn tdcall_extend_rtmr( /// Emulation for tdvmcall_setup_event_notify /// In AzCVMEmu mode, we store the vector so tdvmcall_get_quote can trigger it pub fn tdvmcall_setup_event_notify(vector: u64) -> Result<(), original_tdx_tdcall::TdVmcallError> { + crate::logging_emu::read_log_entries(); log::info!( "AzCVMEmu: tdvmcall_setup_event_notify emulated - vector: {}", vector diff --git a/migtdemu.sh b/migtdemu.sh index 52374b69..2482ba4d 100755 --- a/migtdemu.sh +++ b/migtdemu.sh @@ -26,6 +26,7 @@ USE_MOCK_REPORT=false EXTRA_FEATURES="" USE_POLICY_V2=false MOCK_QUOTE_FILE="" # Optional mock quote file path +USE_MOCK_QUOTE_RETRY=false EXPLICIT_POLICY_FILE=false EXPLICIT_POLICY_ISSUER_CHAIN=false DEFAULT_RUST_BACKTRACE="1" @@ -61,6 +62,7 @@ show_usage() { echo " --skip-ra Skip remote attestation (uses mock TD reports/quotes for non-TDX environments)" echo " --mock-report Use mock report data for RA and policy v2 (non-TDX, but full attestation flow)" echo " --mock-quote-file FILE Path to mock quote file (used with --mock-report, defaults to output_data_v4.bin)" + echo " --mock-quote-retry Enable mock_quote_retry feature (get_quote fails first 8 times to test retry logic)" echo " --both Start destination first, then source (same host)" echo " --no-sudo Run without sudo (useful for local testing)" echo " --features FEATURES Add extra cargo features (comma-separated, e.g., 'spdm_attestation,feature2')" @@ -279,6 +281,10 @@ while [[ $# -gt 0 ]]; do MOCK_QUOTE_FILE="$2" shift 2 ;; + --mock-quote-retry) + USE_MOCK_QUOTE_RETRY=true + shift + ;; --both) RUN_BOTH=true shift @@ -446,6 +452,10 @@ build_features_string() { features="$features,test_mock_report" fi + if [[ "$USE_MOCK_QUOTE_RETRY" == true ]]; then + features="$features,mock_quote_retry" + fi + if [[ -n "$EXTRA_FEATURES" ]]; then features="$features,$EXTRA_FEATURES" fi diff --git a/src/migtd/Cargo.toml b/src/migtd/Cargo.toml index e7786718..b25e942b 100644 --- a/src/migtd/Cargo.toml +++ b/src/migtd/Cargo.toml @@ -85,6 +85,7 @@ test_stack_size = ["td-benchmark"] test_disable_ra_and_accept_all = ["attestation/test", "tdx-tdcall-emu?/test_mock_report"] # Dangerous: can only be used for test purpose to bypass the remote attestation spdm_attestation = ["main"] test_mock_report = ["AzCVMEmu", "tdx-tdcall-emu/test_mock_report"] +mock_quote_retry = ["AzCVMEmu", "tdx-tdcall-emu/mock_quote_retry"] AzCVMEmu = [ "main", # Include main feature by default "attestation/AzCVMEmu", diff --git a/src/migtd/src/quote.rs b/src/migtd/src/quote.rs index 12dce2e9..4f1878bf 100644 --- a/src/migtd/src/quote.rs +++ b/src/migtd/src/quote.rs @@ -20,8 +20,13 @@ use tdx_tdcall::tdreport::tdcall_report; use tdx_tdcall_emu::tdreport::tdcall_report; /// Initial retry delay in milliseconds (5 seconds) +#[cfg(not(feature = "AzCVMEmu"))] const INITIAL_DELAY_MS: u64 = 5000; +//shorter for testing +#[cfg(feature = "AzCVMEmu")] +const INITIAL_DELAY_MS: u64 = 20; + /// Maximum number of attempts before giving up const MAX_ATTEMPTS: u32 = 6; // Total wait time up to ~2.5 minutes with 5s initial delay From 2bdfb2756fcf9448a435d7d31609e898bc9c5a9d Mon Sep 17 00:00:00 2001 From: Haitao Huang Date: Fri, 20 Mar 2026 11:31:49 -0700 Subject: [PATCH 3/3] ci: add mock quote retry test to integration-emu matrix Adds a new 'Mock Quote Retry' test that exercises the quote retry logic by enabling mock_quote_retry feature. The test verifies: - Migration completes successfully after 8 simulated quote failures - Both source and destination logs contain exactly 8 retry failures and at least 1 success message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Haitao Huang --- .github/workflows/integration-emu.yml | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/integration-emu.yml b/.github/workflows/integration-emu.yml index 280f4b4c..777cc09a 100644 --- a/.github/workflows/integration-emu.yml +++ b/.github/workflows/integration-emu.yml @@ -85,6 +85,13 @@ jobs: test-command: "./migtdemu.sh --operation rebind-prepare --policy-file ./config/AzCVMEmu/policy_v2_signed.json --policy-issuer-chain-file ./config/AzCVMEmu/policy_issuer_chain.pem --skip-ra --features spdm_attestation --both --no-sudo --log-level info" artifact-name: "spdm-rebind-skip-ra-test-logs" + - test-name: "Mock Quote Retry" + test-type: "mock-quote-retry" + install-jq: 'false' + timeout-seconds: 900 + test-command: "./migtdemu.sh --mock-report --mock-quote-retry --both --no-sudo --log-level info" + artifact-name: "mock-quote-retry-test-logs" + steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 @@ -218,6 +225,38 @@ jobs: fi fi + # Verify retry messages for mock-quote-retry test + if [[ "${{ matrix.test-type }}" == "mock-quote-retry" ]]; then + echo "" + echo "=== Verifying quote retry logs ===" + FAIL=0 + for LOG in migtd_migration_source.log migtd_migration_destination.log; do + if [[ ! -f "$LOG" ]]; then + echo "❌ Missing log file: $LOG" + FAIL=1 + continue + fi + RETRY_COUNT=$(grep -ac "Simulating quote failure" "$LOG" || true) + SUCCESS_COUNT=$(grep -ac "Quote generated successfully" "$LOG" || true) + echo "$LOG: $RETRY_COUNT retry failures, $SUCCESS_COUNT successes" + if [[ "$RETRY_COUNT" -ne 8 ]]; then + echo "❌ Expected 8 retry failures in $LOG, got $RETRY_COUNT" + FAIL=1 + fi + if [[ "$SUCCESS_COUNT" -lt 1 ]]; then + echo "❌ Expected at least 1 success in $LOG" + FAIL=1 + fi + done + if [[ "$FAIL" -eq 1 ]]; then + echo "" + echo "Dumping retry-related log lines:" + grep -a "retry\|Simulating\|Quote generated\|GetQuote failed" migtd_migration_source.log migtd_migration_destination.log || true + exit 1 + fi + echo "✅ Quote retry verification passed (8 failures + success on both sides)" + fi + - name: Upload test artifacts on failure if: failure() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0