Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/integration-emu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions deps/td-shim-AzCVMEmu/tdx-tdcall/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdreport_emu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,31 @@ pub fn tdcall_report_emulated(additional_data: &[u8; 64]) -> Result<tdx::TdRepor
/// Emulated quote generation using mock quote
#[cfg(feature = "test_mock_report")]
pub fn get_quote_emulated(td_report_data: &[u8]) -> Result<Vec<u8>, 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))
}
Expand Down
4 changes: 4 additions & 0 deletions deps/td-shim-AzCVMEmu/tdx-tdcall/src/tdx_emu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions migtdemu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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')"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/migtd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/migtd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 1 addition & 3 deletions src/migtd/src/mig_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&quote, policy.get_collaterals())?;
setup_evaluation_data(fmspc, &suppl_data, policy, policy.get_collaterals())
Expand Down
119 changes: 119 additions & 0 deletions src/migtd/src/quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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)
#[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

/// 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<u8>, Vec<u8>), 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();
}
}
18 changes: 13 additions & 5 deletions src/migtd/src/ratls/server_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,20 @@ pub fn client_rebinding<T: AsyncRead + AsyncWrite + Unpin>(
}

fn gen_quote(public_key: &[u8]) -> Result<Vec<u8>> {
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<TdxReport> {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions src/migtd/src/spdm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,13 @@ pub fn gen_quote_spdm(report_data: &[u8]) -> Result<Vec<u8>, 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.
Expand Down
Loading