From 4c7a33ca7c48155b5e2be89a3a37e411641e8261 Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Tue, 22 Sep 2026 12:20:00 +0200 Subject: [PATCH] fix(migtd): bind local quote to generated TD report Verify that locally generated quote data matches the TD report supplied to the quote service before using it in RA-TLS or SPDM flows. Cover matching, mismatched, and malformed evidence with focused tests while preserving explicit test-only bypass modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Stanislaw Grams --- src/migtd/src/quote.rs | 158 +++++++++++++++++++++++++++ src/migtd/src/ratls/server_client.rs | 9 +- src/migtd/src/spdm/mod.rs | 9 +- 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/migtd/src/quote.rs b/src/migtd/src/quote.rs index 5792b8fc1..98097a7e0 100644 --- a/src/migtd/src/quote.rs +++ b/src/migtd/src/quote.rs @@ -6,6 +6,8 @@ #![cfg(feature = "attestation")] use alloc::vec::Vec; +use core::mem::size_of; +use tdx_tdcall::tdreport::TdxReport; #[cfg(not(feature = "AzCVMEmu"))] use tdx_tdcall::tdreport::tdcall_report; @@ -34,6 +36,71 @@ pub enum QuoteError { ReportGenerationFailed, /// Quote generation failed after all retry attempts QuoteGenerationFailed, + /// Quote verification failed + QuoteVerificationFailed, + /// The verified quote does not describe the TD REPORT used to request it + QuoteReportMismatch, +} + +const VERIFIED_TEE_TCB_SVN: core::ops::Range = 0..16; +const VERIFIED_MRSEAM: core::ops::Range = 16..64; +const VERIFIED_MRSIGNER_SEAM: core::ops::Range = 64..112; +const VERIFIED_SEAM_ATTRIBUTES: core::ops::Range = 112..120; +const VERIFIED_TD_INFO: core::ops::Range = 120..520; +const VERIFIED_REPORT_DATA: core::ops::Range = 520..584; + +/// Verify a local quote and bind it to the TD REPORT supplied to the quote service. +#[cfg(not(any(feature = "use-mock-quote", feature = "test_disable_ra_and_accept_all")))] +pub fn verify_local_quote(quote: &[u8], report: &[u8]) -> Result<(), QuoteError> { + let verified_report = + attestation::verify_quote(quote).map_err(|_| QuoteError::QuoteVerificationFailed)?; + verify_local_report(&verified_report, report) +} + +/// Verify the static mock quote without binding it to the live TD REPORT. +#[cfg(all( + feature = "use-mock-quote", + not(feature = "test_disable_ra_and_accept_all") +))] +pub fn verify_local_quote(quote: &[u8], _report: &[u8]) -> Result<(), QuoteError> { + attestation::verify_quote(quote).map_err(|_| QuoteError::QuoteVerificationFailed)?; + log::warn!( + "use-mock-quote mode: Skipping local quote binding verification. This is NOT secure for production use.\n" + ); + Ok(()) +} + +/// Skip verification when remote attestation is explicitly disabled for testing. +#[cfg(feature = "test_disable_ra_and_accept_all")] +pub fn verify_local_quote(_quote: &[u8], _report: &[u8]) -> Result<(), QuoteError> { + log::warn!( + "test_disable_ra_and_accept_all mode: Skipping local quote binding verification. This is NOT secure for production use.\n" + ); + Ok(()) +} + +/// Compare quote verification output with the authentic local TD REPORT. +pub fn verify_local_report(verified_report: &[u8], report: &[u8]) -> Result<(), QuoteError> { + if verified_report.len() < VERIFIED_REPORT_DATA.end || report.len() != size_of::() { + return Err(QuoteError::QuoteReportMismatch); + } + + let report = TdxReport::read_from_bytes(report).ok_or(QuoteError::QuoteReportMismatch)?; + let tee_tcb_info = report.tee_tcb_info; + let td_info = report.td_info; + let report_mac = report.report_mac; + + if verified_report[VERIFIED_TEE_TCB_SVN] != tee_tcb_info.tee_tcb_svn + || verified_report[VERIFIED_MRSEAM] != tee_tcb_info.mrseam + || verified_report[VERIFIED_MRSIGNER_SEAM] != tee_tcb_info.mrsigner_seam + || verified_report[VERIFIED_SEAM_ATTRIBUTES] != tee_tcb_info.attributes + || verified_report[VERIFIED_TD_INFO] != td_info.as_bytes()[..400] + || verified_report[VERIFIED_REPORT_DATA] != report_mac.report_data + { + return Err(QuoteError::QuoteReportMismatch); + } + + Ok(()) } /// Get a quote with retry logic to handle transient and retriable errors @@ -114,3 +181,94 @@ fn delay_milliseconds(ms: u64) { disable(); } } + +#[cfg(test)] +mod tests { + use super::*; + + fn matching_evidence() -> (Vec, Vec) { + let zeroed = vec![0u8; size_of::()]; + let mut report = TdxReport::read_from_bytes(&zeroed).unwrap(); + report.report_mac.report_data = [0x11; 64]; + report.tee_tcb_info.tee_tcb_svn = [0x20; 16]; + report.tee_tcb_info.mrseam = [0x22; 48]; + report.tee_tcb_info.mrsigner_seam = [0x33; 48]; + report.tee_tcb_info.attributes = [0x44; 8]; + + let mut td_info = report.td_info; + td_info.attributes = [0x51; 8]; + td_info.xfam = [0x52; 8]; + td_info.mrtd = [0x53; 48]; + td_info.mrconfig_id = [0x54; 48]; + td_info.mrowner = [0x55; 48]; + td_info.mrownerconfig = [0x56; 48]; + td_info.rtmr0 = [0x57; 48]; + td_info.rtmr1 = [0x58; 48]; + td_info.rtmr2 = [0x59; 48]; + td_info.rtmr3 = [0x5a; 48]; + report.td_info = td_info; + + let mut verified = vec![0u8; attestation::TD_VERIFIED_REPORT_SIZE]; + verified[VERIFIED_TEE_TCB_SVN].fill(0x20); + verified[VERIFIED_REPORT_DATA].fill(0x11); + verified[VERIFIED_MRSEAM].fill(0x22); + verified[VERIFIED_MRSIGNER_SEAM].fill(0x33); + verified[VERIFIED_SEAM_ATTRIBUTES].fill(0x44); + verified[VERIFIED_TD_INFO].copy_from_slice(&td_info.as_bytes()[..400]); + (verified, report.as_bytes().to_vec()) + } + + #[test] + fn accepts_quote_for_authentic_report() { + let (verified, report) = matching_evidence(); + assert!(verify_local_report(&verified, &report).is_ok()); + } + + #[test] + fn rejects_substituted_td_identity() { + let (mut verified, report) = matching_evidence(); + verified[VERIFIED_TD_INFO.start + 16] ^= 1; + assert!(matches!( + verify_local_report(&verified, &report), + Err(QuoteError::QuoteReportMismatch) + )); + } + + #[test] + fn rejects_substituted_tdx_module_identity() { + let (mut verified, report) = matching_evidence(); + verified[VERIFIED_TEE_TCB_SVN.start] ^= 1; + assert!(matches!( + verify_local_report(&verified, &report), + Err(QuoteError::QuoteReportMismatch) + )); + } + + #[test] + fn rejects_substituted_report_data() { + let (mut verified, report) = matching_evidence(); + verified[VERIFIED_REPORT_DATA.start] ^= 1; + assert!(matches!( + verify_local_report(&verified, &report), + Err(QuoteError::QuoteReportMismatch) + )); + } + + #[test] + fn rejects_malformed_evidence() { + let (verified, report) = matching_evidence(); + assert!(verify_local_report(&verified[..583], &report).is_err()); + assert!(verify_local_report(&verified, &report[..report.len() - 1]).is_err()); + } + + #[test] + fn ignores_td_info_fields_not_present_in_quote() { + let (verified, report) = matching_evidence(); + let mut parsed = TdxReport::read_from_bytes(&report).unwrap(); + let mut td_info = parsed.td_info; + td_info.servtd_hash = [0xaa; 48]; + td_info.reserved = [0xbb; 64]; + parsed.td_info = td_info; + assert!(verify_local_report(&verified, parsed.as_bytes()).is_ok()); + } +} diff --git a/src/migtd/src/ratls/server_client.rs b/src/migtd/src/ratls/server_client.rs index 2371dd29b..81384e304 100644 --- a/src/migtd/src/ratls/server_client.rs +++ b/src/migtd/src/ratls/server_client.rs @@ -228,10 +228,17 @@ fn prepare_report_data(public_key: &[u8]) -> Result<[u8; 64]> { fn gen_quote(public_key: &[u8]) -> Result> { let additional_data = prepare_report_data(public_key)?; - let (quote, _report) = crate::quote::get_quote_with_retry(&additional_data).map_err(|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 })?; + crate::quote::verify_local_quote("e, &report).map_err(|e| { + log::error!( + "Local quote does not match the generated TD report: {:?}\n", + e + ); + RatlsError::VerifyQuote + })?; Ok(quote) } diff --git a/src/migtd/src/spdm/mod.rs b/src/migtd/src/spdm/mod.rs index cba83ed00..cdd5b7bd5 100644 --- a/src/migtd/src/spdm/mod.rs +++ b/src/migtd/src/spdm/mod.rs @@ -156,10 +156,17 @@ pub fn gen_quote_spdm(report_data: &[u8]) -> Result, MigrationResult> { let mut additional_data = [0u8; 64]; additional_data[..hash.len()].copy_from_slice(hash.as_ref()); - let (quote, _report) = crate::quote::get_quote_with_retry(&additional_data).map_err(|e| { + 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 })?; + crate::quote::verify_local_quote("e, &report).map_err(|e| { + log::error!( + "Local quote does not match the generated TD report: {:?}\n", + e + ); + MigrationResult::MutualAttestationError + })?; Ok(quote) }