From 521fa5084c35e2486a05a4ee3e61d512f6aa2352 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere <9342524+samlaf@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:12:16 -0400 Subject: [PATCH 1/2] feat(attestation)!: pin verification to archived collateral with VerifyMode Closes flashbots/attested-tls#84 Second of two changes for that issue, on top of flashbots/attested-tls#85. Reporting the endorsements a verification consumed is half of provenance. The other half is running the same verification again later, against that snapshot, and getting the same answer. Nothing exposed that: the public entry points always fetched and always read the wall clock, and the only way to supply a bundle was through variants that also took a bare timestamp. API before and after -------------------- AttestationVerifier verify_attestation(msg, input) -> verify_attestation(msg, input, mode) verify_attestation_sync(msg, input) -> verify_attestation_sync(msg, input, mode) dcap verify_dcap_attestation(q, input, pccs) -> verify_dcap_attestation(q, input, mode, pccs) verify_dcap_attestation_sync(q, input, pccs) -> verify_dcap_attestation_sync(q, input, mode, pccs) verify_dcap_attestation_with_given_timestamp( q, input, pccs, Option, now, override_azure_outdated_tcb) -> (removed) verify_dcap_attestation_with_timestamp_sync( q, input, pccs, Option, now, override_azure_outdated_tcb) -> (removed) azure verify_azure_attestation(a, input, pccs, override) -> verify_azure_attestation(a, input, mode, pccs, override) verify_azure_attestation_sync(a, input, pccs, override) -> verify_azure_attestation_sync(a, input, mode, pccs, override) new enum VerifyMode { Live, Archived(EndorsementSnapshot) } DcapVerificationError::ArchivedWithoutDcapCollateral Why this shape -------------- One input instead of two. The removed variants took the collateral and the instant as separate arguments, so a caller could pair a pinned bundle with the wrong instant, or a live fetch with a pinned instant, and get a verdict that reproduces nothing. VerifyMode::Archived takes the EndorsementSnapshot that #85 hands back, so the bundle and the instant it was held to travel together and the mistake has no spelling. The mixed case is refused too: an archived snapshot with no bundle for the DCAP leg fails with ArchivedWithoutDcapCollateral rather than being completed by a fetch. The mode reaches the verifier. The measurement-policy check lives on AttestationVerifier, and a relying party re-checking archived evidence needs both it and the pinned instant. The removed variants sat below the verifier, so that combination did not exist. The mode is a parameter of the call rather than the builder because it is a fact about one verification, not about the verifier: the same instance serves a live handshake and an archive replay. One instant for both Azure legs. The DCAP leg reports the instant it evaluated at, and the vTPM AK chain is checked at that same instant, in either mode. The wall clock is read in exactly one place. The Azure TCB override leaves the public surface. It rode along on the removed variants only because the Azure verifier and the fixture tests shared them. Both now call the crate-private body that the two public entry points wrap, so the override is an argument of the Azure leg and nothing else. Only Azure has a reason to relax TCB checks. Live is behaviour-preserving. Every existing caller passes VerifyMode::Live and gets what it got before: collateral from the PCCS or Intel, freshness at the wall clock. The two in-tree callers, attested-tls and attestation-provider-server, needed only that argument. GCP checks and Archived mode ---------------------------- The GCP host provenance check from flashbots/attested-tls#54 stays live in either mode, as does the firmware fetch for the quote's MRTD. Neither rests on signed material a replay could re-verify: the provenance document is an unsigned JSON object whose trust is the TLS connection to Google's bucket, so archiving it would not make a replay stronger. The docs on VerifyMode::Archived and verify_attestation state the carve-out. Whether Archived should skip the provenance lookup instead is left open. BREAKING CHANGE: verify_attestation and verify_attestation_sync take a VerifyMode; the DCAP and Azure entry points take mode before pccs; the *_with_given_timestamp variants are gone, replaced by VerifyMode::Archived, which fails with DcapVerificationError::ArchivedWithoutDcapCollateral when its snapshot carries no DCAP bundle. --- crates/attestation-provider-server/src/lib.rs | 6 +- crates/attestation/src/azure/attester/mod.rs | 5 +- crates/attestation/src/azure/mod.rs | 4 - crates/attestation/src/azure/verify.rs | 172 +++------- crates/attestation/src/dcap.rs | 323 ++++++++++-------- crates/attestation/src/gcp/firmware.rs | 35 +- crates/attestation/src/lib.rs | 75 +++- crates/attested-tls/src/lib.rs | 3 +- 8 files changed, 330 insertions(+), 293 deletions(-) diff --git a/crates/attestation-provider-server/src/lib.rs b/crates/attestation-provider-server/src/lib.rs index a167b37..9bd7e11 100644 --- a/crates/attestation-provider-server/src/lib.rs +++ b/crates/attestation-provider-server/src/lib.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; pub use attestation::AttestationGenerator; -use attestation::{AttestationError, AttestationExchangeMessage, AttestationVerifier}; +use attestation::{AttestationError, AttestationExchangeMessage, AttestationVerifier, VerifyMode}; use axum::{ extract::{Path, State}, http::StatusCode, @@ -61,7 +61,9 @@ pub async fn attestation_provider_client( println!("Remote attestation type: {remote_attestation_type}"); - attestation_verifier.verify_attestation(remote_attestation_message.clone(), input_data).await?; + attestation_verifier + .verify_attestation(remote_attestation_message.clone(), input_data, VerifyMode::Live) + .await?; Ok(remote_attestation_message) } diff --git a/crates/attestation/src/azure/attester/mod.rs b/crates/attestation/src/azure/attester/mod.rs index bf4de2a..04fb3b9 100644 --- a/crates/attestation/src/azure/attester/mod.rs +++ b/crates/attestation/src/azure/attester/mod.rs @@ -20,7 +20,6 @@ use super::{ ak_certificate::verify_ak_cert_with_azure_roots, ensure_azure_attestation_payload_size, tpm_quote::TpmQuote, - unix_time_now_secs, }; /// Used in attestation type detection to check if we are on Azure @@ -151,6 +150,10 @@ impl TryFrom<&vtpm::Quote> for TpmQuote { } } +fn unix_time_now_secs() -> Result { + Ok(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs()) +} + /// Fetch intermediate certificates from the Authority Information Access /// (AIA) CA Issuers URLs in the leaf and each fetched intermediate. /// diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index fd0e778..3527d56 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -103,10 +103,6 @@ where Ok(certificates) } -fn unix_time_now_secs() -> Result { - Ok(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs()) -} - /// An error when generating or verifying a Microsoft Azure vTPM attestation /// (MAA is short for Microsoft Azure Attestation) #[derive(Error, Debug)] diff --git a/crates/attestation/src/azure/verify.rs b/crates/attestation/src/azure/verify.rs index 234196a..5e563db 100644 --- a/crates/attestation/src/azure/verify.rs +++ b/crates/attestation/src/azure/verify.rs @@ -4,7 +4,7 @@ //! chain verification against pinned Azure roots. use az_cvm_vtpm::{hcl, tdx}; use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE}; -use dcap_qvl::QuoteCollateralV3; +use dcap_qvl::verify::QuoteVerifier; use num_bigint::BigUint; use openssl::pkey::PKey; use pccs::Pccs; @@ -17,14 +17,11 @@ use super::{ TpmAttest, ak_certificate::verify_ak_cert_with_azure_roots, ensure_azure_attestation_payload_size, - unix_time_now_secs, }; use crate::{ VerifiedAttestation, - dcap::{ - verify_dcap_attestation_with_given_timestamp, - verify_dcap_attestation_with_timestamp_sync, - }, + VerifyMode, + dcap::{verify_quote, verify_quote_sync}, measurements::MultiMeasurements, }; @@ -39,60 +36,17 @@ struct PreparedAzureAttestation { } /// Verify a TDX attestation from Azure -pub async fn verify_azure_attestation( - input: Vec, - expected_input_data: [u8; 64], - pccs: Pccs, - override_azure_outdated_tcb: bool, -) -> Result { - let now = unix_time_now_secs()?; - - verify_azure_attestation_with_given_timestamp( - input, - expected_input_data, - pccs, - None, - now, - override_azure_outdated_tcb, - ) - .await -} - -/// Verify a TDX attestation from Azure - synchronous version -/// -/// This relies on having DCAP collateral already present in the cache -/// -/// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not -/// supported because fetching collateral requires asynchronous I/O. /// -/// If possible, prefer the async version -pub fn verify_azure_attestation_sync( - input: Vec, - expected_input_data: [u8; 64], - pccs: Pccs, - override_azure_outdated_tcb: bool, -) -> Result { - let now = unix_time_now_secs()?; - - verify_azure_attestation_with_given_timestamp_sync( - input, - expected_input_data, - pccs, - None, - now, - override_azure_outdated_tcb, - ) -} - -/// Do the verification, passing in the current time -/// This allows us to test this function without time checks going out of -/// date -async fn verify_azure_attestation_with_given_timestamp( +/// `mode` gates the DCAP leg and the vTPM leg alike: on +/// [VerifyMode::Archived] the AK certificate chain is checked as of the +/// same instant as the snapshot, and nothing reaches the network. +/// `pccs` only matters on [VerifyMode::Live]; see +/// [crate::dcap::verify_dcap_attestation]. +pub async fn verify_azure_attestation( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, ) -> Result { let PreparedAzureAttestation { @@ -103,26 +57,30 @@ async fn verify_azure_attestation_with_given_timestamp( tpm_attestation, } = prepare_azure_attestation(input)?; - // 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( + // The DCAP leg reports the instant it evaluated at, so the vTPM leg + // below is held to the same one - on [VerifyMode::Live] the clock + // is read once, not once per leg. Only the endorsements travel + // upward: this platform is judged on the vTPM PCRs, not the TD + // quote + let (dcap, _) = verify_quote( tdx_quote_bytes, expected_tdx_input_data, + mode, pccs, - collateral, - now, override_azure_outdated_tcb, + &QuoteVerifier::new_prod(), + None, ) .await?; - // The vTPM leg fetches nothing — AK chain in the evidence, roots - // compiled in — so it adds no endorsements of its own + // 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, + dcap.endorsements.at, )?; Ok(VerifiedAttestation { measurements, @@ -131,13 +89,21 @@ async fn verify_azure_attestation_with_given_timestamp( }) } -/// Synchronous version of the verifier -fn verify_azure_attestation_with_given_timestamp_sync( +/// Verify a TDX attestation from Azure - synchronous version +/// +/// `pccs` only matters on [VerifyMode::Live], and then the collateral has +/// to be in its cache already; see +/// [crate::dcap::verify_dcap_attestation_sync]. +/// +/// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not +/// supported because fetching collateral requires asynchronous I/O. +/// +/// If possible, prefer the async version +pub fn verify_azure_attestation_sync( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, ) -> Result { let PreparedAzureAttestation { @@ -148,13 +114,14 @@ fn verify_azure_attestation_with_given_timestamp_sync( tpm_attestation, } = prepare_azure_attestation(input)?; - let (dcap, _) = verify_dcap_attestation_with_timestamp_sync( + let (dcap, _) = verify_quote_sync( tdx_quote_bytes, expected_tdx_input_data, + mode, pccs, - collateral, - now, override_azure_outdated_tcb, + &QuoteVerifier::new_prod(), + None, )?; let measurements = finish_azure_attestation_verification( @@ -162,7 +129,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( var_data_hash, tpm_attestation, expected_input_data, - now, + dcap.endorsements.at, )?; Ok(VerifiedAttestation { measurements, @@ -357,10 +324,9 @@ impl RsaPubKey { #[cfg(test)] mod tests { - use dcap_qvl::QuoteCollateralV3; use super::{super::MAX_AZURE_ATTESTATION_PAYLOAD_SIZE, *}; - use crate::EndorsementSnapshot; + use crate::{EndorsementSnapshot, QuoteCollateralV3}; fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] { let attestation_document: AttestationDocument = @@ -397,8 +363,9 @@ mod tests { } /// All verification entry points must reject an oversized payload, and - /// must do so before attempting DCAP verification (no collateral or - /// usable PCCS is provided here). + /// must do so before attempting DCAP verification. [VerifyMode::Live] + /// with no PCCS is the strict case: were the size gate to miss, the + /// verification would reach out to Intel. #[tokio::test] async fn verify_rejects_oversized_payload_before_deserialize() { let actual = MAX_AZURE_ATTESTATION_PAYLOAD_SIZE + 1; @@ -407,6 +374,7 @@ mod tests { let err = verify_azure_attestation( input.clone(), [0; 64], + VerifyMode::Live, Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::Passthrough, @@ -418,41 +386,13 @@ mod tests { assert_payload_too_large(err, actual); let err = verify_azure_attestation_sync( - input.clone(), - [0; 64], - Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::OnDemand, - ), - false, - ) - .unwrap_err(); - assert_payload_too_large(err, actual); - - let err = verify_azure_attestation_with_given_timestamp( - input.clone(), - [0; 64], - Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::Passthrough, - ), - None, - 0, - false, - ) - .await - .unwrap_err(); - assert_payload_too_large(err, actual); - - let err = verify_azure_attestation_with_given_timestamp_sync( input, [0; 64], + VerifyMode::Live, Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::OnDemand, ), - None, - 0, false, ) .unwrap_err(); @@ -501,15 +441,14 @@ mod tests { measurements: async_measurements, endorsements: async_endorsements, .. - } = verify_azure_attestation_with_given_timestamp( + } = verify_azure_attestation( attestation_json.clone(), [0; 64], + VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::Passthrough, ), - Some(fixture_collateral.clone()), - now, false, ) .await @@ -519,23 +458,23 @@ mod tests { measurements: sync_measurements, endorsements: sync_endorsements, .. - } = verify_azure_attestation_with_given_timestamp_sync( + } = verify_azure_attestation_sync( attestation_json, [0; 64], + VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::OnDemand, ), - 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 + // The bundle handed back is the one the verification consumed, + // which is what makes archiving it provenance rather than a + // second copy, and it arrives paired with the instant it + // was held to let expected = EndorsementSnapshot::dcap(fixture_collateral, now); assert_eq!(async_endorsements, expected); assert_eq!(sync_endorsements, expected); @@ -558,15 +497,14 @@ mod tests { ) .unwrap(); - let err = verify_azure_attestation_with_given_timestamp( + let err = verify_azure_attestation( attestation_json, expected_input_data, + VerifyMode::Archived(EndorsementSnapshot::dcap(collateral, now)), Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::Passthrough, ), - Some(collateral), - now, false, ) .await diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 98f96e4..0e290f6 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -10,6 +10,7 @@ use dcap_qvl::{ intel::{quote_ca, quote_fmspc}, quote::{Quote, Report}, tcb_info::TcbInfo, + verify::QuoteVerifier, }; #[cfg(any(test, feature = "mock"))] use mock_tdx::generate_mock_tdx_quote; @@ -22,6 +23,7 @@ use crate::{ AttestationError, EndorsementSnapshot, VerifiedAttestation, + VerifyMode, measurements::MultiMeasurements, }; @@ -37,28 +39,49 @@ pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, Attestat } /// Verify a DCAP TDX quote +/// +/// `pccs` only matters on [VerifyMode::Live]: collateral comes from it. +/// [VerifyMode::Archived] carries its own bundle and never consults it. #[cfg(not(any(test, feature = "mock")))] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, + pccs: Pccs, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + verify_quote(input, expected_input_data, mode, pccs, false, &QuoteVerifier::new_prod(), None) + .await +} + +/// Verify a quote minted by [mock_tdx], which chains to the mock root CA +/// +/// With neither a pinned bundle nor a PCCS this verifies against the +/// embedded mock collateral, which is what lets a mock build run with no +/// network at all. +#[cfg(any(test, feature = "mock"))] +pub async fn verify_dcap_attestation( + input: Vec, + expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, ) -> 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( + verify_quote( input, expected_input_data, + mode, pccs, - None, - now, - override_azure_outdated_tcb, + false, + &mock_tdx::mock_dcap_verifier(), + Some(mock_tdx::mock_collateral()), ) .await } /// Synchronous version - verify a DCAP TDX quote /// -/// This relies on having DCAP collateral already present in the cache +/// `pccs` only matters on [VerifyMode::Live], and then the collateral has +/// to be in its cache already. [VerifyMode::Archived] carries its own +/// bundle and never consults it. /// /// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not /// supported because @@ -69,100 +92,152 @@ pub async fn verify_dcap_attestation( pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, ) -> 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( + verify_quote_sync( input, expected_input_data, + mode, pccs, + false, + &QuoteVerifier::new_prod(), None, - now, - override_azure_outdated_tcb, ) } -/// 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 +/// Synchronous version - verify a quote minted by [mock_tdx] +#[cfg(any(test, feature = "mock"))] +pub fn verify_dcap_attestation_sync( + input: Vec, + expected_input_data: [u8; 64], + mode: VerifyMode, + pccs: Pccs, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + verify_quote_sync( + input, + expected_input_data, + mode, + pccs, + false, + &mock_tdx::mock_dcap_verifier(), + Some(mock_tdx::mock_collateral()), + ) +} + +/// The collateral a DCAP verification runs against, or `None` to fetch it, +/// and the instant to evaluate freshness at /// -/// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not -/// supported unless `collateral` is provided. +/// The one place a verification reads the wall clock. An archived snapshot +/// has to carry a DCAP bundle: completing one with a fetch would evaluate +/// live collateral at a pinned instant, which is neither mode. +fn resolve_mode( + mode: VerifyMode, +) -> Result<(Option, u64), DcapVerificationError> { + match mode { + VerifyMode::Live => { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?; + Ok((None, now.as_secs())) + } + VerifyMode::Archived(EndorsementSnapshot { at, dcap: Some(collateral) }) => { + Ok((Some(collateral), at)) + } + VerifyMode::Archived(EndorsementSnapshot { dcap: None, .. }) => { + Err(DcapVerificationError::ArchivedWithoutDcapCollateral) + } + } +} + +/// Resolve the collateral a verification runs against, then verify /// -/// If possible, prefer the async version -pub fn verify_dcap_attestation_with_timestamp_sync( - input: Vec, +/// Every root goes through here: the public entry points pick one per +/// build, while the Azure verifier and the fixture tests replaying real +/// captures pass Intel's, whatever the build. `override_azure_outdated_tcb` +/// is the TCB relaxation the Azure verifier applies to the quote inside an +/// HCL report. `fallback_collateral` is the bundle of last resort, used +/// when the mode pins none and the PCCS is passthrough; `None` fetches +/// through the PCCS. +pub(crate) async fn verify_quote( + raw_quote: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, + fallback_collateral: Option, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let quote = Quote::parse(&input)?; - + let (pinned_collateral, now) = resolve_mode(mode)?; + let quote = Quote::parse(&raw_quote)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); - let collateral = if let Some(given_collateral) = collateral { - given_collateral + let collateral = if let Some(pinned_collateral) = pinned_collateral { + pinned_collateral + } else if let (true, Some(fallback_collateral)) = (pccs.is_passthrough(), fallback_collateral) { + fallback_collateral } else { - pccs.get_collateral_sync(fmspc.clone(), ca, now)? + let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; + collateral }; - verify_dcap_attestation_with_collateral_and_timestamp( - input, + verify_quote_with_collateral( + raw_quote, quote, expected_input_data, collateral, now, override_azure_outdated_tcb, + verifier, ) } -/// Allows the timestamp to be given, making it possible to test with -/// existing attestations +/// [verify_quote], for a caller with no async runtime /// -/// If collateral is given, it is used instead of contacting PCCS (used in -/// tests) -pub async fn verify_dcap_attestation_with_given_timestamp( - input: Vec, +/// On [VerifyMode::Live] the collateral has to be in the PCCS cache +/// already: there is no fetch of last resort here, only +/// `fallback_collateral` when the PCCS is passthrough. +pub(crate) fn verify_quote_sync( + raw_quote: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, + fallback_collateral: Option, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let quote = Quote::parse(&input)?; - + let (pinned_collateral, now) = resolve_mode(mode)?; + let quote = Quote::parse(&raw_quote)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); - let collateral = if let Some(given_collateral) = collateral { - given_collateral + let collateral = if let Some(pinned_collateral) = pinned_collateral { + pinned_collateral + } else if let (true, Some(fallback_collateral)) = (pccs.is_passthrough(), fallback_collateral) { + fallback_collateral } else { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; - collateral + pccs.get_collateral_sync(fmspc, ca, now)? }; - verify_dcap_attestation_with_collateral_and_timestamp( - input, + verify_quote_with_collateral( + raw_quote, quote, expected_input_data, collateral, now, override_azure_outdated_tcb, + verifier, ) } -fn verify_dcap_attestation_with_collateral_and_timestamp( +/// Verify a quote against collateral already in hand, at a given instant +fn verify_quote_with_collateral( raw_quote: Vec, quote: Quote, expected_input_data: [u8; 64], collateral: QuoteCollateralV3, now: u64, override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { tracing::info!("Verifying DCAP attestation: {quote:?}"); @@ -186,7 +261,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( |tcb_info: TcbInfo| tcb_info }; - let verified_report = dcap_qvl::verify::dangerous_verify_with_tcb_override( + let verified_report = verifier.dangerous_verify_with_tcb_override( &raw_quote, &collateral, now, @@ -218,75 +293,6 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( )) } -#[cfg(any(test, feature = "mock"))] -pub async fn verify_dcap_attestation( - input: Vec, - expected_input_data: [u8; 64], - pccs: Pccs, -) -> 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)?); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); - - let collateral = if pccs.is_passthrough() { - mock_tdx::mock_collateral() - } else { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?; - collateral - }; - let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; - - let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - if get_quote_input_data("e.report) != expected_input_data { - return Err(DcapVerificationError::InputMismatch); - } - - Ok(( - VerifiedAttestation { - measurements, - expected_measurements: None, - endorsements: EndorsementSnapshot::dcap(collateral, now), - }, - quote, - )) -} - -#[cfg(any(test, feature = "mock"))] -pub fn verify_dcap_attestation_sync( - input: Vec, - expected_input_data: [u8; 64], - pccs: Pccs, -) -> 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)?); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); - - let collateral = if pccs.is_passthrough() { - mock_tdx::mock_collateral() - } else { - pccs.get_collateral_sync(fmspc, ca, now)? - }; - - let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; - - let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - if get_quote_input_data("e.report) != expected_input_data { - return Err(DcapVerificationError::InputMismatch); - } - Ok(( - VerifiedAttestation { - measurements, - expected_measurements: None, - endorsements: EndorsementSnapshot::dcap(collateral, now), - }, - quote, - )) -} - /// Create a mock quote for testing on non-confidential hardware #[cfg(any(test, feature = "mock"))] fn generate_quote(input: [u8; 64]) -> Result, AttestationError> { @@ -323,6 +329,8 @@ pub enum DcapVerificationError { Pccs(#[from] PccsError), #[error("Timestamp exceeds i64 range")] TimeStampExceedsI64, + #[error("Archived snapshot carries no DCAP collateral to replay the quote against")] + ArchivedWithoutDcapCollateral, } #[cfg(test)] @@ -332,6 +340,36 @@ mod tests { use super::*; use crate::measurements::MeasurementPolicy; + /// An archived snapshot without a bundle is refused up front, before + /// the quote is even parsed: completing it with a fetch would evaluate + /// live collateral at a pinned instant, which is neither mode + #[tokio::test] + async fn archived_without_collateral_is_refused() { + let mode = VerifyMode::Archived(EndorsementSnapshot { at: 0, dcap: None }); + + let err = verify_dcap_attestation( + Vec::new(), + [0; 64], + mode.clone(), + Pccs::new( + CollateralSource::IntelPcs { subscription_key: None }, + CachePolicy::Passthrough, + ), + ) + .await + .unwrap_err(); + assert!(matches!(err, DcapVerificationError::ArchivedWithoutDcapCollateral), "{err:?}"); + + let err = verify_dcap_attestation_sync( + Vec::new(), + [0; 64], + mode, + Pccs::new(CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::OnDemand), + ) + .unwrap_err(); + assert!(matches!(err, DcapVerificationError::ArchivedWithoutDcapCollateral), "{err:?}"); + } + #[tokio::test] async fn test_dcap_verify() { let attestation_bytes: &'static [u8] = @@ -365,7 +403,7 @@ mod tests { serde_saphyr::from_slice(collateral_bytes).unwrap(); let (VerifiedAttestation { measurements: async_measurements, endorsements, .. }, _) = - verify_dcap_attestation_with_given_timestamp( + verify_quote( attestation_bytes.to_vec(), [ 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, @@ -373,35 +411,33 @@ mod tests { 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, ], + VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), Pccs::new( CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::Passthrough, ), - Some(fixture_collateral.clone()), - now, false, + &QuoteVerifier::new_prod(), + None, ) .await .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( - CollateralSource::IntelPcs { subscription_key: None }, - CachePolicy::OnDemand, - ), - Some(fixture_collateral.clone()), - now, - false, - ) - .unwrap(); + let (VerifiedAttestation { measurements: sync_measurements, .. }, _) = verify_quote_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, + ], + VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), + Pccs::new(CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::OnDemand), + false, + &QuoteVerifier::new_prod(), + None, + ) + .unwrap(); assert_eq!(async_measurements, sync_measurements); // A caller archiving provenance gets back the bundle the @@ -433,20 +469,21 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - verify_dcap_attestation_with_given_timestamp( + verify_quote( attestation_bytes.to_vec(), [ 210, 20, 43, 100, 53, 152, 235, 95, 174, 43, 200, 82, 157, 215, 154, 85, 139, 41, 248, 104, 204, 187, 101, 49, 203, 40, 218, 185, 220, 228, 119, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], + VerifyMode::Archived(EndorsementSnapshot::dcap(collateral, now)), Pccs::new( CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::Passthrough, ), - Some(collateral), - now, true, + &QuoteVerifier::new_prod(), + None, ) .await .unwrap(); @@ -468,7 +505,9 @@ mod tests { let quote = create_dcap_attestation(expected_input_data).unwrap(); let (verified, _) = - verify_dcap_attestation(quote, expected_input_data, pccs).await.unwrap(); + verify_dcap_attestation(quote, expected_input_data, VerifyMode::Live, pccs) + .await + .unwrap(); assert_eq!(verified.measurements, crate::measurements::mock_dcap_measurements()); assert_eq!(mock_pcs.tcb_call_count(), 1); diff --git a/crates/attestation/src/gcp/firmware.rs b/crates/attestation/src/gcp/firmware.rs index e57b2e1..417c784 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -73,13 +73,15 @@ pub(crate) enum GcpFirmwareCacheError { mod tests { use attest_measure::dcap::DcapFirmware; use attest_types::{AcpiHashes, DcapImageHashes}; - use dcap_qvl::quote::Quote; + use dcap_qvl::{quote::Quote, verify::QuoteVerifier}; use super::GcpFirmwareCache; use crate::{ + EndorsementSnapshot, PlatformMetadata, VerifiedAttestation, - dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp}, + VerifyMode, + dcap::{get_quote_input_data, verify_quote}, measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, }; @@ -156,20 +158,23 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap(); - let (VerifiedAttestation { measurements, .. }, _) = - verify_dcap_attestation_with_given_timestamp( - attestation_bytes.to_vec(), - expected_input_data, - pccs::Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::Passthrough, - ), - Some(collateral), + let (VerifiedAttestation { measurements, .. }, _) = verify_quote( + attestation_bytes.to_vec(), + expected_input_data, + VerifyMode::Archived(EndorsementSnapshot::dcap( + collateral, GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, - false, - ) - .await - .unwrap(); + )), + pccs::Pccs::new( + pccs::CollateralSource::IntelPcs { subscription_key: None }, + pccs::CachePolicy::Passthrough, + ), + false, + &QuoteVerifier::new_prod(), + None, + ) + .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 44ce5a2..93204a5 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -22,8 +22,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 +/// Re-exported so callers can archive [EndorsementSnapshot::dcap] and +/// replay it through [VerifyMode::Archived] without depending on `dcap-qvl` +/// directly pub use dcap_qvl::QuoteCollateralV3; use measurements::{ExpectedMeasurements, MultiMeasurements}; use parity_scale_codec::{Decode, Encode}; @@ -386,6 +387,29 @@ impl EndorsementSnapshot { } } +/// Where one verification gets its endorsements, and the instant it +/// evaluates freshness at +/// +/// This is a per-verification fact rather than verifier configuration: a +/// relying party re-checking archived evidence pins both to when that +/// evidence was collected, while a live handshake through the same verifier +/// does not. +// The snapshot makes `Archived` far larger than an empty `Live`. A mode is +// built once, passed once and dropped; boxing it would cost a `Box::new` at +// every call site for a value that is never stored. +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum VerifyMode { + /// Fetch whatever endorsements the evidence needs, and evaluate every + /// freshness check at the wall clock + Live, + /// Verify against a pinned [EndorsementSnapshot]: the endorsements as + /// of the instant they were held to, with nothing fetched. A leg the + /// snapshot carries no endorsements for is refused rather than + /// completed by a fetch + Archived(EndorsementSnapshot), +} + /// Evidence whose authenticity a Verifier established, with what it was /// established against /// @@ -631,10 +655,21 @@ impl AttestationVerifier { /// Verify an attestation, and return the expected measurements from the /// matching policy record. + /// + /// [VerifyMode::Live] fetches endorsements and evaluates every + /// freshness check at the wall clock. [VerifyMode::Archived] + /// verifies the DCAP quote, and on Azure the AK certificate chain, + /// as of a given instant with nothing fetched. Two GCP checks stay + /// live in either mode, since neither rests on signed material a + /// replay could re-verify: + /// + /// - the host provenance lookup against Google's PPID registry + /// - the firmware fetch for the quote's MRTD, on a cache miss pub async fn verify_attestation( &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], + mode: VerifyMode, ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -664,6 +699,7 @@ impl AttestationVerifier { azure::verify_azure_attestation( attestation_evidence.quote.clone(), expected_input_data, + mode, self.internal_pccs.clone(), self.override_azure_outdated_tcb, ) @@ -682,6 +718,7 @@ impl AttestationVerifier { let (verified, quote) = dcap::verify_dcap_attestation( attestation_evidence.quote.clone(), expected_input_data, + mode, self.internal_pccs.clone(), ) .await?; @@ -742,6 +779,7 @@ impl AttestationVerifier { &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], + mode: VerifyMode, ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -771,6 +809,7 @@ impl AttestationVerifier { azure::verify_azure_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, + mode, self.internal_pccs.clone(), self.override_azure_outdated_tcb, )? @@ -790,6 +829,7 @@ impl AttestationVerifier { let (verified, quote) = dcap::verify_dcap_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, + mode, pccs, )?; if attestation_type == AttestationType::GcpTdx { @@ -1194,9 +1234,18 @@ mod tests { let input_data = [0u8; 64]; assert!( - verifier.verify_attestation(attestation.clone(), input_data).await.unwrap().is_none() + verifier + .verify_attestation(attestation.clone(), input_data, VerifyMode::Live) + .await + .unwrap() + .is_none() + ); + assert!( + verifier + .verify_attestation_sync(attestation, input_data, VerifyMode::Live) + .unwrap() + .is_none() ); - assert!(verifier.verify_attestation_sync(attestation, input_data).unwrap().is_none()); } #[tokio::test] @@ -1211,8 +1260,9 @@ mod tests { let verifier = AttestationVerifier::mock(); let message: AttestationExchangeMessage = attestation_evidence.into(); - let async_result = verifier.verify_attestation(message.clone(), input_data).await; - let sync_result = verifier.verify_attestation_sync(message, input_data); + let async_result = + verifier.verify_attestation(message.clone(), input_data, VerifyMode::Live).await; + let sync_result = verifier.verify_attestation_sync(message, input_data, VerifyMode::Live); assert!( matches!( @@ -1255,7 +1305,7 @@ mod tests { let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); let verified = verifier - .verify_attestation(attestation_evidence.into(), input_data) + .verify_attestation(attestation_evidence.into(), input_data, VerifyMode::Live) .await .unwrap() .expect("mock evidence carries an attestation"); @@ -1288,14 +1338,17 @@ mod tests { let input_data = [0; 64]; assert!(matches!( - verifier.verify_attestation_sync(message.clone(), input_data), + verifier.verify_attestation_sync(message.clone(), input_data, VerifyMode::Live), Err(AttestationError::AttestationTypeNotAccepted) )); let generation = verifier_clone.measurement_policy_read().generation; verifier_clone.set_measurement_policy(MeasurementPolicy::expect_none(), generation); - assert!(matches!(verifier.verify_attestation_sync(message, input_data), Ok(None))); + assert!(matches!( + verifier.verify_attestation_sync(message, input_data, VerifyMode::Live), + Ok(None) + )); } #[test] @@ -1336,7 +1389,7 @@ mod tests { tokio::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).await.unwrap(); let verified = verifier - .verify_attestation(attestation.into(), input_data) + .verify_attestation(attestation.into(), input_data, VerifyMode::Live) .await .unwrap() .expect("mock evidence carries an attestation"); @@ -1444,7 +1497,7 @@ mod tests { std::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).unwrap(); let verified = verifier - .verify_attestation_sync(attestation.into(), input_data) + .verify_attestation_sync(attestation.into(), input_data, VerifyMode::Live) .unwrap() .expect("mock evidence carries an attestation"); diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 46a9b19..7df1459 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -13,6 +13,7 @@ pub use attestation::{ AttestationType, AttestationVerifier, PlatformMetadata, + VerifyMode, }; use ra_tls::{ attestation::{Attestation, AttestationQuote, VersionedAttestation}, @@ -676,7 +677,7 @@ impl AttestedCertificateVerifier { let attestation = Self::extract_custom_attestation_from_cert(cert)?; self.attestation_verifier - .verify_attestation_sync(attestation, expected_input_data) + .verify_attestation_sync(attestation, expected_input_data, VerifyMode::Live) .map_err(|err| { tracing::warn!( "Rejecting certificate after attestation verification failure: {err}" From e955a9499212d51014fd431d0dfff6351e12ff65 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere <9342524+samlaf@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:14:33 -0400 Subject: [PATCH 2/2] refactor(attestation): make archived replay a separate method, not a VerifyMode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on flashbots/attested-tls#93 asked for the archived case to be its own method rather than an enum threaded through verify_attestation. This does that, and drops the mode everywhere it had reached. API --- AttestationVerifier verify_attestation(msg, input, mode) -> verify_attestation(msg, input) as on main verify_attestation_sync(msg, input, mode) -> verify_attestation_sync(msg, input) as on main verify_attestation_archived(msg, input, &EndorsementSnapshot) new dcap verify_dcap_attestation(q, input, mode, pccs) -> verify_dcap_attestation(q, input, pccs) as on main verify_dcap_attestation_sync(q, input, mode, pccs) -> verify_dcap_attestation_sync(q, input, pccs) as on main verify_dcap_attestation_archived(q, input, &snapshot) new azure verify_azure_attestation(a, input, mode, pccs, override) -> (…, pccs, override) as on main verify_azure_attestation_sync(a, input, mode, pccs, override) -> (…, pccs, override) as on main verify_azure_attestation_archived(a, input, &snapshot, override) new enum VerifyMode removed DcapVerificationError::ArchivedWithoutDcapCollateral kept Why a method ------------ Live verification and archive replay are different operations a relying party does in different places, never both per call, so a parameter that selects between them buys nothing and costs every live caller a VerifyMode::Live. Keeping them apart leaves the live signatures exactly as main has them and lets the replay state its own contract. The snapshot stays the input. The archived methods take the EndorsementSnapshot that #85 hands back, not bare collateral, so the bundle and the instant it was held to travel together. A snapshot with no DCAP bundle is still refused with ArchivedWithoutDcapCollateral rather than completed by a fetch. This is deliberately not the case where collateral arrives out of band and is checked at the wall clock, as flashbots/attested-tls#65 proposes; that would be a live method. Synchronous. A replay fetches nothing time-dependent, so it needs no runtime and has no async twin. The Azure AK chain is checked at the snapshot's instant too, so one instant sits behind every freshness check. GCP --- The previous version kept the provenance lookup live in archived mode and left "should it?" open. It is now skipped on replay. The registry is unsigned and mutable, so a replay could only learn what it says today, and if an entry disappeared a sound archive would stop verifying, which is the failure the archive exists to prevent. The original verification already consulted it. Firmware for a portable-image policy is still fetched on a cache miss: it is signed by Google and content-addressed by the quote's MRTD, so the fetch cannot change the verdict. Recording the provenance outcome and the firmware in the snapshot would remove both caveats and is left for a later change. Internals --------- resolve_mode and the fallback-collateral plumbing go. The three crate-private bodies verify_quote, verify_quote_sync and verify_quote_archived each know their own source and reach one collateral-in-hand verifier, still parameterised on the dcap-qvl QuoteVerifier so mock builds replay against the mock root. The mock live entry points with a passthrough PCCS reuse the archived body with the embedded bundle at the wall clock, which is what they always did in effect. The Azure verifier splits its prepared evidence into the TD quote and the vTPM leg, so its three entry points share everything but the DCAP call, and the finish function now assembles the VerifiedAttestation and reads the instant from the DCAP leg in one place. The two verifier methods that need no runtime share one body that differs only in where the DCAP leg gets its endorsements and whether provenance runs. Module docs on dcap and azure::verify say which situation each entry point is for before saying how they differ. The README gains a paragraph on the snapshot and the archived method. Tests: the fixture replays move from the removed variants onto the archived path; VerifyMode::Live disappears from every live call; two new verifier-level tests replay a live mock verdict and check that a GCP replay does not consult the registry. attested-tls and attestation-provider-server are back to their main versions. Co-Authored-By: Claude Fable 5.1 --- crates/attestation-provider-server/src/lib.rs | 6 +- crates/attestation/README.md | 8 + crates/attestation/src/azure/attester/mod.rs | 8 +- crates/attestation/src/azure/mod.rs | 7 +- crates/attestation/src/azure/verify.rs | 267 +++++++------- crates/attestation/src/dcap.rs | 339 +++++++++--------- crates/attestation/src/gcp/firmware.rs | 22 +- crates/attestation/src/lib.rs | 239 ++++++++---- crates/attested-tls/src/lib.rs | 3 +- 9 files changed, 477 insertions(+), 422 deletions(-) diff --git a/crates/attestation-provider-server/src/lib.rs b/crates/attestation-provider-server/src/lib.rs index 9bd7e11..a167b37 100644 --- a/crates/attestation-provider-server/src/lib.rs +++ b/crates/attestation-provider-server/src/lib.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; pub use attestation::AttestationGenerator; -use attestation::{AttestationError, AttestationExchangeMessage, AttestationVerifier, VerifyMode}; +use attestation::{AttestationError, AttestationExchangeMessage, AttestationVerifier}; use axum::{ extract::{Path, State}, http::StatusCode, @@ -61,9 +61,7 @@ pub async fn attestation_provider_client( println!("Remote attestation type: {remote_attestation_type}"); - attestation_verifier - .verify_attestation(remote_attestation_message.clone(), input_data, VerifyMode::Live) - .await?; + attestation_verifier.verify_attestation(remote_attestation_message.clone(), input_data).await?; Ok(remote_attestation_message) } diff --git a/crates/attestation/README.md b/crates/attestation/README.md index 2af5194..54a138f 100644 --- a/crates/attestation/README.md +++ b/crates/attestation/README.md @@ -21,6 +21,14 @@ returns `ExpectedMeasurements::Image`, while an allow-any DCAP policy returns `ExpectedMeasurements::Dcap` with an empty register map. Successful verification without attestation returns `None`. +`VerifiedAttestation` also carries an `EndorsementSnapshot`: the DCAP +collateral bundle the verification consumed and the instant its freshness +checks were evaluated at. Archived beside the evidence, it lets +`AttestationVerifier::verify_attestation_archived` reproduce the verdict later, +against that bundle and at that instant, without fetching anything whose answer +could have changed since. This is for re-verifying archived evidence, not for +verifying live evidence with collateral obtained out of band. + Matched expected measurements can be transported in an HTTP header using `ExpectedMeasurements::to_header_format` and reconstructed with `ExpectedMeasurements::from_header_format`. See diff --git a/crates/attestation/src/azure/attester/mod.rs b/crates/attestation/src/azure/attester/mod.rs index 04fb3b9..0943852 100644 --- a/crates/attestation/src/azure/attester/mod.rs +++ b/crates/attestation/src/azure/attester/mod.rs @@ -47,6 +47,10 @@ const AIA_CA_ISSUERS_ACCESS_METHOD_OID: &str = "1.3.6.1.5.5.7.48.2"; /// need network access or AIA-fetching logic. This keeps verification /// deterministic and easier to reuse in constrained verifier environments /// such as TEEs, onchain verification, or zero-knowledge proof generation. +fn unix_time_now_secs() -> Result { + Ok(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs()) +} + pub fn create_azure_attestation(input_data: [u8; 64]) -> Result, MaaError> { let hcl_report_bytes = vtpm::get_report_with_report_data(&input_data)?; @@ -150,10 +154,6 @@ impl TryFrom<&vtpm::Quote> for TpmQuote { } } -fn unix_time_now_secs() -> Result { - Ok(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs()) -} - /// Fetch intermediate certificates from the Authority Information Access /// (AIA) CA Issuers URLs in the leaf and each fetched intermediate. /// diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index 3527d56..0a53029 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -15,7 +15,12 @@ use thiserror::Error; use tpm_quote::TpmQuote; pub use tpm_quote::TpmQuoteError; pub use tpms_attest::AttestError; -pub use verify::{get_measurements, verify_azure_attestation, verify_azure_attestation_sync}; +pub use verify::{ + get_measurements, + verify_azure_attestation, + verify_azure_attestation_archived, + verify_azure_attestation_sync, +}; /// The attestation evidence payload that gets sent over the channel #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/attestation/src/azure/verify.rs b/crates/attestation/src/azure/verify.rs index 5e563db..039cc78 100644 --- a/crates/attestation/src/azure/verify.rs +++ b/crates/attestation/src/azure/verify.rs @@ -2,6 +2,27 @@ //! computation over the evidence bytes: DCAP verification of the TDX quote, //! HCL report binding checks, vTPM quote verification, and AK certificate //! chain verification against pinned Azure roots. +//! +//! Three entry points, one per situation a relying party is in: +//! +//! - [verify_azure_attestation] is for a live handshake with an async +//! runtime to hand: it fetches whatever collateral the quote needs and +//! judges freshness at the wall clock. +//! - [verify_azure_attestation_sync] is for a live handshake inside a +//! callback that cannot await, such as a rustls certificate verifier. It +//! can only read the PCCS cache, so the collateral has to be there +//! already; a miss fails and starts a background fetch for next time. +//! - [verify_azure_attestation_archived] is for re-checking evidence long +//! after the fact, against the [EndorsementSnapshot] its original +//! verification reported. It fetches nothing and judges freshness at the +//! snapshot's instant, so the verdict is the same however much later it +//! runs. +//! +//! They share one body and differ only in the DCAP leg, which is where +//! collateral and the instant come from. Parsing before it and the vTPM leg +//! after it are identical, and the vTPM leg takes its instant from whatever +//! the DCAP leg reported, so the archived path pins the AK chain check +//! without any code of its own. use az_cvm_vtpm::{hcl, tdx}; use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE}; use dcap_qvl::verify::QuoteVerifier; @@ -19,81 +40,58 @@ use super::{ ensure_azure_attestation_payload_size, }; use crate::{ + EndorsementSnapshot, VerifiedAttestation, - VerifyMode, - dcap::{verify_quote, verify_quote_sync}, + dcap::{verify_quote, verify_quote_archived, verify_quote_sync}, measurements::MultiMeasurements, }; -/// Used during verification to support both sync and async verification -/// paths without duplicating code -struct PreparedAzureAttestation { +/// The TD quote inside an Azure attestation, and the input data it has to +/// commit to +/// +/// Split from [VtpmLeg] so the three verification entry points share +/// everything but the DCAP call +struct PreparedTdQuote { tdx_quote_bytes: Vec, + expected_tdx_input_data: [u8; 64], +} + +/// Everything the vTPM leg of an Azure verification needs once the TD quote +/// has been verified +struct VtpmLeg { hcl_report: hcl::HclReport, var_data_hash: [u8; 32], - expected_tdx_input_data: [u8; 64], tpm_attestation: TpmAttest, } /// Verify a TDX attestation from Azure /// -/// `mode` gates the DCAP leg and the vTPM leg alike: on -/// [VerifyMode::Archived] the AK certificate chain is checked as of the -/// same instant as the snapshot, and nothing reaches the network. -/// `pccs` only matters on [VerifyMode::Live]; see -/// [crate::dcap::verify_dcap_attestation]. +/// Collateral for the TD quote comes from `pccs`, and both legs are held +/// to the wall clock. To re-verify archived evidence, see +/// [verify_azure_attestation_archived]. pub async fn verify_azure_attestation( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, override_azure_outdated_tcb: bool, ) -> Result { - let PreparedAzureAttestation { - tdx_quote_bytes, - hcl_report, - var_data_hash, - expected_tdx_input_data, - tpm_attestation, - } = prepare_azure_attestation(input)?; - - // The DCAP leg reports the instant it evaluated at, so the vTPM leg - // below is held to the same one - on [VerifyMode::Live] the clock - // is read once, not once per leg. Only the endorsements travel - // upward: this platform is judged on the vTPM PCRs, not the TD - // quote + let (prepared, vtpm) = prepare_azure_attestation(input)?; + let (dcap, _) = verify_quote( - tdx_quote_bytes, - expected_tdx_input_data, - mode, + prepared.tdx_quote_bytes, + prepared.expected_tdx_input_data, pccs, override_azure_outdated_tcb, &QuoteVerifier::new_prod(), - None, ) .await?; - // 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, - dcap.endorsements.at, - )?; - Ok(VerifiedAttestation { - measurements, - expected_measurements: None, - endorsements: dcap.endorsements, - }) + finish_azure_attestation_verification(vtpm, expected_input_data, dcap) } /// Verify a TDX attestation from Azure - synchronous version /// -/// `pccs` only matters on [VerifyMode::Live], and then the collateral has -/// to be in its cache already; see -/// [crate::dcap::verify_dcap_attestation_sync]. +/// This relies on having DCAP collateral already present in the cache /// /// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not /// supported because fetching collateral requires asynchronous I/O. @@ -102,44 +100,51 @@ pub async fn verify_azure_attestation( pub fn verify_azure_attestation_sync( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, override_azure_outdated_tcb: bool, ) -> Result { - let PreparedAzureAttestation { - tdx_quote_bytes, - hcl_report, - var_data_hash, - expected_tdx_input_data, - tpm_attestation, - } = prepare_azure_attestation(input)?; + let (prepared, vtpm) = prepare_azure_attestation(input)?; let (dcap, _) = verify_quote_sync( - tdx_quote_bytes, - expected_tdx_input_data, - mode, + prepared.tdx_quote_bytes, + prepared.expected_tdx_input_data, pccs, override_azure_outdated_tcb, &QuoteVerifier::new_prod(), - None, )?; - let measurements = finish_azure_attestation_verification( - hcl_report, - var_data_hash, - tpm_attestation, - expected_input_data, - dcap.endorsements.at, + finish_azure_attestation_verification(vtpm, expected_input_data, dcap) +} + +/// Re-verify a TDX attestation from Azure against the endorsements a +/// previous verification reported +/// +/// The TD quote is checked against the snapshot's collateral bundle and the +/// AK certificate chain is checked at the snapshot's instant, so both legs +/// are held to the same instant as the original verification and nothing +/// is fetched. A snapshot with no DCAP bundle is refused; see +/// [crate::dcap::verify_dcap_attestation_archived]. +pub fn verify_azure_attestation_archived( + input: Vec, + expected_input_data: [u8; 64], + endorsements: &EndorsementSnapshot, + override_azure_outdated_tcb: bool, +) -> Result { + let (prepared, vtpm) = prepare_azure_attestation(input)?; + + let (dcap, _) = verify_quote_archived( + prepared.tdx_quote_bytes, + prepared.expected_tdx_input_data, + endorsements, + override_azure_outdated_tcb, + &QuoteVerifier::new_prod(), )?; - Ok(VerifiedAttestation { - measurements, - expected_measurements: None, - endorsements: dcap.endorsements, - }) + + finish_azure_attestation_verification(vtpm, expected_input_data, dcap) } /// Parses the attestation during verification -fn prepare_azure_attestation(input: Vec) -> Result { +fn prepare_azure_attestation(input: Vec) -> Result<(PreparedTdQuote, VtpmLeg), MaaError> { ensure_azure_attestation_payload_size(&input)?; let attestation_document: AttestationDocument = serde_json::from_slice(&input)?; @@ -157,23 +162,27 @@ fn prepare_azure_attestation(input: Vec) -> Result Result { + dcap: VerifiedAttestation, +) -> Result { + let VtpmLeg { hcl_report, var_data_hash, tpm_attestation } = vtpm; + let now = dcap.endorsements.at; let hcl_ak_pub = hcl_report.ak_pub()?; // Get attestation key from runtime claims @@ -240,7 +249,11 @@ fn finish_azure_attestation_verification( now, )?; - Ok(MultiMeasurements::from_indexed_pcrs(pcrs)) + Ok(VerifiedAttestation { + measurements: MultiMeasurements::from_indexed_pcrs(pcrs), + expected_measurements: None, + endorsements: dcap.endorsements, + }) } /// Extract the measurements from the attestation, but do not verify @@ -324,9 +337,10 @@ impl RsaPubKey { #[cfg(test)] mod tests { + use dcap_qvl::QuoteCollateralV3; use super::{super::MAX_AZURE_ATTESTATION_PAYLOAD_SIZE, *}; - use crate::{EndorsementSnapshot, QuoteCollateralV3}; + use crate::EndorsementSnapshot; fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] { let attestation_document: AttestationDocument = @@ -363,9 +377,8 @@ mod tests { } /// All verification entry points must reject an oversized payload, and - /// must do so before attempting DCAP verification. [VerifyMode::Live] - /// with no PCCS is the strict case: were the size gate to miss, the - /// verification would reach out to Intel. + /// must do so before attempting DCAP verification (no collateral or + /// usable PCCS is provided here). #[tokio::test] async fn verify_rejects_oversized_payload_before_deserialize() { let actual = MAX_AZURE_ATTESTATION_PAYLOAD_SIZE + 1; @@ -374,7 +387,6 @@ mod tests { let err = verify_azure_attestation( input.clone(), [0; 64], - VerifyMode::Live, Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::Passthrough, @@ -386,9 +398,8 @@ mod tests { assert_payload_too_large(err, actual); let err = verify_azure_attestation_sync( - input, + input.clone(), [0; 64], - VerifyMode::Live, Pccs::new( pccs::CollateralSource::IntelPcs { subscription_key: None }, pccs::CachePolicy::OnDemand, @@ -397,6 +408,15 @@ mod tests { ) .unwrap_err(); assert_payload_too_large(err, actual); + + let err = verify_azure_attestation_archived( + input, + [0; 64], + &EndorsementSnapshot { at: 0, dcap: None }, + false, + ) + .unwrap_err(); + assert_payload_too_large(err, actual); } #[tokio::test] @@ -415,8 +435,8 @@ mod tests { /// Verify a complete observed Azure attestation payload that includes /// AK intermediates fetched from the leaf certificate's AIA URLs. - #[tokio::test] - async fn test_verify() { + #[test] + fn test_verify() { // generated using the attester module's [capture_azure_fixture]. let attestation_bytes: &'static [u8] = include_bytes!("../../test-assets/azure-tdx-with-ak-intermediates-1780922561.yaml"); @@ -437,51 +457,19 @@ mod tests { let fixture_collateral: QuoteCollateralV3 = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let VerifiedAttestation { - measurements: async_measurements, - endorsements: async_endorsements, - .. - } = verify_azure_attestation( - attestation_json.clone(), - [0; 64], - VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), - Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::Passthrough, - ), - false, - ) - .await - .unwrap(); + let endorsements = EndorsementSnapshot::dcap(fixture_collateral, now); + let VerifiedAttestation { measurements, endorsements: reported, .. } = + verify_azure_attestation_archived(attestation_json, [0; 64], &endorsements, false) + .unwrap(); - let VerifiedAttestation { - measurements: sync_measurements, - endorsements: sync_endorsements, - .. - } = verify_azure_attestation_sync( - attestation_json, - [0; 64], - VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), - Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::OnDemand, - ), - false, - ) - .unwrap(); - - assert_eq!(async_measurements, sync_measurements); - // The bundle handed back is the one the verification consumed, - // which is what makes archiving it provenance rather than a - // second copy, and it arrives paired with the instant it - // was held to - let expected = EndorsementSnapshot::dcap(fixture_collateral, now); - assert_eq!(async_endorsements, expected); - assert_eq!(sync_endorsements, expected); + // The snapshot handed back is the one both legs were held to, so a + // replay of a replay reports the same thing + assert_eq!(reported, endorsements); + assert!(matches!(measurements, MultiMeasurements::Azure(_))); } - #[tokio::test] - async fn test_verify_fails_on_input_mismatch() { + #[test] + fn test_verify_fails_on_input_mismatch() { let attestation_bytes: &'static [u8] = include_bytes!("../../test-assets/azure-tdx-1764662251380464271.yaml"); let now = 1771423480; @@ -497,17 +485,12 @@ mod tests { ) .unwrap(); - let err = verify_azure_attestation( + let err = verify_azure_attestation_archived( attestation_json, expected_input_data, - VerifyMode::Archived(EndorsementSnapshot::dcap(collateral, now)), - Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::Passthrough, - ), + &EndorsementSnapshot::dcap(collateral, now), false, ) - .await .unwrap_err(); assert!(matches!(err, MaaError::ClaimsUserDataInputMismatch)); diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 0e290f6..39883e2 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -1,6 +1,24 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and //! verification //! +//! Three entry points, one per situation a relying party is in: +//! +//! - [verify_dcap_attestation] is for a live handshake with an async +//! runtime to hand: it fetches whatever collateral the quote needs and +//! judges freshness at the wall clock. +//! - [verify_dcap_attestation_sync] is for a live handshake inside a +//! callback that cannot await, such as a rustls certificate verifier. It +//! can only read the PCCS cache, so the collateral has to be there +//! already; a miss fails and starts a background fetch for next time. +//! - [verify_dcap_attestation_archived] is for re-checking evidence long +//! after the fact, against the [EndorsementSnapshot] its original +//! verification reported. It fetches nothing and judges freshness at the +//! snapshot's instant, so the verdict is the same however much later it +//! runs. +//! +//! They differ only in where collateral and the instant come from; the +//! verification itself is one function they all reach. +//! //! 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 @@ -23,7 +41,6 @@ use crate::{ AttestationError, EndorsementSnapshot, VerifiedAttestation, - VerifyMode, measurements::MultiMeasurements, }; @@ -40,145 +57,148 @@ pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, Attestat /// Verify a DCAP TDX quote /// -/// `pccs` only matters on [VerifyMode::Live]: collateral comes from it. -/// [VerifyMode::Archived] carries its own bundle and never consults it. +/// Collateral comes from `pccs`, and every freshness check is evaluated +/// at the wall clock. To re-verify archived evidence, see +/// [verify_dcap_attestation_archived]. #[cfg(not(any(test, feature = "mock")))] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - verify_quote(input, expected_input_data, mode, pccs, false, &QuoteVerifier::new_prod(), None) - .await + verify_quote(input, expected_input_data, pccs, false, &QuoteVerifier::new_prod()).await } -/// Verify a quote minted by [mock_tdx], which chains to the mock root CA +/// Synchronous version - verify a DCAP TDX quote /// -/// With neither a pinned bundle nor a PCCS this verifies against the -/// embedded mock collateral, which is what lets a mock build run with no -/// network at all. -#[cfg(any(test, feature = "mock"))] -pub async fn verify_dcap_attestation( +/// This relies on having DCAP collateral already present in the cache +/// +/// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not +/// supported because fetching collateral requires asynchronous I/O. +/// +/// If possible, prefer the async version +#[cfg(not(any(test, feature = "mock")))] +pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - verify_quote( - input, - expected_input_data, - mode, - pccs, - false, - &mock_tdx::mock_dcap_verifier(), - Some(mock_tdx::mock_collateral()), - ) - .await + verify_quote_sync(input, expected_input_data, pccs, false, &QuoteVerifier::new_prod()) } -/// Synchronous version - verify a DCAP TDX quote -/// -/// `pccs` only matters on [VerifyMode::Live], and then the collateral has -/// to be in its cache already. [VerifyMode::Archived] carries its own -/// bundle and never consults it. +/// Re-verify a DCAP TDX quote against the endorsements a previous +/// verification reported /// -/// [`CachePolicy::Passthrough`](pccs::CachePolicy::Passthrough) is not -/// supported because -/// fetching collateral requires asynchronous I/O. +/// The quote is checked against the snapshot's collateral bundle, with +/// every freshness check evaluated at the snapshot's instant rather than +/// the wall clock, and nothing is fetched. Same evidence, same snapshot, +/// same verdict, however much later it runs. /// -/// If possible, prefer the async version +/// A snapshot with no DCAP bundle is refused rather than completed by a +/// fetch: that would evaluate live collateral at a pinned instant, which +/// reproduces nothing. #[cfg(not(any(test, feature = "mock")))] -pub fn verify_dcap_attestation_sync( +pub fn verify_dcap_attestation_archived( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, - pccs: Pccs, + endorsements: &EndorsementSnapshot, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - verify_quote_sync( + verify_quote_archived( input, expected_input_data, - mode, - pccs, + endorsements, false, &QuoteVerifier::new_prod(), - None, ) } +/// Verify a quote minted by [mock_tdx], which chains to the mock root CA +/// +/// With a passthrough PCCS this verifies against the embedded mock +/// collateral, which is what lets a mock build run with no network at all. +#[cfg(any(test, feature = "mock"))] +pub async fn verify_dcap_attestation( + input: Vec, + expected_input_data: [u8; 64], + pccs: Pccs, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + if pccs.is_passthrough() { + return verify_quote_archived( + input, + expected_input_data, + &mock_endorsements_now()?, + false, + &mock_tdx::mock_dcap_verifier(), + ); + } + verify_quote(input, expected_input_data, pccs, false, &mock_tdx::mock_dcap_verifier()).await +} + /// Synchronous version - verify a quote minted by [mock_tdx] #[cfg(any(test, feature = "mock"))] pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - verify_quote_sync( + if pccs.is_passthrough() { + return verify_quote_archived( + input, + expected_input_data, + &mock_endorsements_now()?, + false, + &mock_tdx::mock_dcap_verifier(), + ); + } + verify_quote_sync(input, expected_input_data, pccs, false, &mock_tdx::mock_dcap_verifier()) +} + +/// Re-verify a quote minted by [mock_tdx] against a reported snapshot +#[cfg(any(test, feature = "mock"))] +pub fn verify_dcap_attestation_archived( + input: Vec, + expected_input_data: [u8; 64], + endorsements: &EndorsementSnapshot, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + verify_quote_archived( input, expected_input_data, - mode, - pccs, + endorsements, false, &mock_tdx::mock_dcap_verifier(), - Some(mock_tdx::mock_collateral()), ) } -/// The collateral a DCAP verification runs against, or `None` to fetch it, -/// and the instant to evaluate freshness at -/// -/// The one place a verification reads the wall clock. An archived snapshot -/// has to carry a DCAP bundle: completing one with a fetch would evaluate -/// live collateral at a pinned instant, which is neither mode. -fn resolve_mode( - mode: VerifyMode, -) -> Result<(Option, u64), DcapVerificationError> { - match mode { - VerifyMode::Live => { - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?; - Ok((None, now.as_secs())) - } - VerifyMode::Archived(EndorsementSnapshot { at, dcap: Some(collateral) }) => { - Ok((Some(collateral), at)) - } - VerifyMode::Archived(EndorsementSnapshot { dcap: None, .. }) => { - Err(DcapVerificationError::ArchivedWithoutDcapCollateral) - } - } +/// The embedded mock collateral, held to the wall clock +#[cfg(any(test, feature = "mock"))] +fn mock_endorsements_now() -> Result { + Ok(EndorsementSnapshot::dcap(mock_tdx::mock_collateral(), unix_time_now_secs()?)) } -/// Resolve the collateral a verification runs against, then verify +fn unix_time_now_secs() -> Result { + Ok(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs()) +} + +/// Fetch collateral through the PCCS and verify at the wall clock /// -/// Every root goes through here: the public entry points pick one per -/// build, while the Azure verifier and the fixture tests replaying real -/// captures pass Intel's, whatever the build. `override_azure_outdated_tcb` -/// is the TCB relaxation the Azure verifier applies to the quote inside an -/// HCL report. `fallback_collateral` is the bundle of last resort, used -/// when the mode pins none and the PCCS is passthrough; `None` fetches -/// through the PCCS. +/// Every live verification goes through here or [verify_quote_sync]: the +/// public entry points pick the root per build, while the Azure verifier +/// passes Intel's whatever the build. `override_azure_outdated_tcb` is the +/// TCB relaxation the Azure verifier applies to the quote inside an HCL +/// report. pub(crate) async fn verify_quote( raw_quote: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, override_azure_outdated_tcb: bool, verifier: &QuoteVerifier, - fallback_collateral: Option, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let (pinned_collateral, now) = resolve_mode(mode)?; + let now = unix_time_now_secs()?; let quote = Quote::parse(&raw_quote)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); - let collateral = if let Some(pinned_collateral) = pinned_collateral { - pinned_collateral - } else if let (true, Some(fallback_collateral)) = (pccs.is_passthrough(), fallback_collateral) { - fallback_collateral - } else { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; - collateral - }; + let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?; verify_quote_with_collateral( raw_quote, @@ -193,30 +213,20 @@ pub(crate) async fn verify_quote( /// [verify_quote], for a caller with no async runtime /// -/// On [VerifyMode::Live] the collateral has to be in the PCCS cache -/// already: there is no fetch of last resort here, only -/// `fallback_collateral` when the PCCS is passthrough. +/// The collateral has to be in the PCCS cache already. pub(crate) fn verify_quote_sync( raw_quote: Vec, expected_input_data: [u8; 64], - mode: VerifyMode, pccs: Pccs, override_azure_outdated_tcb: bool, verifier: &QuoteVerifier, - fallback_collateral: Option, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let (pinned_collateral, now) = resolve_mode(mode)?; + let now = unix_time_now_secs()?; let quote = Quote::parse(&raw_quote)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); - let collateral = if let Some(pinned_collateral) = pinned_collateral { - pinned_collateral - } else if let (true, Some(fallback_collateral)) = (pccs.is_passthrough(), fallback_collateral) { - fallback_collateral - } else { - pccs.get_collateral_sync(fmspc, ca, now)? - }; + let collateral = pccs.get_collateral_sync(fmspc, ca, now)?; verify_quote_with_collateral( raw_quote, @@ -229,6 +239,33 @@ pub(crate) fn verify_quote_sync( ) } +/// Verify against a reported snapshot: its bundle, at its instant, with +/// nothing fetched +/// +/// The snapshot is checked before the quote is parsed, so a snapshot with +/// no DCAP bundle is refused up front. +pub(crate) fn verify_quote_archived( + raw_quote: Vec, + expected_input_data: [u8; 64], + endorsements: &EndorsementSnapshot, + override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + let collateral = + endorsements.dcap.clone().ok_or(DcapVerificationError::ArchivedWithoutDcapCollateral)?; + let quote = Quote::parse(&raw_quote)?; + + verify_quote_with_collateral( + raw_quote, + quote, + expected_input_data, + collateral, + endorsements.at, + override_azure_outdated_tcb, + verifier, + ) +} + /// Verify a quote against collateral already in hand, at a given instant fn verify_quote_with_collateral( raw_quote: Vec, @@ -340,38 +377,8 @@ mod tests { use super::*; use crate::measurements::MeasurementPolicy; - /// An archived snapshot without a bundle is refused up front, before - /// the quote is even parsed: completing it with a fetch would evaluate - /// live collateral at a pinned instant, which is neither mode - #[tokio::test] - async fn archived_without_collateral_is_refused() { - let mode = VerifyMode::Archived(EndorsementSnapshot { at: 0, dcap: None }); - - let err = verify_dcap_attestation( - Vec::new(), - [0; 64], - mode.clone(), - Pccs::new( - CollateralSource::IntelPcs { subscription_key: None }, - CachePolicy::Passthrough, - ), - ) - .await - .unwrap_err(); - assert!(matches!(err, DcapVerificationError::ArchivedWithoutDcapCollateral), "{err:?}"); - - let err = verify_dcap_attestation_sync( - Vec::new(), - [0; 64], - mode, - Pccs::new(CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::OnDemand), - ) - .unwrap_err(); - assert!(matches!(err, DcapVerificationError::ArchivedWithoutDcapCollateral), "{err:?}"); - } - - #[tokio::test] - async fn test_dcap_verify() { + #[test] + fn test_dcap_verify() { let attestation_bytes: &'static [u8] = include_bytes!("../test-assets/dcap-tdx-1766059550570652607"); @@ -402,28 +409,10 @@ mod tests { let fixture_collateral: QuoteCollateralV3 = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let (VerifiedAttestation { measurements: async_measurements, endorsements, .. }, _) = - verify_quote( - 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, - ], - VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), - Pccs::new( - CollateralSource::IntelPcs { subscription_key: None }, - CachePolicy::Passthrough, - ), - false, - &QuoteVerifier::new_prod(), - None, - ) - .await - .unwrap(); - - let (VerifiedAttestation { measurements: sync_measurements, .. }, _) = verify_quote_sync( + // A real Intel quote, so it is checked against Intel's root + // whatever the build: the public archived entry point would + // use the mock root under `test` + let (VerifiedAttestation { measurements, endorsements, .. }, _) = verify_quote_archived( attestation_bytes.to_vec(), [ 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, 227, @@ -431,32 +420,36 @@ mod tests { 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, ], - VerifyMode::Archived(EndorsementSnapshot::dcap(fixture_collateral.clone(), now)), - Pccs::new(CollateralSource::IntelPcs { subscription_key: None }, CachePolicy::OnDemand), + &EndorsementSnapshot::dcap(fixture_collateral.clone(), now), false, &QuoteVerifier::new_prod(), - None, ) .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); + // The snapshot handed back is the one the verification ran against, + // which is what lets a caller archive and replay it + assert_eq!(endorsements, EndorsementSnapshot::dcap(fixture_collateral, now)); let platform_metadata = crate::mock_platform_metadata(crate::AttestationType::DcapTdx).unwrap(); - measurement_policy - .check_measurement(&async_measurements, Some(&platform_metadata)) - .unwrap(); + measurement_policy.check_measurement(&measurements, Some(&platform_metadata)).unwrap(); + } + + /// An archived snapshot without a bundle is refused up front, before + /// the quote is even parsed: completing it with a fetch would evaluate + /// live collateral at a pinned instant, which reproduces nothing + #[test] + fn archived_without_collateral_is_refused() { + let endorsements = EndorsementSnapshot { at: 0, dcap: None }; + + let err = verify_dcap_attestation_archived(Vec::new(), [0; 64], &endorsements).unwrap_err(); + + assert!(matches!(err, DcapVerificationError::ArchivedWithoutDcapCollateral), "{err:?}"); } // This specifically tests a quote which has outdated TCB level from // Azure - #[tokio::test] - async fn test_dcap_verify_azure_override() { + #[test] + fn test_dcap_verify_azure_override() { let attestation_bytes: &'static [u8] = include_bytes!("../test-assets/azure_failed_dcap_quote_10.bin"); @@ -469,23 +462,17 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - verify_quote( + verify_quote_archived( attestation_bytes.to_vec(), [ 210, 20, 43, 100, 53, 152, 235, 95, 174, 43, 200, 82, 157, 215, 154, 85, 139, 41, 248, 104, 204, 187, 101, 49, 203, 40, 218, 185, 220, 228, 119, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], - VerifyMode::Archived(EndorsementSnapshot::dcap(collateral, now)), - Pccs::new( - CollateralSource::IntelPcs { subscription_key: None }, - CachePolicy::Passthrough, - ), + &EndorsementSnapshot::dcap(collateral, now), true, &QuoteVerifier::new_prod(), - None, ) - .await .unwrap(); } @@ -505,9 +492,7 @@ mod tests { let quote = create_dcap_attestation(expected_input_data).unwrap(); let (verified, _) = - verify_dcap_attestation(quote, expected_input_data, VerifyMode::Live, pccs) - .await - .unwrap(); + verify_dcap_attestation(quote, expected_input_data, pccs).await.unwrap(); assert_eq!(verified.measurements, crate::measurements::mock_dcap_measurements()); assert_eq!(mock_pcs.tcb_call_count(), 1); diff --git a/crates/attestation/src/gcp/firmware.rs b/crates/attestation/src/gcp/firmware.rs index 417c784..ae06e61 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -80,8 +80,7 @@ mod tests { EndorsementSnapshot, PlatformMetadata, VerifiedAttestation, - VerifyMode, - dcap::{get_quote_input_data, verify_quote}, + dcap::{get_quote_input_data, verify_quote_archived}, measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, }; @@ -142,8 +141,8 @@ mod tests { } } - #[tokio::test] - async fn test_gcp_tdx_portable_policy_with_stored_collateral() { + #[test] + fn test_gcp_tdx_portable_policy_with_stored_collateral() { let attestation_bytes: &'static [u8] = include_bytes!("../../test-assets/gcp-tdx-1782809233226668671"); let collateral_bytes: &'static [u8] = @@ -158,22 +157,15 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap(); - let (VerifiedAttestation { measurements, .. }, _) = verify_quote( + // A real GCP quote, so it is checked against Intel's root whatever + // the build + let (VerifiedAttestation { measurements, .. }, _) = verify_quote_archived( attestation_bytes.to_vec(), expected_input_data, - VerifyMode::Archived(EndorsementSnapshot::dcap( - collateral, - GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, - )), - pccs::Pccs::new( - pccs::CollateralSource::IntelPcs { subscription_key: None }, - pccs::CachePolicy::Passthrough, - ), + &EndorsementSnapshot::dcap(collateral, GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP), false, &QuoteVerifier::new_prod(), - None, ) - .await .unwrap(); let measurement_policy = MeasurementPolicy { diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 93204a5..8be629e 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -22,9 +22,8 @@ use std::{ use attest_measure::platform::PlatformError; pub use attest_types::{AttestationEvidence, PlatformMetadata}; -/// Re-exported so callers can archive [EndorsementSnapshot::dcap] and -/// replay it through [VerifyMode::Archived] without depending on `dcap-qvl` -/// directly +/// Re-exported so callers can archive [EndorsementSnapshot::dcap] without +/// depending on `dcap-qvl` directly pub use dcap_qvl::QuoteCollateralV3; use measurements::{ExpectedMeasurements, MultiMeasurements}; use parity_scale_codec::{Decode, Encode}; @@ -362,6 +361,10 @@ impl AttestationGenerator { /// instead. Hence a struct that grows fields rather than an enum keyed by /// platform, and `#[non_exhaustive]` to keep that growth additive. /// +/// Handed back by every verification and accepted back by +/// [`AttestationVerifier::verify_attestation_archived`], which re-verifies +/// the evidence against it with nothing fetched. +/// /// 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 @@ -387,29 +390,6 @@ impl EndorsementSnapshot { } } -/// Where one verification gets its endorsements, and the instant it -/// evaluates freshness at -/// -/// This is a per-verification fact rather than verifier configuration: a -/// relying party re-checking archived evidence pins both to when that -/// evidence was collected, while a live handshake through the same verifier -/// does not. -// The snapshot makes `Archived` far larger than an empty `Live`. A mode is -// built once, passed once and dropped; boxing it would cost a `Box::new` at -// every call site for a value that is never stored. -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum VerifyMode { - /// Fetch whatever endorsements the evidence needs, and evaluate every - /// freshness check at the wall clock - Live, - /// Verify against a pinned [EndorsementSnapshot]: the endorsements as - /// of the instant they were held to, with nothing fetched. A leg the - /// snapshot carries no endorsements for is refused rather than - /// completed by a fetch - Archived(EndorsementSnapshot), -} - /// Evidence whose authenticity a Verifier established, with what it was /// established against /// @@ -656,20 +636,14 @@ impl AttestationVerifier { /// Verify an attestation, and return the expected measurements from the /// matching policy record. /// - /// [VerifyMode::Live] fetches endorsements and evaluates every - /// freshness check at the wall clock. [VerifyMode::Archived] - /// verifies the DCAP quote, and on Azure the AK certificate chain, - /// as of a given instant with nothing fetched. Two GCP checks stay - /// live in either mode, since neither rests on signed material a - /// replay could re-verify: - /// - /// - the host provenance lookup against Google's PPID registry - /// - the firmware fetch for the quote's MRTD, on a cache miss + /// Endorsements are fetched and every freshness check is evaluated at + /// the wall clock. To re-verify archived evidence against the + /// endorsements it was originally verified with, see + /// [`Self::verify_attestation_archived`]. pub async fn verify_attestation( &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - mode: VerifyMode, ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -699,7 +673,6 @@ impl AttestationVerifier { azure::verify_azure_attestation( attestation_evidence.quote.clone(), expected_input_data, - mode, self.internal_pccs.clone(), self.override_azure_outdated_tcb, ) @@ -718,7 +691,6 @@ impl AttestationVerifier { let (verified, quote) = dcap::verify_dcap_attestation( attestation_evidence.quote.clone(), expected_input_data, - mode, self.internal_pccs.clone(), ) .await?; @@ -779,7 +751,59 @@ impl AttestationVerifier { &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - mode: VerifyMode, + ) -> Result, AttestationError> { + self.verify_attestation_blocking(attestation_exchange_message, expected_input_data, None) + } + + /// Re-verifies archived evidence against the endorsements a previous + /// verification reported, then checks the configured policy. + /// + /// `endorsements` is the [`EndorsementSnapshot`] that verification + /// handed back in its [`VerifiedAttestation`]. The DCAP quote is + /// checked against that snapshot's collateral bundle, and on Azure + /// the AK certificate chain is checked as well, both at the + /// snapshot's instant rather than the wall clock. Same evidence, + /// same snapshot, same verdict, however long after the fact. + /// + /// Nothing whose answer can change over time is fetched. On GCP that + /// means the provenance lookup against Google's PPID registry is + /// skipped: the registry is unsigned and mutable, so a replay could + /// only learn what it says today, and the original verification + /// already consulted it. Firmware for a portable-image policy is + /// still fetched on a cache miss, since it is signed and + /// content-addressed by the quote's MRTD, so the fetch cannot + /// change the verdict. Recording the provenance outcome and the + /// firmware in the snapshot would remove these caveats and is left + /// for a later change. + /// + /// This is not a way to verify live evidence with collateral obtained + /// out of band: the snapshot pins the instant along with the bundle, + /// and a live verification belongs on [`Self::verify_attestation`]. + /// A snapshot with no DCAP bundle fails with + /// [`DcapVerificationError::ArchivedWithoutDcapCollateral`] rather than + /// being completed by a fetch. + pub fn verify_attestation_archived( + &self, + attestation_exchange_message: AttestationExchangeMessage, + expected_input_data: [u8; 64], + endorsements: &EndorsementSnapshot, + ) -> Result, AttestationError> { + self.verify_attestation_blocking( + attestation_exchange_message, + expected_input_data, + Some(endorsements), + ) + } + + /// The body shared by [`Self::verify_attestation_sync`] and + /// [`Self::verify_attestation_archived`], which differ only in where + /// the DCAP leg gets its endorsements and whether the GCP provenance + /// lookup runs + fn verify_attestation_blocking( + &self, + attestation_exchange_message: AttestationExchangeMessage, + expected_input_data: [u8; 64], + archived: Option<&EndorsementSnapshot>, ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -806,13 +830,20 @@ impl AttestationVerifier { .attestation_evidence .as_ref() .ok_or(AttestationError::AttestationTypeNotAccepted)?; - azure::verify_azure_attestation_sync( - attestation_evidence.quote.clone(), - expected_input_data, - mode, - self.internal_pccs.clone(), - self.override_azure_outdated_tcb, - )? + match archived { + None => azure::verify_azure_attestation_sync( + attestation_evidence.quote.clone(), + expected_input_data, + self.internal_pccs.clone(), + self.override_azure_outdated_tcb, + )?, + Some(endorsements) => azure::verify_azure_attestation_archived( + attestation_evidence.quote.clone(), + expected_input_data, + endorsements, + self.override_azure_outdated_tcb, + )?, + } } #[cfg(not(feature = "azure-verifier"))] { @@ -824,15 +855,21 @@ impl AttestationVerifier { .attestation_evidence .as_ref() .ok_or(AttestationError::AttestationTypeNotAccepted)?; - let pccs = self.internal_pccs.clone(); - - let (verified, quote) = dcap::verify_dcap_attestation_sync( - attestation_evidence.quote.clone(), - expected_input_data, - mode, - pccs, - )?; - if attestation_type == AttestationType::GcpTdx { + let (verified, quote) = match archived { + None => dcap::verify_dcap_attestation_sync( + attestation_evidence.quote.clone(), + expected_input_data, + self.internal_pccs.clone(), + )?, + Some(endorsements) => dcap::verify_dcap_attestation_archived( + attestation_evidence.quote.clone(), + expected_input_data, + endorsements, + )?, + }; + // The registry is unsigned and mutable: a replay could only + // learn what it says now, so only a live verification asks + if attestation_type == AttestationType::GcpTdx && archived.is_none() { self.gcp_provenance_checker.verify_provenance_sync("e)?; } verified @@ -1234,18 +1271,9 @@ mod tests { let input_data = [0u8; 64]; assert!( - verifier - .verify_attestation(attestation.clone(), input_data, VerifyMode::Live) - .await - .unwrap() - .is_none() - ); - assert!( - verifier - .verify_attestation_sync(attestation, input_data, VerifyMode::Live) - .unwrap() - .is_none() + verifier.verify_attestation(attestation.clone(), input_data).await.unwrap().is_none() ); + assert!(verifier.verify_attestation_sync(attestation, input_data).unwrap().is_none()); } #[tokio::test] @@ -1260,9 +1288,8 @@ mod tests { let verifier = AttestationVerifier::mock(); let message: AttestationExchangeMessage = attestation_evidence.into(); - let async_result = - verifier.verify_attestation(message.clone(), input_data, VerifyMode::Live).await; - let sync_result = verifier.verify_attestation_sync(message, input_data, VerifyMode::Live); + let async_result = verifier.verify_attestation(message.clone(), input_data).await; + let sync_result = verifier.verify_attestation_sync(message, input_data); assert!( matches!( @@ -1286,6 +1313,67 @@ mod tests { ); } + /// What a live verification reports is enough to reproduce its verdict + /// later, policy check included, with nothing fetched + #[tokio::test] + async fn archived_replay_reproduces_the_live_verdict() { + let input_data = [7u8; 64]; + let quote = dcap::create_dcap_attestation(input_data).unwrap(); + let attestation_evidence = AttestationEvidence { + quote, + platform: mock_platform_metadata(AttestationType::DcapTdx).unwrap(), + }; + let verifier = AttestationVerifier::mock(); + let message: AttestationExchangeMessage = attestation_evidence.into(); + + let live = verifier + .verify_attestation(message.clone(), input_data) + .await + .unwrap() + .expect("mock evidence carries an attestation"); + let replayed = verifier + .verify_attestation_archived(message, input_data, &live.endorsements) + .unwrap() + .expect("mock evidence carries an attestation"); + + assert_eq!(replayed.measurements, live.measurements); + assert_eq!(replayed.expected_measurements, live.expected_measurements); + assert_eq!(replayed.endorsements, live.endorsements); + } + + /// A replay of GCP evidence does not consult the provenance registry. + /// A mock PPID is not in Google's registry, so were the lookup to run + /// it would fail closed + #[test] + fn archived_gcp_replay_skips_the_provenance_lookup() { + let input_data = [7u8; 64]; + let quote = dcap::create_dcap_attestation(input_data).unwrap(); + let attestation_evidence = AttestationEvidence { + quote, + platform: mock_platform_metadata(AttestationType::GcpTdx).unwrap(), + }; + let verifier = AttestationVerifier::mock(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let endorsements = EndorsementSnapshot::dcap(mock_tdx::mock_collateral(), now); + + let replayed = verifier.verify_attestation_archived( + attestation_evidence.into(), + input_data, + &endorsements, + ); + + assert!( + matches!( + replayed, + Ok(Some(VerifiedAttestation { + expected_measurements: Some(ExpectedMeasurements::Dcap(_)), + .. + })) + ), + "expected archived GCP replay to pass the policy check: {replayed:?}" + ); + } + /// 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. @@ -1305,7 +1393,7 @@ mod tests { let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); let verified = verifier - .verify_attestation(attestation_evidence.into(), input_data, VerifyMode::Live) + .verify_attestation(attestation_evidence.into(), input_data) .await .unwrap() .expect("mock evidence carries an attestation"); @@ -1338,17 +1426,14 @@ mod tests { let input_data = [0; 64]; assert!(matches!( - verifier.verify_attestation_sync(message.clone(), input_data, VerifyMode::Live), + verifier.verify_attestation_sync(message.clone(), input_data), Err(AttestationError::AttestationTypeNotAccepted) )); let generation = verifier_clone.measurement_policy_read().generation; verifier_clone.set_measurement_policy(MeasurementPolicy::expect_none(), generation); - assert!(matches!( - verifier.verify_attestation_sync(message, input_data, VerifyMode::Live), - Ok(None) - )); + assert!(matches!(verifier.verify_attestation_sync(message, input_data), Ok(None))); } #[test] @@ -1389,7 +1474,7 @@ mod tests { tokio::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).await.unwrap(); let verified = verifier - .verify_attestation(attestation.into(), input_data, VerifyMode::Live) + .verify_attestation(attestation.into(), input_data) .await .unwrap() .expect("mock evidence carries an attestation"); @@ -1497,7 +1582,7 @@ mod tests { std::fs::write(&policy_path, br#"[{"attestation_type":"dcap-tdx"}]"#).unwrap(); let verified = verifier - .verify_attestation_sync(attestation.into(), input_data, VerifyMode::Live) + .verify_attestation_sync(attestation.into(), input_data) .unwrap() .expect("mock evidence carries an attestation"); diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 7df1459..46a9b19 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -13,7 +13,6 @@ pub use attestation::{ AttestationType, AttestationVerifier, PlatformMetadata, - VerifyMode, }; use ra_tls::{ attestation::{Attestation, AttestationQuote, VersionedAttestation}, @@ -677,7 +676,7 @@ impl AttestedCertificateVerifier { let attestation = Self::extract_custom_attestation_from_cert(cert)?; self.attestation_verifier - .verify_attestation_sync(attestation, expected_input_data, VerifyMode::Live) + .verify_attestation_sync(attestation, expected_input_data) .map_err(|err| { tracing::warn!( "Rejecting certificate after attestation verification failure: {err}"