From eb41328f134c2d7e04c669f204fdf8559c8f2f64 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 29 May 2026 08:42:20 +0200 Subject: [PATCH 1/3] DCAP attestation - bail on non-up-to-date TCB status --- crates/attestation/src/dcap.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index dca2c10..57a23f4 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -193,6 +193,11 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( fmspc, "DCAP verification succeeded with non-UpToDate TCB status" ); + return Err(DcapVerificationError::BadTcbStatus( + verified_report.status, + verified_report.advisory_ids, + fmspc, + )); } let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; @@ -290,6 +295,8 @@ pub enum DcapVerificationError { Pccs(#[from] PccsError), #[error("Timestamp exceeds i64 range")] TimeStampExceedsI64, + #[error("Bad TCB status: {0}, Advisory IDs: {1:?}, FMSPC: {2}")] + BadTcbStatus(String, Vec, String), } #[cfg(test)] From c45cc58dc44e6277506c566c65670f00eb2dfd38 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 1 Jun 2026 10:39:12 +0200 Subject: [PATCH 2/3] Add test to demonstrate failing attestation with non-uptodate TCB --- crates/attestation/src/dcap.rs | 70 +++++++++++++++++++++++++++------ crates/mock-tdx/src/lib.rs | 2 + crates/mock-tdx/src/mock_pcs.rs | 38 ++++++++++++++---- crates/pccs/src/lib.rs | 6 +++ 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 57a23f4..7355b1b 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -5,6 +5,7 @@ use dcap_qvl::{ collateral::get_collateral_for_fmspc, quote::{Quote, Report}, tcb_info::TcbInfo, + verify::VerifiedReport, }; #[cfg(any(test, feature = "mock"))] use mock_tdx::generate_mock_tdx_quote; @@ -186,6 +187,21 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( override_outdated_tcb, )?; + ensure_up_to_date_tcb(verified_report, fmspc)?; + + let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; + + if get_quote_input_data(quote.report) != expected_input_data { + return Err(DcapVerificationError::InputMismatch); + } + + Ok(measurements) +} + +fn ensure_up_to_date_tcb( + verified_report: VerifiedReport, + fmspc: String, +) -> Result<(), DcapVerificationError> { if verified_report.status != "UpToDate" { tracing::warn!( status = %verified_report.status, @@ -199,14 +215,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( fmspc, )); } - - let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - - if get_quote_input_data(quote.report) != expected_input_data { - return Err(DcapVerificationError::InputMismatch); - } - - Ok(measurements) + Ok(()) } #[cfg(any(test, feature = "mock"))] @@ -220,13 +229,17 @@ pub async fn verify_dcap_attestation( let fmspc = hex::encode_upper(quote.fmspc()?); let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let collateral = if let Some(ref pccs) = pccs { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?; + let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; collateral } else { mock_tdx::mock_collateral() }; let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; + let verified_report = + verifier + .dangerous_verify_with_tcb_override(&input, &collateral, now, |tcb_info| tcb_info)?; + + ensure_up_to_date_tcb(verified_report, fmspc)?; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; if get_quote_input_data(quote.report) != expected_input_data { @@ -248,7 +261,11 @@ pub fn verify_dcap_attestation_sync( let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let collateral = pccs.get_collateral_sync(fmspc, ca, now)?; let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; + let verified_report = + verifier + .dangerous_verify_with_tcb_override(&input, &collateral, now, |tcb_info| tcb_info)?; + + ensure_up_to_date_tcb(verified_report, hex::encode_upper(quote.fmspc()?))?; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; if get_quote_input_data(quote.report.clone()) != expected_input_data { @@ -301,6 +318,7 @@ pub enum DcapVerificationError { #[cfg(test)] mod tests { + use dcap_qvl::tcb_info::TcbStatus; use mock_tdx::{MockPcsConfig, spawn_mock_pcs_server}; use super::*; @@ -425,4 +443,34 @@ mod tests { assert_eq!(mock_pcs.tcb_call_count(), 1); assert_eq!(mock_pcs.qe_call_count(), 1); } + + #[tokio::test] + async fn test_mock_dcap_verify_rejects_non_up_to_date_tcb_collateral() { + let mock_pcs = spawn_mock_pcs_server(MockPcsConfig { + include_fmspcs_listing: false, + tcb_status: Some(TcbStatus::OutOfDate), + tcb_advisory_ids: vec!["INTEL-SA-MOCK".to_string()], + ..MockPcsConfig::default() + }) + .await + .unwrap(); + let pccs = Pccs::new_without_prewarm(Some(mock_pcs.base_url.clone())); + let expected_input_data = [0xB6; 64]; + let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap(); + + let err = verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs)) + .await + .unwrap_err(); + + match err { + DcapVerificationError::BadTcbStatus(status, advisory_ids, fmspc) => { + assert_eq!(status, "OutOfDate"); + assert_eq!(advisory_ids, vec!["INTEL-SA-MOCK"]); + assert_eq!(fmspc, "00906EA10000"); + } + other => panic!("unexpected verification error: {other:?}"), + } + assert_eq!(mock_pcs.tcb_call_count(), 1); + assert_eq!(mock_pcs.qe_call_count(), 1); + } } diff --git a/crates/mock-tdx/src/lib.rs b/crates/mock-tdx/src/lib.rs index 50a2989..975f5d4 100644 --- a/crates/mock-tdx/src/lib.rs +++ b/crates/mock-tdx/src/lib.rs @@ -32,6 +32,8 @@ const EMBEDDED_ROOT_CA_DER: &[u8] = include_bytes!("../assets/mock-root-ca.der") const EMBEDDED_PCK_KEY_PEM: &str = include_str!("../assets/mock-pck-key.pem"); /// Embedded PCK chain PEM contents const EMBEDDED_PCK_CHAIN_PEM: &str = include_str!("../assets/mock-pck-chain.pem"); +/// Deterministic TCB signer secret key bytes +pub(crate) const TCB_SIGNER_SK: [u8; 32] = [0x33; 32]; /// Deterministic attestation secret key bytes const ATTESTATION_SK: [u8; 32] = [0x55; 32]; diff --git a/crates/mock-tdx/src/mock_pcs.rs b/crates/mock-tdx/src/mock_pcs.rs index c9fba83..4f407c2 100644 --- a/crates/mock-tdx/src/mock_pcs.rs +++ b/crates/mock-tdx/src/mock_pcs.rs @@ -14,11 +14,11 @@ use axum::{ response::IntoResponse, routing::get, }; -use dcap_qvl::QuoteCollateralV3; +use dcap_qvl::{QuoteCollateralV3, tcb_info::TcbStatus}; use serde_json::{Value, json}; use tokio::{net::TcpListener, task::JoinHandle}; -use crate::mock_collateral; +use crate::{TCB_SIGNER_SK, mock_collateral, sign_raw_p256, signing_key_from_secret}; /// Configuration for a mock PCS server backed by `mock-tdx` collateral #[derive(Clone)] @@ -33,6 +33,10 @@ pub struct MockPcsConfig { pub refreshed_tcb_next_update: Option, /// Optional `nextUpdate` value returned by later QE identity responses pub refreshed_qe_next_update: Option, + /// Optional TCB status to serve for all TCB levels + pub tcb_status: Option, + /// Advisory IDs to serve with the configured TCB status + pub tcb_advisory_ids: Vec, } impl Default for MockPcsConfig { @@ -48,6 +52,8 @@ impl Default for MockPcsConfig { qe_next_update: qe_identity["nextUpdate"].as_str().unwrap().to_string(), refreshed_tcb_next_update: None, refreshed_qe_next_update: None, + tcb_status: None, + tcb_advisory_ids: Vec::new(), } } } @@ -86,12 +92,12 @@ struct MockPcsState { include_fmspcs_listing: bool, base_tcb_info: Value, base_qe_identity: Value, - tcb_signature_hex: String, - qe_signature_hex: String, tcb_next_update: String, qe_next_update: String, refreshed_tcb_next_update: Option, refreshed_qe_next_update: Option, + tcb_status: Option, + tcb_advisory_ids: Vec, pck_crl: Vec, pck_crl_issuer_chain: String, tcb_issuer_chain: String, @@ -120,12 +126,12 @@ pub async fn spawn_mock_pcs_server( include_fmspcs_listing: config.include_fmspcs_listing, base_tcb_info: tcb_info, base_qe_identity: qe_identity, - tcb_signature_hex: hex::encode(&base_collateral.tcb_info_signature), - qe_signature_hex: hex::encode(&base_collateral.qe_identity_signature), tcb_next_update: config.tcb_next_update, qe_next_update: config.qe_next_update, refreshed_tcb_next_update: config.refreshed_tcb_next_update, refreshed_qe_next_update: config.refreshed_qe_next_update, + tcb_status: config.tcb_status, + tcb_advisory_ids: config.tcb_advisory_ids, pck_crl: base_collateral.pck_crl, pck_crl_issuer_chain: urlencoding::encode(&base_collateral.pck_crl_issuer_chain).into(), tcb_issuer_chain: urlencoding::encode(&base_collateral.tcb_info_issuer_chain).into(), @@ -191,11 +197,21 @@ async fn mock_tcb_handler( state.refreshed_tcb_next_update.clone().unwrap_or_else(|| state.tcb_next_update.clone()) }; tcb_info["nextUpdate"] = Value::String(next_update); + if let Some(status) = state.tcb_status && + let Some(tcb_levels) = tcb_info["tcbLevels"].as_array_mut() + { + for tcb_level in tcb_levels { + tcb_level["tcbStatus"] = Value::String(status.to_string()); + tcb_level["advisoryIDs"] = + Value::Array(state.tcb_advisory_ids.iter().cloned().map(Value::String).collect()); + } + } + let tcb_signature_hex = sign_json_value_hex(&tcb_info); ( [("SGX-TCB-Info-Issuer-Chain", state.tcb_issuer_chain.clone())], Json(json!({ "tcbInfo": tcb_info, - "signature": state.tcb_signature_hex, + "signature": tcb_signature_hex, })), ) } @@ -214,11 +230,12 @@ async fn mock_qe_identity_handler( state.refreshed_qe_next_update.clone().unwrap_or_else(|| state.qe_next_update.clone()) }; qe_identity["nextUpdate"] = Value::String(next_update); + let qe_signature_hex = sign_json_value_hex(&qe_identity); ( [("SGX-Enclave-Identity-Issuer-Chain", state.qe_issuer_chain.clone())], Json(json!({ "enclaveIdentity": qe_identity, - "signature": state.qe_signature_hex, + "signature": qe_signature_hex, })), ) } @@ -227,3 +244,8 @@ async fn mock_qe_identity_handler( async fn mock_root_ca_crl_handler(State(state): State>) -> impl IntoResponse { state.root_ca_crl_hex.clone() } + +fn sign_json_value_hex(value: &Value) -> String { + let signing_key = signing_key_from_secret(TCB_SIGNER_SK).expect("valid mock TCB signer key"); + hex::encode(sign_raw_p256(&signing_key, value.to_string().as_bytes())) +} diff --git a/crates/pccs/src/lib.rs b/crates/pccs/src/lib.rs index 774443b..4ea20e5 100644 --- a/crates/pccs/src/lib.rs +++ b/crates/pccs/src/lib.rs @@ -744,6 +744,7 @@ mod tests { qe_next_update: "2999-01-01T00:00:00Z".to_string(), refreshed_tcb_next_update: None, refreshed_qe_next_update: None, + ..MockPcsConfig::default() }) .await .unwrap(); @@ -791,6 +792,7 @@ mod tests { qe_next_update: initial_next_update, refreshed_tcb_next_update: Some(refreshed_next_update.clone()), refreshed_qe_next_update: Some(refreshed_next_update), + ..MockPcsConfig::default() }) .await .unwrap(); @@ -834,6 +836,7 @@ mod tests { qe_next_update: "2999-01-01T00:00:00Z".to_string(), refreshed_tcb_next_update: None, refreshed_qe_next_update: None, + ..MockPcsConfig::default() }) .await .unwrap(); @@ -870,6 +873,7 @@ mod tests { qe_next_update: "2999-01-01T00:00:00Z".to_string(), refreshed_tcb_next_update: None, refreshed_qe_next_update: None, + ..MockPcsConfig::default() }) .await .unwrap(); @@ -907,6 +911,7 @@ mod tests { qe_next_update: "2999-01-01T00:00:00Z".to_string(), refreshed_tcb_next_update: None, refreshed_qe_next_update: None, + ..MockPcsConfig::default() }) .await .unwrap(); @@ -947,6 +952,7 @@ mod tests { qe_next_update: initial_next_update, refreshed_tcb_next_update: Some(refreshed_next_update.clone()), refreshed_qe_next_update: Some(refreshed_next_update), + ..MockPcsConfig::default() }) .await .unwrap(); From cd1d61afe01bf865aff42f80bf8efb693bd4cec4 Mon Sep 17 00:00:00 2001 From: peg Date: Wed, 3 Jun 2026 08:52:46 +0200 Subject: [PATCH 3/3] Add doccomment following review --- crates/attestation/src/dcap.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 7355b1b..072fd24 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -198,6 +198,16 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( Ok(measurements) } +/// This returns an error if the TCB status associated with the attestation +/// is not marked 'UpToDate'. +/// +/// dcap-qvl's verification already rejects 'revoked' TCB status, so this +/// serves to reject the following remaining non-up-to-date variants: +/// - SWHardeningNeeded +/// - ConfigurationNeeded +/// - ConfigurationAndSWHardeningNeeded +/// - OutOfDate +/// - OutOfDateConfigurationNeeded fn ensure_up_to_date_tcb( verified_report: VerifiedReport, fmspc: String,