From 627b4a03f194885aa178e49be578d55b5bb1f846 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 29 Jul 2026 16:24:30 +0000 Subject: [PATCH] refactor(verifier): make TCB policy sources exhaustive TCB policy source handling used a catch-all match arm, so a newly added DstackVerifiedReport variant would silently inherit another platform's trust semantics instead of requiring an explicit decision. Match all report variants exhaustively in policy_tcb_fields so adding a platform fails the build here, and document why each platform reads the source it does: GCP has a bundled TPM report with no TCB surface, SNP derives its status from TCB version comparison rather than a field, AWS NitroTPM is normalized to "UpToDate" to match the KMS bootAuth payload, and Nitro Enclave stays empty so an "UpToDate" gate fails closed. Cover the mapping with a decision table over constructed attestations. The table exercises policy_tcb_fields itself rather than a helper, since the report-to-policy mapping is where a wrong source can silently downgrade the auth gate. --- dstack/verifier/src/verification.rs | 274 +++++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 6 deletions(-) diff --git a/dstack/verifier/src/verification.rs b/dstack/verifier/src/verification.rs index a234c4055..883a79ba1 100644 --- a/dstack/verifier/src/verification.rs +++ b/dstack/verifier/src/verification.rs @@ -37,8 +37,22 @@ use crate::types::{ }; /// Return the canonical TCB status and advisory list used by auth policy. +/// +/// Matched exhaustively on purpose: adding a `DstackVerifiedReport` variant must +/// fail the build here rather than silently inherit another platform's TCB +/// semantics through a catch-all arm. pub fn policy_tcb_fields(attestation: &VerifiedAttestation) -> (String, Vec) { match &attestation.report { + DstackVerifiedReport::DstackTdx(report) => { + (report.status.clone(), report.advisory_ids.clone()) + } + // GCP binds a TPM quote next to the TDX quote, but only the TDX report + // carries a TCB surface, so the TPM report contributes nothing here. + DstackVerifiedReport::DstackGcpTdx { tdx_report, .. } => { + (tdx_report.status.clone(), tdx_report.advisory_ids.clone()) + } + // SNP has no upstream status string; it is derived by comparing the + // report's TCB versions, so read it from `tcb_info` rather than a field. DstackVerifiedReport::DstackAmdSevSnp(report) => ( report.tcb_info.tcb_status().to_string(), report.advisory_ids.clone(), @@ -46,13 +60,10 @@ pub fn policy_tcb_fields(attestation: &VerifiedAttestation) -> (String, Vec ("UpToDate".to_string(), Vec::new()), - _ => attestation - .report - .tdx_report() - .map(|report| (report.status.clone(), report.advisory_ids.clone())) - .unwrap_or_default(), + // Other no-TCB platforms (currently Nitro Enclave) stay empty so a + // relying party's UpToDate requirement fails closed. + DstackVerifiedReport::DstackNitroEnclave(_) => (String::new(), Vec::new()), } } @@ -1363,6 +1374,19 @@ mod tests { use super::*; + // Kept inside `mod tests` so the non-test build stays free of unused imports. + use dcap_qvl::{ + quote::{Report as DcapReport, TDReport10}, + tcb_info::{TcbStatus, TcbStatusWithAdvisory}, + verify::VerifiedReport as DcapVerifiedReport, + }; + use dstack_attest::amd_sev_snp::{AmdSnpTcbInfo, VerifiedAmdSnpReport}; + use ra_tls::attestation::{ + AwsNitroTpmVerifiedReport, DstackAwsNitroTpmQuote, DstackGcpTdxQuote, DstackNitroQuote, + NitroVerifiedReport, SnpQuote, TdxQuote, + }; + use tpm_qvl::verify::{ClockInfo, QuoteInfo, TpmAttest, VerifiedReport as TpmVerifiedReport}; + fn aws_boot_pcrs(pcr4: u8) -> BTreeMap> { BTreeMap::from([ (4, vec![pcr4; 48]), @@ -1405,6 +1429,244 @@ mod tests { Arc::new(AttestationVerifier::new_prod(None).unwrap()) } + /// Build a TDX verified report carrying `status`/`advisory_ids`. Everything + /// else is zeroed: `policy_tcb_fields` reads only those two fields. + fn tdx_report(status: &str, advisory_ids: &[&str]) -> DcapVerifiedReport { + DcapVerifiedReport { + status: status.to_string(), + advisory_ids: advisory_ids.iter().map(|id| id.to_string()).collect(), + report: DcapReport::TD10(TDReport10 { + tee_tcb_svn: [0; 16], + mr_seam: [0; 48], + mr_signer_seam: [0; 48], + seam_attributes: [0; 8], + td_attributes: [0; 8], + xfam: [0; 8], + mr_td: [0; 48], + mr_config_id: [0; 48], + mr_owner: [0; 48], + mr_owner_config: [0; 48], + rt_mr0: [0; 48], + rt_mr1: [0; 48], + rt_mr2: [0; 48], + rt_mr3: [0; 48], + report_data: [0; 64], + }), + ppid: Vec::new(), + qe_status: TcbStatusWithAdvisory::new(TcbStatus::UpToDate, Vec::new()), + platform_status: TcbStatusWithAdvisory::new(TcbStatus::UpToDate, Vec::new()), + } + } + + fn snp_report(tcb_info: AmdSnpTcbInfo, advisory_ids: &[&str]) -> VerifiedAmdSnpReport { + VerifiedAmdSnpReport { + measurement: [0; 48], + report_data: [0; 64], + host_data: [0; 32], + chip_id: [0; 64], + tcb_info, + advisory_ids: advisory_ids.iter().map(|id| id.to_string()).collect(), + } + } + + fn tpm_report_for_gcp() -> TpmVerifiedReport { + TpmVerifiedReport { + attest: TpmAttest { + magic: 0, + type_: 0, + qualified_signer: Vec::new(), + qualified_data: Vec::new(), + clock_info: ClockInfo { + clock: 0, + reset_count: 0, + restart_count: 0, + safe: 0, + }, + firmware_version: 0, + attested_quote_info: QuoteInfo { + pcr_selections: Vec::new(), + pcr_digest: Vec::new(), + }, + }, + platform: dstack_types::Platform::Gcp, + pcr_values: Vec::new(), + } + } + + /// `policy_tcb_fields` ignores the quote, but pair each report with its own + /// platform's quote so the rows stay readable as real attestations. + fn verified_attestation( + quote: AttestationQuote, + report: DstackVerifiedReport, + ) -> VerifiedAttestation { + Attestation { + quote, + runtime_events: Vec::new(), + report_data: [0; 64], + config: String::new(), + report, + } + } + + fn empty_tdx_quote() -> AttestationQuote { + AttestationQuote::DstackTdx(TdxQuote { + quote: Vec::new(), + event_log: Vec::new(), + }) + } + + /// Exercises the report-to-policy mapping itself, which is where a wrong + /// source can silently downgrade the auth gate (e.g. reading the wrong + /// report on GCP, or letting a no-TCB platform report "UpToDate"). + #[test] + fn tcb_policy_fields_map_each_platform_to_its_own_tcb_source() { + let advisories = ["INTEL-SA-00001", "INTEL-SA-00002"]; + let expected_advisories: Vec = advisories.iter().map(|id| id.to_string()).collect(); + + // SNP derives its status by comparing TCB versions rather than reading + // a field, so cover both the equal (UpToDate) and unequal (OutOfDate) + // shapes. + let snp_up_to_date = AmdSnpTcbInfo::default(); + let mut snp_out_of_date = AmdSnpTcbInfo::default(); + snp_out_of_date.current.microcode = 1; + + let rows: [(&str, VerifiedAttestation, &str, Vec); 7] = [ + ( + "tdx-up-to-date", + verified_attestation( + empty_tdx_quote(), + DstackVerifiedReport::DstackTdx(tdx_report("UpToDate", &[])), + ), + "UpToDate", + Vec::new(), + ), + ( + "tdx-out-of-date-carries-advisories", + verified_attestation( + empty_tdx_quote(), + DstackVerifiedReport::DstackTdx(tdx_report("OutOfDate", &advisories)), + ), + "OutOfDate", + expected_advisories.clone(), + ), + ( + "tdx-revoked", + verified_attestation( + empty_tdx_quote(), + DstackVerifiedReport::DstackTdx(tdx_report("Revoked", &advisories)), + ), + "Revoked", + expected_advisories.clone(), + ), + ( + // The bundled TPM report has no TCB surface: the TDX report must + // still drive policy, and must not be flattened to empty. + "gcp-tdx-reads-the-tdx-report-not-the-tpm-report", + verified_attestation( + AttestationQuote::DstackGcpTdx(DstackGcpTdxQuote { + tdx_quote: TdxQuote { + quote: Vec::new(), + event_log: Vec::new(), + }, + tpm_quote: TpmQuote { + message: Vec::new(), + signature: Vec::new(), + pcr_values: Vec::new(), + ak_cert: Vec::new(), + platform: dstack_types::Platform::Gcp, + event_log: Vec::new(), + }, + }), + DstackVerifiedReport::DstackGcpTdx { + tdx_report: tdx_report("OutOfDate", &advisories), + tpm_report: tpm_report_for_gcp(), + }, + ), + "OutOfDate", + expected_advisories.clone(), + ), + ( + "sev-snp-matching-tcb-versions-are-up-to-date", + verified_attestation( + AttestationQuote::DstackAmdSevSnp(SnpQuote { + report: Vec::new(), + cert_chain: Vec::new(), + mr_config: String::new(), + }), + DstackVerifiedReport::DstackAmdSevSnp(snp_report(snp_up_to_date, &[])), + ), + "UpToDate", + Vec::new(), + ), + ( + "sev-snp-mismatched-tcb-versions-are-out-of-date", + verified_attestation( + AttestationQuote::DstackAmdSevSnp(SnpQuote { + report: Vec::new(), + cert_chain: Vec::new(), + mr_config: String::new(), + }), + DstackVerifiedReport::DstackAmdSevSnp(snp_report(snp_out_of_date, &advisories)), + ), + "OutOfDate", + expected_advisories.clone(), + ), + ( + // No TCB surface, but normalized to UpToDate so the verifier's + // policy boot info matches the KMS bootAuth payload. + "aws-nitro-tpm-is-normalized-to-up-to-date", + verified_attestation( + AttestationQuote::DstackAwsNitroTpm(DstackAwsNitroTpmQuote { + attestation_doc: Vec::new(), + }), + DstackVerifiedReport::DstackAwsNitroTpm(AwsNitroTpmVerifiedReport { + module_id: String::new(), + pcrs: BTreeMap::new(), + public_key: None, + user_data: Vec::new(), + nonce: None, + timestamp: 0, + }), + ), + "UpToDate", + Vec::new(), + ), + ]; + + for (name, attestation, expected_status, expected_advisory_ids) in rows { + let (status, advisory_ids) = policy_tcb_fields(&attestation); + assert_eq!(status, expected_status, "{name}"); + assert_eq!(advisory_ids, expected_advisory_ids, "{name}"); + } + } + + /// Split out from the table above because it asserts the opposite property: + /// Nitro Enclave must stay empty so a relying party's "UpToDate" gate fails + /// closed rather than accepting a platform with no TCB surface. + #[test] + fn nitro_enclave_reports_no_tcb_status_and_fails_an_up_to_date_gate() { + let attestation = verified_attestation( + AttestationQuote::DstackNitroEnclave(DstackNitroQuote { + nsm_quote: Vec::new(), + }), + DstackVerifiedReport::DstackNitroEnclave(NitroVerifiedReport { + module_id: String::new(), + pcrs: NitroPcrs { + pcr0: vec![0x10; 48], + pcr1: vec![0x11; 48], + pcr2: vec![0x12; 48], + }, + user_data: Vec::new(), + timestamp: 0, + }), + ); + + let (status, advisory_ids) = policy_tcb_fields(&attestation); + assert_eq!(status, ""); + assert_ne!(status, "UpToDate"); + assert!(advisory_ids.is_empty()); + } + #[test] fn gcp_and_nitro_enclave_measurement_bindings_matrix() { let verifier = test_verifier();