From a9e05f3886b6907afdcf23ada3bbb7ee90dd72f8 Mon Sep 17 00:00:00 2001 From: Samuel Laferriere <9342524+samlaf@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:35:03 -0400 Subject: [PATCH] feat(attestation)!: pin verification to archived collateral with VerifyMode Second of two changes for flashbots/attested-tls#84, stacked on flashbots/attested-tls#85. Reporting the collateral a verification consumed is half of provenance. The other half is running the same verification again later, against that bundle, and getting the same answer. Nothing exposed that: the public entry points always fetched, and the only way to supply a bundle was through test-only variants that also took a bare timestamp. Verification now takes a VerifyMode. Live fetches collateral and reads the wall clock. Archived carries the bundle and the instant it was collected, so the two can never be supplied apart - a pinned bundle evaluated at the wrong instant is the mistake the old (Option, now) pair permitted. Azure holds its AK certificate chain to the instant the DCAP leg reported, so both legs evaluate at one time in either mode. The Azure TCB override leaves the public DCAP entry points. Only the Azure verifier has a reason to relax TCB checks, so it reaches the override through a crate-private variant; verify_dcap_attestation always verifies at full strictness. The mock verifier likewise becomes its own function rather than a cfg switch inside the production one, so verify_dcap_attestation means Intel-rooted in every build and the fixture tests replay real captures against it. AttestationVerifier is the one place that picks between them. BREAKING CHANGE: verify_attestation and verify_attestation_sync take a VerifyMode; the DCAP and Azure entry points take mode before pccs and the DCAP ones drop override_azure_outdated_tcb; the *_with_given_timestamp variants are gone, replaced by VerifyMode::Archived. 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. Closes flashbots/attested-tls#84 --- 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 | 154 ++++------ crates/attestation/src/dcap.rs | 270 ++++++++++-------- crates/attestation/src/gcp/firmware.rs | 24 +- crates/attestation/src/lib.rs | 85 +++++- crates/attested-tls/src/lib.rs | 3 +- 8 files changed, 295 insertions(+), 256 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 0a557b7..cab17c7 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 d0cfd10..21a20ac 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 8c6285d..4bb780a 100644 --- a/crates/attestation/src/azure/verify.rs +++ b/crates/attestation/src/azure/verify.rs @@ -4,7 +4,6 @@ //! 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 num_bigint::BigUint; use openssl::pkey::PKey; use pccs::Pccs; @@ -17,13 +16,13 @@ use super::{ TpmAttest, ak_certificate::verify_ak_cert_with_azure_roots, ensure_azure_attestation_payload_size, - unix_time_now_secs, }; use crate::{ VerifiedAttestation, + VerifyMode, dcap::{ - verify_dcap_attestation_with_given_timestamp, - verify_dcap_attestation_with_timestamp_sync, + verify_dcap_attestation_with_tcb_override, + verify_dcap_attestation_with_tcb_override_sync, }, measurements::MultiMeasurements, }; @@ -39,57 +38,17 @@ struct PreparedAzureAttestation { } /// Verify a TDX attestation from Azure -pub async fn verify_azure_attestation( - input: Vec, - expected_input_data: [u8; 64], - pccs: Option, - 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 -/// -/// 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 collateral, 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: Option, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, ) -> Result { let PreparedAzureAttestation { @@ -100,37 +59,44 @@ 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_dcap_attestation_with_tcb_override( tdx_quote_bytes, expected_tdx_input_data, + mode, pccs, - collateral, - now, override_azure_outdated_tcb, ) .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, endorsements: dcap.endorsements }) } -/// 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]. +/// +/// 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 { @@ -141,12 +107,11 @@ 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_dcap_attestation_with_tcb_override_sync( tdx_quote_bytes, expected_tdx_input_data, + mode, pccs, - collateral, - now, override_azure_outdated_tcb, )?; @@ -155,7 +120,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( var_data_hash, tpm_attestation, expected_input_data, - now, + dcap.endorsements.at, )?; Ok(VerifiedAttestation { measurements, endorsements: dcap.endorsements }) } @@ -386,43 +351,24 @@ 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; let input = vec![b'{'; actual]; - let err = verify_azure_attestation(input.clone(), [0; 64], None, false).await.unwrap_err(); + let err = verify_azure_attestation(input.clone(), [0; 64], VerifyMode::Live, None, false) + .await + .unwrap_err(); assert_payload_too_large(err, actual); let err = verify_azure_attestation_sync( - input.clone(), - [0; 64], - Pccs::new_without_prewarm(None), - false, - ) - .unwrap_err(); - assert_payload_too_large(err, actual); - - let err = verify_azure_attestation_with_given_timestamp( - input.clone(), - [0; 64], - None, - 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_without_prewarm(None), - None, - 0, false, ) .unwrap_err(); @@ -470,12 +416,11 @@ mod tests { let VerifiedAttestation { 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)), None, - Some(fixture_collateral.clone()), - now, false, ) .await @@ -484,20 +429,20 @@ mod tests { let VerifiedAttestation { 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_without_prewarm(None), - 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); @@ -520,12 +465,11 @@ 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)), None, - Some(collateral), - now, false, ) .await diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index f863ae9..2c9641e 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -11,6 +11,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; @@ -21,6 +22,7 @@ use crate::{ AttestationError, EndorsementSnapshot, VerifiedAttestation, + VerifyMode, measurements::MultiMeasurements, }; @@ -38,130 +40,207 @@ pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, Attestat Ok(quote) } -/// Verify a DCAP TDX quote -#[cfg(not(any(test, feature = "mock")))] +/// Verify a DCAP TDX quote against Intel's production trust root +/// +/// `pccs` only matters on [VerifyMode::Live]: collateral comes from it, or +/// straight from Intel when there is none. [VerifyMode::Archived] carries +/// its own bundle and never consults it. pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Option, ) -> 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_dcap_attestation_with_tcb_override(input, expected_input_data, mode, pccs, false).await +} + +/// [verify_dcap_attestation], with the TCB override the Azure verifier +/// applies to the quote inside an HCL report +pub(crate) async fn verify_dcap_attestation_with_tcb_override( + input: Vec, + expected_input_data: [u8; 64], + mode: VerifyMode, + pccs: Option, + override_azure_outdated_tcb: bool, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + verify_quote( input, expected_input_data, + mode, pccs, - None, - now, override_azure_outdated_tcb, + &QuoteVerifier::new_prod(), + None, ) .await } -/// Synchronous version - verify a DCAP TDX quote +/// Verify a quote minted by [mock_tdx], which chains to the mock root CA /// -/// This relies on having DCAP collateral already present in the cache +/// 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. /// -/// If possible, prefer the async version -#[cfg(not(any(test, feature = "mock")))] -pub fn verify_dcap_attestation_sync( +/// Kept separate so [verify_dcap_attestation] stays Intel-rooted in test +/// builds, where the fixture tests replay real captures against it. +/// [crate::AttestationVerifier] picks between the two. +#[cfg(any(test, feature = "mock"))] +pub async fn verify_mock_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - pccs: Pccs, + mode: VerifyMode, + pccs: Option, ) -> 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( input, expected_input_data, + mode, pccs, - None, - now, - override_azure_outdated_tcb, + false, + &mock_tdx::mock_dcap_verifier(), + Some(mock_tdx::mock_collateral()), ) + .await } -/// Verify a DCAP TDX quote, providing a timestamp and an optional -/// pre-fetched collateral +/// Synchronous version - verify a DCAP TDX quote against Intel's production +/// trust root /// -/// 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. /// /// If possible, prefer the async version -pub fn verify_dcap_attestation_with_timestamp_sync( +pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], + mode: VerifyMode, + pccs: Pccs, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + verify_dcap_attestation_with_tcb_override_sync(input, expected_input_data, mode, pccs, false) +} + +/// [verify_dcap_attestation_sync], with the TCB override the Azure verifier +/// applies to the quote inside an HCL report +pub(crate) fn verify_dcap_attestation_with_tcb_override_sync( + input: Vec, + expected_input_data: [u8; 64], + mode: VerifyMode, pccs: Pccs, - collateral: Option, - now: u64, override_azure_outdated_tcb: bool, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let quote = Quote::parse(&input)?; + verify_quote_sync( + input, + expected_input_data, + mode, + pccs, + override_azure_outdated_tcb, + &QuoteVerifier::new_prod(), + ) +} + +/// Synchronous version - verify a quote minted by [mock_tdx] +#[cfg(any(test, feature = "mock"))] +pub fn verify_mock_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(), + ) +} +/// Resolve the collateral a verification runs against, then verify +/// +/// `fallback_collateral` is the bundle of last resort, used when the mode +/// pins none and there is no PCCS. `None` fetches from Intel. +async fn verify_quote( + raw_quote: Vec, + expected_input_data: [u8; 64], + mode: VerifyMode, + pccs: Option, + override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, + fallback_collateral: Option, +) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { + let (pinned_collateral, now) = mode.resolve()?; + 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 Some(ref pccs) = pccs { + let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; + collateral + } else if let Some(fallback_collateral) = fallback_collateral { + fallback_collateral } else { - pccs.get_collateral_sync(fmspc.clone(), ca, now)? + CollateralClient::with_default_http(PCS_URL)? + .fetch_for_fmspc_without_pck_chain(&fmspc, ca, false) + .await? }; - 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. +fn verify_quote_sync( + raw_quote: Vec, expected_input_data: [u8; 64], - pccs_option: Option, - collateral: Option, - now: u64, + mode: VerifyMode, + pccs: Pccs, override_azure_outdated_tcb: bool, + verifier: &QuoteVerifier, ) -> Result<(VerifiedAttestation, Quote), DcapVerificationError> { - let quote = Quote::parse(&input)?; - + let (pinned_collateral, now) = mode.resolve()?; + 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 - } else if let Some(ref pccs) = pccs_option { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; - collateral - } else { - CollateralClient::with_default_http(PCS_URL)? - .fetch_for_fmspc_without_pck_chain(&fmspc, ca, false) - .await? + let collateral = match pinned_collateral { + Some(pinned_collateral) => pinned_collateral, + None => 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:?}"); @@ -185,7 +264,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, @@ -216,66 +295,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: Option, -) -> 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 let Some(ref pccs) = pccs { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?; - collateral - } else { - mock_tdx::mock_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, - 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 = 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, - 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> { @@ -354,7 +373,7 @@ mod tests { serde_saphyr::from_slice(collateral_bytes).unwrap(); let (VerifiedAttestation { measurements: async_measurements, endorsements }, _) = - verify_dcap_attestation_with_given_timestamp( + verify_dcap_attestation( attestation_bytes.to_vec(), [ 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, @@ -362,16 +381,14 @@ 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)), None, - Some(fixture_collateral.clone()), - now, - false, ) .await .unwrap(); let (VerifiedAttestation { measurements: sync_measurements, .. }, _) = - verify_dcap_attestation_with_timestamp_sync( + verify_dcap_attestation_sync( attestation_bytes.to_vec(), [ 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, @@ -379,10 +396,8 @@ 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_without_prewarm(None), - Some(fixture_collateral.clone()), - now, - false, ) .unwrap(); @@ -416,16 +431,15 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - verify_dcap_attestation_with_given_timestamp( + verify_dcap_attestation_with_tcb_override( 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)), None, - Some(collateral), - now, true, ) .await @@ -445,7 +459,9 @@ mod tests { let quote = create_dcap_attestation(expected_input_data).unwrap(); let (verified, _) = - verify_dcap_attestation(quote, expected_input_data, Some(pccs)).await.unwrap(); + verify_mock_dcap_attestation(quote, expected_input_data, VerifyMode::Live, Some(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 40affc1..3f4278d 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -77,9 +77,11 @@ mod tests { 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_dcap_attestation}, measurements::{ExpectedMeasurements, MeasurementPolicy, MeasurementRecord}, }; @@ -156,17 +158,17 @@ 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, - None, - Some(collateral), + let (VerifiedAttestation { measurements, .. }, _) = verify_dcap_attestation( + attestation_bytes.to_vec(), + expected_input_data, + VerifyMode::Archived(EndorsementSnapshot::dcap( + collateral, GCP_TDX_PORTABLE_FIXTURE_TIMESTAMP, - false, - ) - .await - .unwrap(); + )), + 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 e45e132..46d1597 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -15,13 +15,14 @@ use std::{ fmt::{self, Display, Formatter}, io::Read, net::IpAddr, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH}, }; 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::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; @@ -392,6 +393,41 @@ impl EndorsementSnapshot { } } +/// Where one verification gets its DCAP collateral, 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 it 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 the collateral, and evaluate every freshness check at the wall + /// clock + Live, + /// Verify against a pinned snapshot: the endorsements, as of the + /// instant they were held to. The DCAP leg and, on Azure, the AK + /// certificate chain reach nothing on the network + Archived(EndorsementSnapshot), +} + +impl VerifyMode { + /// The collateral to verify against, and the instant to evaluate + /// freshness at + /// + /// The one place a verification reads the wall clock. + pub(crate) fn resolve(self) -> Result<(Option, u64), SystemTimeError> { + Ok(match self { + Self::Live => (None, SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()), + Self::Archived(EndorsementSnapshot { at, dcap }) => (dcap, at), + }) + } +} + /// Evidence whose authenticity a Verifier established, with what it was /// established against /// @@ -563,10 +599,20 @@ impl AttestationVerifier { /// Verify an attestation, and ensure the measurements match one of our /// accepted measurements + /// + /// [VerifyMode::Live] fetches collateral 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"); @@ -596,6 +642,7 @@ impl AttestationVerifier { azure::verify_azure_attestation( attestation_evidence.quote.clone(), expected_input_data, + mode, self.internal_pccs.clone(), self.override_azure_outdated_tcb, ) @@ -611,9 +658,22 @@ impl AttestationVerifier { .attestation_evidence .as_ref() .ok_or(AttestationError::AttestationTypeNotAccepted)?; + // Which quotes a build accepts: a `mock` build accepts the + // mock-rooted ones [mock_tdx] mints, any other build + // accepts Intel-rooted ones only + #[cfg(any(test, feature = "mock"))] + let (verified, quote) = dcap::verify_mock_dcap_attestation( + attestation_evidence.quote.clone(), + expected_input_data, + mode, + self.internal_pccs.clone(), + ) + .await?; + #[cfg(not(any(test, feature = "mock")))] let (verified, quote) = dcap::verify_dcap_attestation( attestation_evidence.quote.clone(), expected_input_data, + mode, self.internal_pccs.clone(), ) .await?; @@ -643,6 +703,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"); @@ -673,6 +734,7 @@ impl AttestationVerifier { azure::verify_azure_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, + mode, pccs, self.override_azure_outdated_tcb, )? @@ -693,9 +755,18 @@ impl AttestationVerifier { #[cfg(not(any(test, feature = "mock")))] let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; + #[cfg(any(test, feature = "mock"))] + let (verified, quote) = dcap::verify_mock_dcap_attestation_sync( + attestation_evidence.quote.clone(), + expected_input_data, + mode, + pccs, + )?; + #[cfg(not(any(test, feature = "mock")))] let (verified, quote) = dcap::verify_dcap_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, + mode, pccs, )?; if attestation_type == AttestationType::GcpTdx { @@ -917,7 +988,11 @@ mod tests { pccs.ready().await.unwrap(); } - let result = verifier.verify_attestation_sync(attestation_evidence.into(), input_data); + let result = verifier.verify_attestation_sync( + attestation_evidence.into(), + input_data, + VerifyMode::Live, + ); assert!(result.is_ok(), "expected sync mock verification to succeed: {result:?}"); } @@ -941,7 +1016,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"); diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 3835b0e..52094d7 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}, @@ -672,7 +673,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}"