Skip to content
Open
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
87 changes: 76 additions & 11 deletions crates/attestation/src/dcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -186,14 +187,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp(
override_outdated_tcb,
)?;

if verified_report.status != "UpToDate" {
tracing::warn!(
status = %verified_report.status,
advisory_ids = ?verified_report.advisory_ids,
fmspc,
"DCAP verification succeeded with non-UpToDate TCB status"
);
}
ensure_up_to_date_tcb(verified_report, fmspc)?;

let measurements = MultiMeasurements::from_dcap_qvl_quote(&quote)?;

Expand All @@ -204,6 +198,36 @@ 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'd drop a comment here that "revoked" status is rejected by qvl so this only removes the other two non-up-to-date statuses

verified_report: VerifiedReport,
fmspc: String,
) -> Result<(), DcapVerificationError> {
if verified_report.status != "UpToDate" {
tracing::warn!(
status = %verified_report.status,
advisory_ids = ?verified_report.advisory_ids,
fmspc,
"DCAP verification succeeded with non-UpToDate TCB status"
);
return Err(DcapVerificationError::BadTcbStatus(
verified_report.status,
verified_report.advisory_ids,
fmspc,
));
}
Ok(())
}

#[cfg(any(test, feature = "mock"))]
pub async fn verify_dcap_attestation(
input: Vec<u8>,
Expand All @@ -215,13 +239,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)?;
Copy link
Copy Markdown
Collaborator Author

@ameba23 ameba23 Jun 1, 2026

Choose a reason for hiding this comment

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

This change is not strictly relevant to the PR, but just to ensure that the mock verifier uses the exact same method as the non-mock verifier.


ensure_up_to_date_tcb(verified_report, fmspc)?;

let measurements = MultiMeasurements::from_dcap_qvl_quote(&quote)?;
if get_quote_input_data(quote.report) != expected_input_data {
Expand All @@ -243,7 +271,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(&quote)?;
if get_quote_input_data(quote.report.clone()) != expected_input_data {
Expand Down Expand Up @@ -290,10 +322,13 @@ 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>, String),
}

#[cfg(test)]
mod tests {
use dcap_qvl::tcb_info::TcbStatus;
use mock_tdx::{MockPcsConfig, spawn_mock_pcs_server};

use super::*;
Expand Down Expand Up @@ -418,4 +453,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);
}
}
2 changes: 2 additions & 0 deletions crates/mock-tdx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
38 changes: 30 additions & 8 deletions crates/mock-tdx/src/mock_pcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -33,6 +33,10 @@ pub struct MockPcsConfig {
pub refreshed_tcb_next_update: Option<String>,
/// Optional `nextUpdate` value returned by later QE identity responses
pub refreshed_qe_next_update: Option<String>,
/// Optional TCB status to serve for all TCB levels
pub tcb_status: Option<TcbStatus>,
/// Advisory IDs to serve with the configured TCB status
pub tcb_advisory_ids: Vec<String>,
}

impl Default for MockPcsConfig {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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<String>,
refreshed_qe_next_update: Option<String>,
tcb_status: Option<TcbStatus>,
tcb_advisory_ids: Vec<String>,
pck_crl: Vec<u8>,
pck_crl_issuer_chain: String,
tcb_issuer_chain: String,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
})),
)
}
Expand All @@ -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,
})),
)
}
Expand All @@ -227,3 +244,8 @@ async fn mock_qe_identity_handler(
async fn mock_root_ca_crl_handler(State(state): State<Arc<MockPcsState>>) -> 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()))
}
6 changes: 6 additions & 0 deletions crates/pccs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading