diff --git a/crates/attestation/src/azure/verify.rs b/crates/attestation/src/azure/verify.rs index b2ec97f..8c6285d 100644 --- a/crates/attestation/src/azure/verify.rs +++ b/crates/attestation/src/azure/verify.rs @@ -20,6 +20,7 @@ use super::{ unix_time_now_secs, }; use crate::{ + VerifiedAttestation, dcap::{ verify_dcap_attestation_with_given_timestamp, verify_dcap_attestation_with_timestamp_sync, @@ -43,7 +44,7 @@ pub async fn verify_azure_attestation( expected_input_data: [u8; 64], pccs: Option, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let now = unix_time_now_secs()?; verify_azure_attestation_with_given_timestamp( @@ -67,7 +68,7 @@ pub fn verify_azure_attestation_sync( expected_input_data: [u8; 64], pccs: Pccs, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let now = unix_time_now_secs()?; verify_azure_attestation_with_given_timestamp_sync( @@ -90,7 +91,7 @@ async fn verify_azure_attestation_with_given_timestamp( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let PreparedAzureAttestation { tdx_quote_bytes, hcl_report, @@ -99,7 +100,9 @@ async fn verify_azure_attestation_with_given_timestamp( tpm_attestation, } = prepare_azure_attestation(input)?; - let _dcap_measurements = verify_dcap_attestation_with_given_timestamp( + // Only the endorsements travel upward: this platform is judged on the + // vTPM PCRs, not the TD quote + let (dcap, _) = verify_dcap_attestation_with_given_timestamp( tdx_quote_bytes, expected_tdx_input_data, pccs, @@ -109,13 +112,16 @@ async fn verify_azure_attestation_with_given_timestamp( ) .await?; - finish_azure_attestation_verification( + // The vTPM leg fetches nothing — AK chain in the evidence, roots + // compiled in — so it adds no endorsements of its own + let measurements = finish_azure_attestation_verification( hcl_report, var_data_hash, tpm_attestation, expected_input_data, now, - ) + )?; + Ok(VerifiedAttestation { measurements, endorsements: dcap.endorsements }) } /// Synchronous version of the verifier @@ -126,7 +132,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let PreparedAzureAttestation { tdx_quote_bytes, hcl_report, @@ -135,7 +141,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( tpm_attestation, } = prepare_azure_attestation(input)?; - let _dcap_measurements = verify_dcap_attestation_with_timestamp_sync( + let (dcap, _) = verify_dcap_attestation_with_timestamp_sync( tdx_quote_bytes, expected_tdx_input_data, pccs, @@ -144,13 +150,14 @@ fn verify_azure_attestation_with_given_timestamp_sync( override_azure_outdated_tcb, )?; - finish_azure_attestation_verification( + let measurements = finish_azure_attestation_verification( hcl_report, var_data_hash, tpm_attestation, expected_input_data, now, - ) + )?; + Ok(VerifiedAttestation { measurements, endorsements: dcap.endorsements }) } /// Parses the attestation during verification @@ -339,7 +346,10 @@ impl RsaPubKey { #[cfg(test)] mod tests { + use dcap_qvl::QuoteCollateralV3; + use super::{super::MAX_AZURE_ATTESTATION_PAYLOAD_SIZE, *}; + use crate::EndorsementSnapshot; fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] { let attestation_document: AttestationDocument = @@ -454,31 +464,43 @@ mod tests { assert_eq!(attestation_document.tpm_attestation.ak_intermediate_certificates_pem.len(), 2); let attestation_json = serde_json::to_vec(&attestation_document).unwrap(); - let async_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let sync_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); + let fixture_collateral: QuoteCollateralV3 = + serde_saphyr::from_slice(collateral_bytes).unwrap(); - let async_measurements = verify_azure_attestation_with_given_timestamp( + let VerifiedAttestation { + measurements: async_measurements, + endorsements: async_endorsements, + } = verify_azure_attestation_with_given_timestamp( attestation_json.clone(), [0; 64], None, - Some(async_collateral), + Some(fixture_collateral.clone()), now, false, ) .await .unwrap(); - let sync_measurements = verify_azure_attestation_with_given_timestamp_sync( + let VerifiedAttestation { + measurements: sync_measurements, + endorsements: sync_endorsements, + } = verify_azure_attestation_with_given_timestamp_sync( attestation_json, [0; 64], Pccs::new_without_prewarm(None), - Some(sync_collateral), + Some(fixture_collateral.clone()), now, false, ) .unwrap(); assert_eq!(async_measurements, sync_measurements); + // The bundle handed back is the one the DCAP leg consumed, which is + // what makes archiving it provenance rather than a second copy, and + // it arrives paired with the instant both legs were held to + let expected = EndorsementSnapshot::dcap(fixture_collateral, now); + assert_eq!(async_endorsements, expected); + assert_eq!(sync_endorsements, expected); } #[tokio::test] diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index ee91fe2..f863ae9 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -1,5 +1,10 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and //! verification +//! +//! Every verify function returns the parsed [Quote] beside the +//! [VerifiedAttestation]: verification parses it anyway, and the GCP +//! provenance check needs the PPID from its PCK leaf. Other callers drop +//! it. use dcap_qvl::{ QuoteCollateralV3, collateral::CollateralClient, @@ -12,7 +17,12 @@ use mock_tdx::generate_mock_tdx_quote; use pccs::{Pccs, PccsError}; use thiserror::Error; -use crate::{AttestationError, measurements::MultiMeasurements}; +use crate::{ + AttestationError, + EndorsementSnapshot, + VerifiedAttestation, + measurements::MultiMeasurements, +}; /// FMSPC with which to override TCB level checks on Azure (not used for GCP /// or other platforms) @@ -28,13 +38,13 @@ pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, Attestat Ok(quote) } -/// Verify a DCAP TDX quote, and return the measurement values +/// Verify a DCAP TDX quote #[cfg(not(any(test, feature = "mock")))] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; verify_dcap_attestation_with_given_timestamp( @@ -48,8 +58,7 @@ pub async fn verify_dcap_attestation( .await } -/// Synchronous version - Verify a DCAP TDX quote, and return the -/// measurement values +/// Synchronous version - verify a DCAP TDX quote /// /// This relies on having DCAP collateral already present in the cache /// @@ -59,7 +68,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; verify_dcap_attestation_with_timestamp_sync( @@ -72,8 +81,8 @@ pub fn verify_dcap_attestation_sync( ) } -/// Verify a DCAP TDX quote, and return the measurement values, providing a -/// timestamp an optional pre-fetched collateral +/// Verify a DCAP TDX quote, providing a timestamp and an optional +/// pre-fetched collateral /// /// This relies on having DCAP collateral already present in the cache /// @@ -85,7 +94,7 @@ pub fn verify_dcap_attestation_with_timestamp_sync( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); @@ -119,7 +128,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); @@ -153,7 +162,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( collateral: QuoteCollateralV3, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { tracing::info!("Verifying DCAP attestation: {quote:?}"); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -198,7 +207,13 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(( + VerifiedAttestation { + measurements, + endorsements: EndorsementSnapshot::dcap(collateral, now), + }, + quote, + )) } #[cfg(any(test, feature = "mock"))] @@ -206,7 +221,7 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -225,7 +240,13 @@ pub async fn verify_dcap_attestation( return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(( + VerifiedAttestation { + measurements, + endorsements: EndorsementSnapshot::dcap(collateral, now), + }, + quote, + )) } #[cfg(any(test, feature = "mock"))] @@ -233,7 +254,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -246,7 +267,13 @@ pub fn verify_dcap_attestation_sync( if get_quote_input_data("e.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(( + VerifiedAttestation { + measurements, + endorsements: EndorsementSnapshot::dcap(collateral, now), + }, + quote, + )) } /// Create a mock quote for testing on non-confidential hardware @@ -323,41 +350,49 @@ mod tests { let collateral_bytes: &'static [u8] = include_bytes!("../test-assets/dcap-quote-collateral-00.yaml"); - let async_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let sync_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - - let (async_measurements, _) = verify_dcap_attestation_with_given_timestamp( - attestation_bytes.to_vec(), - [ - 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, 227, - 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, 161, 136, - 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, 245, 114, 33, - 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, - ], - None, - Some(async_collateral), - now, - false, - ) - .await - .unwrap(); + let fixture_collateral: QuoteCollateralV3 = + serde_saphyr::from_slice(collateral_bytes).unwrap(); + + let (VerifiedAttestation { measurements: async_measurements, endorsements }, _) = + verify_dcap_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + [ + 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, + 227, 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, + 161, 136, 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, + 245, 114, 33, 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, + ], + None, + Some(fixture_collateral.clone()), + now, + false, + ) + .await + .unwrap(); - let (sync_measurements, _) = verify_dcap_attestation_with_timestamp_sync( - attestation_bytes.to_vec(), - [ - 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, 227, - 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, 161, 136, - 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, 245, 114, 33, - 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, - ], - Pccs::new_without_prewarm(None), - Some(sync_collateral), - now, - false, - ) - .unwrap(); + let (VerifiedAttestation { measurements: sync_measurements, .. }, _) = + verify_dcap_attestation_with_timestamp_sync( + attestation_bytes.to_vec(), + [ + 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, + 227, 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, + 161, 136, 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, + 245, 114, 33, 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, + ], + Pccs::new_without_prewarm(None), + Some(fixture_collateral.clone()), + now, + false, + ) + .unwrap(); assert_eq!(async_measurements, sync_measurements); + // A caller archiving provenance gets back the bundle the + // verification consumed, not a second copy of it + assert_eq!(endorsements.dcap, Some(fixture_collateral)); + // ... and the instant it was held to, which is the other half of + // what makes the verification reproducible + assert_eq!(endorsements.at, now); let platform_metadata = crate::mock_platform_metadata(crate::AttestationType::DcapTdx).unwrap(); measurement_policy @@ -381,7 +416,7 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let _measurements = verify_dcap_attestation_with_given_timestamp( + verify_dcap_attestation_with_given_timestamp( attestation_bytes.to_vec(), [ 210, 20, 43, 100, 53, 152, 235, 95, 174, 43, 200, 82, 157, 215, 154, 85, 139, 41, @@ -409,10 +444,10 @@ mod tests { let expected_input_data = [0xA5; 64]; let quote = create_dcap_attestation(expected_input_data).unwrap(); - let (measurements, _) = + let (verified, _) = verify_dcap_attestation(quote, expected_input_data, Some(pccs)).await.unwrap(); - assert_eq!(measurements, crate::measurements::mock_dcap_measurements()); + assert_eq!(verified.measurements, crate::measurements::mock_dcap_measurements()); assert_eq!(mock_pcs.tcb_call_count(), 1); assert_eq!(mock_pcs.qe_call_count(), 1); } diff --git a/crates/attestation/src/gcp/firmware.rs b/crates/attestation/src/gcp/firmware.rs index b6d37ec..40affc1 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -78,6 +78,7 @@ mod tests { use super::GcpFirmwareCache; use crate::{ PlatformMetadata, + VerifiedAttestation, dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp}, measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, }; @@ -155,16 +156,17 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap(); - let (measurements, _) = verify_dcap_attestation_with_given_timestamp( - attestation_bytes.to_vec(), - expected_input_data, - None, - Some(collateral), - GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, - false, - ) - .await - .unwrap(); + let (VerifiedAttestation { measurements, .. }, _) = + verify_dcap_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + expected_input_data, + None, + Some(collateral), + GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, + false, + ) + .await + .unwrap(); let measurement_policy = MeasurementPolicy { accepted_measurements: vec![MeasurementRecord { diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index d3a0d0d..e45e132 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -20,6 +20,9 @@ use std::{ use attest_measure::platform::PlatformError; pub use attest_types::{AttestationEvidence, PlatformMetadata}; +/// Re-exported so callers can archive [EndorsementSnapshot::dcap] without +/// depending on `dcap-qvl` directly +pub use dcap_qvl::QuoteCollateralV3; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; @@ -352,6 +355,64 @@ pub enum PccsMode { Lazy, } +/// Fetched endorsement material, bound to the instant it was evaluated at +/// +/// Everything fetched expires — `nextUpdate` on TCB Info, QE Identity and +/// both CRLs, `notAfter` on the issuer chains — so a bundle answers +/// freshness only with respect to an instant. Pairing the two is what makes +/// a verdict reproducible: same evidence, same snapshot, same verdict. +/// +/// What a verifier fetches is a transport choice of the protocol, not a +/// property of the platform: evidence can carry its own endorsements +/// instead. Hence a struct that grows fields rather than an enum keyed by +/// platform, and `#[non_exhaustive]` to keep that growth additive. +/// +/// Two caveats. Trust anchors are compiled in rather than captured here, so +/// a replay needs a build carrying the same ones — under `mock`, the mock +/// root. And "endorsements" is loose: in [RFC 9334] terms a DCAP bundle +/// spans both Endorsements (issuer chains, CRLs) and Reference Values (TCB +/// Info, QE Identity). +/// +/// [RFC 9334]: https://www.rfc-editor.org/rfc/rfc9334.html +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct EndorsementSnapshot { + /// Seconds since the Unix epoch — the unit `dcap-qvl` and webpki take + pub at: u64, + /// `Some` when the verification fetched a DCAP bundle, `None` when the + /// evidence carried its own or the platform has no DCAP leg. The bundle + /// consumed, not a second copy: a cache can refresh between two fetches + pub dcap: Option, +} + +impl EndorsementSnapshot { + /// A verification that fetched one DCAP collateral bundle + pub fn dcap(collateral: QuoteCollateralV3, at: u64) -> Self { + Self { at, dcap: Some(collateral) } + } +} + +/// Evidence whose authenticity a Verifier established, with what it was +/// established against +/// +/// Not an Attestation Result in [RFC 9334] terms: the appraisal policy runs +/// after this value is built, and a caller may configure it to check +/// nothing, so no Reference Value comparison is implied. Archived beside +/// the evidence, it reproduces the verdict. +/// +/// [RFC 9334]: https://www.rfc-editor.org/rfc/rfc9334.html +#[derive(Clone, Debug)] +pub struct VerifiedAttestation { + /// MRTD and RTMR0–3 from the quote on DCAP and GCP. On Azure the vTPM + /// PCRs, which measure the guest boot rather than the launched TD and + /// chain to the TD quote: its report data commits to the HCL var data + /// carrying the AK public key that signs the vTPM quote + pub measurements: MultiMeasurements, + /// The half of a reproducible verdict that does not ride in the + /// evidence + pub endorsements: EndorsementSnapshot, +} + /// Allows remote attestations to be verified #[derive(Clone, Debug)] pub struct AttestationVerifier { @@ -506,7 +567,7 @@ impl AttestationVerifier { &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -514,7 +575,7 @@ impl AttestationVerifier { log_attestation(&attestation_exchange_message); } - let measurements = match attestation_type { + let verified = match attestation_type { AttestationType::None => { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); @@ -550,7 +611,7 @@ impl AttestationVerifier { .attestation_evidence .as_ref() .ok_or(AttestationError::AttestationTypeNotAccepted)?; - let (measurements, quote) = dcap::verify_dcap_attestation( + let (verified, quote) = dcap::verify_dcap_attestation( attestation_evidence.quote.clone(), expected_input_data, self.internal_pccs.clone(), @@ -559,7 +620,7 @@ impl AttestationVerifier { if attestation_type == AttestationType::GcpTdx { self.gcp_provenance_checker.verify_provenance(quote).await?; } - measurements + verified } }; @@ -569,20 +630,20 @@ impl AttestationVerifier { .as_ref() .map(|evidence| evidence.platform.clone()); self.measurement_policy.check_measurement_with_gcp_cache( - &measurements, + &verified.measurements, platform_metadata.as_ref(), Some(&self.known_gcp_firmware), )?; tracing::debug!("Verification successful"); - Ok(Some(measurements)) + Ok(Some(verified)) } pub fn verify_attestation_sync( &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -590,7 +651,7 @@ impl AttestationVerifier { log_attestation(&attestation_exchange_message); } - let measurements = match attestation_type { + let verified = match attestation_type { AttestationType::None => { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); @@ -632,7 +693,7 @@ impl AttestationVerifier { #[cfg(not(any(test, feature = "mock")))] let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; - let (measurements, quote) = dcap::verify_dcap_attestation_sync( + let (verified, quote) = dcap::verify_dcap_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, pccs, @@ -640,7 +701,7 @@ impl AttestationVerifier { if attestation_type == AttestationType::GcpTdx { self.gcp_provenance_checker.verify_provenance_sync("e)?; } - measurements + verified } }; @@ -650,13 +711,13 @@ impl AttestationVerifier { .as_ref() .map(|evidence| evidence.platform.clone()); self.measurement_policy.check_measurement_with_gcp_cache( - &measurements, + &verified.measurements, platform_metadata.as_ref(), Some(&self.known_gcp_firmware), )?; tracing::debug!("Verification successful"); - Ok(Some(measurements)) + Ok(Some(verified)) } /// Whether we allow no remote attestation @@ -860,4 +921,48 @@ mod tests { assert!(result.is_ok(), "expected sync mock verification to succeed: {result:?}"); } + + /// On the fetching path, the reported bundle is the one the fetch + /// produced — the property that makes archiving it provenance rather + /// than a second, possibly different, copy. + #[tokio::test] + async fn verify_reports_the_collateral_the_fetch_produced() { + let input_data = [7u8; 64]; + let quote_bytes = dcap::create_dcap_attestation(input_data).unwrap(); + let quote = dcap_qvl::quote::Quote::parse("e_bytes).unwrap(); + let fmspc = hex::encode_upper(dcap_qvl::intel::quote_fmspc("e).unwrap()); + let ca = dcap_qvl::intel::quote_ca("e).unwrap().as_id_str(); + let attestation_evidence = AttestationEvidence { + quote: quote_bytes, + platform: mock_platform_metadata(AttestationType::DcapTdx).unwrap(), + }; + + let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig::default()).await.unwrap(); + let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); + + let verified = verifier + .verify_attestation(attestation_evidence.into(), input_data) + .await + .unwrap() + .expect("mock evidence carries an attestation"); + + // The second read is served from the PCCS cache, not a second + // fetch, so it yields the same bundle the verification + // consumed. That is what makes it a valid comparison here — + // and the reason a caller must not rely on the pattern in + // general, where a refresh in between would hand back a + // different bundle + let (served, _is_fresh) = verifier + .internal_pccs + .as_ref() + .unwrap() + .get_collateral( + fmspc, + ca, + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + ) + .await + .unwrap(); + assert_eq!(verified.endorsements.dcap, Some(served)); + } }