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}"