diff --git a/crates/attestation/src/azure/verify.rs b/crates/attestation/src/azure/verify.rs index 49daf37..a1ec954 100644 --- a/crates/attestation/src/azure/verify.rs +++ b/crates/attestation/src/azure/verify.rs @@ -20,6 +20,7 @@ use super::{ unix_time_now_secs, }; use crate::{ + AttestationResult, dcap::{ verify_dcap_attestation_with_given_timestamp, verify_dcap_attestation_with_timestamp_sync, @@ -43,7 +44,7 @@ pub async fn verify_azure_attestation( expected_input_data: [u8; 64], pccs: Option, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let now = unix_time_now_secs()?; verify_azure_attestation_with_given_timestamp( @@ -67,7 +68,7 @@ pub fn verify_azure_attestation_sync( expected_input_data: [u8; 64], pccs: Pccs, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let now = unix_time_now_secs()?; verify_azure_attestation_with_given_timestamp_sync( @@ -90,7 +91,7 @@ async fn verify_azure_attestation_with_given_timestamp( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let PreparedAzureAttestation { tdx_quote_bytes, hcl_report, @@ -99,7 +100,7 @@ async fn verify_azure_attestation_with_given_timestamp( tpm_attestation, } = prepare_azure_attestation(input)?; - let _dcap_measurements = verify_dcap_attestation_with_given_timestamp( + let AttestationResult { quote, collateral, .. } = verify_dcap_attestation_with_given_timestamp( tdx_quote_bytes, expected_tdx_input_data, pccs, @@ -109,13 +110,14 @@ async fn verify_azure_attestation_with_given_timestamp( ) .await?; - finish_azure_attestation_verification( + let measurements = finish_azure_attestation_verification( hcl_report, var_data_hash, tpm_attestation, expected_input_data, now, - ) + )?; + Ok(AttestationResult { measurements, quote, collateral }) } /// Synchronous version of the verifier @@ -126,7 +128,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result { let PreparedAzureAttestation { tdx_quote_bytes, hcl_report, @@ -135,7 +137,7 @@ fn verify_azure_attestation_with_given_timestamp_sync( tpm_attestation, } = prepare_azure_attestation(input)?; - let _dcap_measurements = verify_dcap_attestation_with_timestamp_sync( + let AttestationResult { quote, collateral, .. } = verify_dcap_attestation_with_timestamp_sync( tdx_quote_bytes, expected_tdx_input_data, pccs, @@ -144,13 +146,14 @@ fn verify_azure_attestation_with_given_timestamp_sync( override_azure_outdated_tcb, )?; - finish_azure_attestation_verification( + let measurements = finish_azure_attestation_verification( hcl_report, var_data_hash, tpm_attestation, expected_input_data, now, - ) + )?; + Ok(AttestationResult { measurements, quote, collateral }) } /// Parses the attestation during verification @@ -341,7 +344,10 @@ impl RsaPubKey { #[cfg(test)] mod tests { + use dcap_qvl::QuoteCollateralV3; + use super::{super::MAX_AZURE_ATTESTATION_PAYLOAD_SIZE, *}; + use crate::CollateralSnapshot; fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] { let attestation_document: AttestationDocument = @@ -456,31 +462,43 @@ mod tests { assert_eq!(attestation_document.tpm_attestation.ak_intermediate_certificates_pem.len(), 2); let attestation_json = serde_json::to_vec(&attestation_document).unwrap(); - let async_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let sync_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - - let async_measurements = verify_azure_attestation_with_given_timestamp( + let fixture_collateral: QuoteCollateralV3 = + serde_saphyr::from_slice(collateral_bytes).unwrap(); + + let AttestationResult { + measurements: async_measurements, + collateral: async_collateral, + .. + } = verify_azure_attestation_with_given_timestamp( attestation_json.clone(), [0; 64], None, - Some(async_collateral), + Some(fixture_collateral.clone()), now, false, ) .await .unwrap(); - let sync_measurements = verify_azure_attestation_with_given_timestamp_sync( + let AttestationResult { + measurements: sync_measurements, collateral: sync_collateral, .. + } = verify_azure_attestation_with_given_timestamp_sync( attestation_json, [0; 64], Pccs::new_without_prewarm(None), - Some(sync_collateral), + Some(fixture_collateral.clone()), now, false, ) .unwrap(); assert_eq!(async_measurements, sync_measurements); + // The bundle handed back is the one the 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 = CollateralSnapshot { collateral: fixture_collateral, at: now }; + assert_eq!(async_collateral, expected); + assert_eq!(sync_collateral, expected); } #[tokio::test] diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 832822f..ec4aaff 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -12,7 +12,12 @@ use mock_tdx::generate_mock_tdx_quote; use pccs::{Pccs, PccsError}; use thiserror::Error; -use crate::{AttestationError, measurements::MultiMeasurements}; +use crate::{ + AttestationError, + AttestationResult, + CollateralSnapshot, + measurements::MultiMeasurements, +}; /// FMSPC with which to override TCB level checks on Azure (not used for GCP /// or other platforms) @@ -28,13 +33,13 @@ pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, Attestat Ok(quote) } -/// Verify a DCAP TDX quote, and return the measurement values +/// Verify a DCAP TDX quote #[cfg(not(any(test, feature = "mock")))] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; verify_dcap_attestation_with_given_timestamp( @@ -48,8 +53,7 @@ pub async fn verify_dcap_attestation( .await } -/// Synchronous version - Verify a DCAP TDX quote, and return the -/// measurement values +/// Synchronous version - verify a DCAP TDX quote /// /// This relies on having DCAP collateral already present in the cache /// @@ -59,7 +63,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; verify_dcap_attestation_with_timestamp_sync( @@ -72,8 +76,8 @@ pub fn verify_dcap_attestation_sync( ) } -/// Verify a DCAP TDX quote, and return the measurement values, providing a -/// timestamp an optional pre-fetched collateral +/// Verify a DCAP TDX quote, providing a timestamp and an optional +/// pre-fetched collateral /// /// This relies on having DCAP collateral already present in the cache /// @@ -85,7 +89,7 @@ pub fn verify_dcap_attestation_with_timestamp_sync( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); @@ -119,7 +123,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); @@ -153,7 +157,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( collateral: QuoteCollateralV3, now: u64, override_azure_outdated_tcb: bool, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { tracing::info!("Verifying DCAP attestation: {quote:?}"); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -198,7 +202,11 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(AttestationResult { + measurements, + quote, + collateral: CollateralSnapshot { collateral, at: now }, + }) } #[cfg(any(test, feature = "mock"))] @@ -206,7 +214,7 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -225,7 +233,11 @@ pub async fn verify_dcap_attestation( return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(AttestationResult { + measurements, + quote, + collateral: CollateralSnapshot { collateral, at: now }, + }) } #[cfg(any(test, feature = "mock"))] @@ -233,7 +245,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { +) -> Result { let quote = Quote::parse(&input)?; let ca = quote_ca("e)?.as_id_str(); let fmspc = hex::encode_upper(quote_fmspc("e)?); @@ -246,7 +258,11 @@ pub fn verify_dcap_attestation_sync( if get_quote_input_data("e.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } - Ok((measurements, quote)) + Ok(AttestationResult { + measurements, + quote, + collateral: CollateralSnapshot { collateral, at: now }, + }) } /// Create a mock quote for testing on non-confidential hardware @@ -323,41 +339,49 @@ mod tests { let collateral_bytes: &'static [u8] = include_bytes!("../test-assets/dcap-quote-collateral-00.yaml"); - let async_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let sync_collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - - let (async_measurements, _) = verify_dcap_attestation_with_given_timestamp( - attestation_bytes.to_vec(), - [ - 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, 227, - 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, 161, 136, - 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, 245, 114, 33, - 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, - ], - None, - Some(async_collateral), - now, - false, - ) - .await - .unwrap(); - - let (sync_measurements, _) = verify_dcap_attestation_with_timestamp_sync( - attestation_bytes.to_vec(), - [ - 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, 227, - 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, 161, 136, - 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, 245, 114, 33, - 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, - ], - Pccs::new_without_prewarm(None), - Some(sync_collateral), - now, - false, - ) - .unwrap(); + let fixture_collateral: QuoteCollateralV3 = + serde_saphyr::from_slice(collateral_bytes).unwrap(); + + let AttestationResult { measurements: async_measurements, collateral, .. } = + verify_dcap_attestation_with_given_timestamp( + attestation_bytes.to_vec(), + [ + 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, + 227, 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, + 161, 136, 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, + 245, 114, 33, 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, + ], + None, + Some(fixture_collateral.clone()), + now, + false, + ) + .await + .unwrap(); + + let AttestationResult { measurements: sync_measurements, .. } = + verify_dcap_attestation_with_timestamp_sync( + attestation_bytes.to_vec(), + [ + 116, 39, 106, 100, 143, 31, 212, 145, 244, 116, 162, 213, 44, 114, 216, 80, + 227, 118, 129, 87, 180, 62, 194, 151, 169, 145, 116, 130, 189, 119, 39, 139, + 161, 136, 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, + 245, 114, 33, 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, + ], + Pccs::new_without_prewarm(None), + Some(fixture_collateral.clone()), + now, + false, + ) + .unwrap(); assert_eq!(async_measurements, sync_measurements); + // A caller archiving provenance gets back the bundle the verification + // consumed, not a second copy of it + assert_eq!(collateral.collateral, fixture_collateral); + // ... and the instant it was held to, which is the other half of what + // makes the verification reproducible + assert_eq!(collateral.at, now); measurement_policy.check_measurement(&async_measurements, None).unwrap(); } @@ -376,7 +400,7 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); - let _measurements = verify_dcap_attestation_with_given_timestamp( + verify_dcap_attestation_with_given_timestamp( attestation_bytes.to_vec(), [ 210, 20, 43, 100, 53, 152, 235, 95, 174, 43, 200, 82, 157, 215, 154, 85, 139, 41, @@ -404,10 +428,10 @@ mod tests { let expected_input_data = [0xA5; 64]; let quote = create_dcap_attestation(expected_input_data).unwrap(); - let (measurements, _) = + let verified = verify_dcap_attestation(quote, expected_input_data, Some(pccs)).await.unwrap(); - assert_eq!(measurements, crate::measurements::mock_dcap_measurements()); + assert_eq!(verified.measurements, crate::measurements::mock_dcap_measurements()); assert_eq!(mock_pcs.tcb_call_count(), 1); assert_eq!(mock_pcs.qe_call_count(), 1); } diff --git a/crates/attestation/src/gcp/firmware.rs b/crates/attestation/src/gcp/firmware.rs index 4aa56fe..04723c8 100644 --- a/crates/attestation/src/gcp/firmware.rs +++ b/crates/attestation/src/gcp/firmware.rs @@ -77,6 +77,7 @@ mod tests { use super::GcpFirmwareCache; use crate::{ + AttestationResult, AttestationType, PlatformMetadata, dcap::{get_quote_input_data, verify_dcap_attestation_with_given_timestamp}, @@ -153,7 +154,7 @@ mod tests { let collateral = serde_saphyr::from_slice(collateral_bytes).unwrap(); let firmware = serde_saphyr::from_slice(firmware_bytes).unwrap(); - let (measurements, _) = verify_dcap_attestation_with_given_timestamp( + let AttestationResult { measurements, .. } = verify_dcap_attestation_with_given_timestamp( attestation_bytes.to_vec(), expected_input_data, None, diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index cb67f63..6404483 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -19,6 +19,11 @@ use std::{ use attest_measure::platform::PlatformError; pub use attest_types::{AttestationEvidence, PlatformMetadata}; +/// The DCAP collateral a verification consumed, reported inside +/// [AttestationResult::collateral]. Re-exported so callers can archive +/// it without taking a direct dependency on `dcap-qvl` +pub use dcap_qvl::QuoteCollateralV3; +use dcap_qvl::quote::Quote; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; @@ -338,6 +343,51 @@ impl AttestationGenerator { } } +/// A DCAP collateral bundle together with the instant it is evaluated at +/// +/// Every part of the bundle expires: TCB Info, QE Identity and both CRLs +/// carry `nextUpdate`, and the issuer chains carry `notAfter`. A bundle +/// therefore answers a freshness question only with respect to some +/// instant, and the two are one value rather than two. Holding them +/// together is what makes a verification reproducible: the same evidence, +/// the same bundle and the same instant give the same answer forever. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CollateralSnapshot { + /// The collateral the verification consumed. A second copy fetched + /// alongside may differ, since a collateral cache can refresh between + /// the two fetches + pub collateral: QuoteCollateralV3, + /// Seconds since the Unix epoch. Gates every freshness check: + /// certificate validity windows, the CRLs, and the TCB Info and QE + /// Identity windows + pub at: u64, +} + +/// The outcome of verifying one piece of attestation evidence +/// +/// Every attested platform here rests on a DCAP quote: a GCP TDX quote is +/// one, and Azure wraps one in an HCL report and a vTPM attestation. So one +/// verification always consumes exactly one collateral bundle, whichever +/// platform produced the evidence. +#[derive(Clone, Debug)] +pub struct AttestationResult { + /// The measurements the evidence carries + /// + /// Read out of `quote` on the DCAP and GCP paths. On Azure they are + /// the vTPM PCRs, which measure the guest boot rather than the launched + /// TD. They chain to `quote` - its report data commits to the HCL var + /// data, which carries the AK public key that signs the vTPM quote - + /// but are no field of it, so `quote` alone does not yield them + pub measurements: MultiMeasurements, + /// The parsed DCAP quote the verification rests on. On Azure this is + /// the TD quote the evidence wraps, not the vTPM quote + pub quote: Quote, + /// What the verification consumed, and when it was held to — the pair + /// to archive next to the evidence it verified, and to hand back to + /// re-verify that evidence later + pub collateral: CollateralSnapshot, +} + /// Allows remote attestations to be verified #[derive(Clone, Debug)] pub struct AttestationVerifier { @@ -458,11 +508,16 @@ impl AttestationVerifier { /// Verify an attestation, and ensure the measurements match one of our /// accepted measurements + /// + /// The result reports the DCAP collateral the verification consumed, + /// which is the bundle to archive next to the evidence: a second copy + /// fetched alongside may differ, since a collateral cache can refresh + /// between the two fetches. pub async fn verify_attestation( &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -470,7 +525,7 @@ impl AttestationVerifier { log_attestation(&attestation_exchange_message); } - let measurements = match attestation_type { + let verified = match attestation_type { AttestationType::None => { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); @@ -506,16 +561,16 @@ impl AttestationVerifier { .attestation_evidence .as_ref() .ok_or(AttestationError::AttestationTypeNotAccepted)?; - let (measurements, quote) = dcap::verify_dcap_attestation( + let verified = dcap::verify_dcap_attestation( attestation_evidence.quote.clone(), expected_input_data, self.internal_pccs.clone(), ) .await?; if attestation_type == AttestationType::GcpTdx { - self.gcp_provenance_checker.verify_provenance(quote).await?; + self.gcp_provenance_checker.verify_provenance(verified.quote.clone()).await?; } - measurements + verified } }; @@ -525,20 +580,20 @@ impl AttestationVerifier { .as_ref() .map(|evidence| evidence.platform.clone()); self.measurement_policy.check_measurement_with_gcp_cache( - &measurements, + &verified.measurements, platform_metadata.as_ref(), Some(&self.known_gcp_firmware), )?; tracing::debug!("Verification successful"); - Ok(Some(measurements)) + Ok(Some(verified)) } pub fn verify_attestation_sync( &self, attestation_exchange_message: AttestationExchangeMessage, expected_input_data: [u8; 64], - ) -> Result, AttestationError> { + ) -> Result, AttestationError> { let attestation_type = attestation_exchange_message.attestation_type(); tracing::debug!("Verifying {attestation_type} attestation"); @@ -546,7 +601,7 @@ impl AttestationVerifier { log_attestation(&attestation_exchange_message); } - let measurements = match attestation_type { + let verified = match attestation_type { AttestationType::None => { if self.has_remote_attestation() { return Err(AttestationError::AttestationTypeNotAccepted); @@ -588,15 +643,15 @@ impl AttestationVerifier { #[cfg(not(any(test, feature = "mock")))] let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; - let (measurements, quote) = dcap::verify_dcap_attestation_sync( + let verified = dcap::verify_dcap_attestation_sync( attestation_evidence.quote.clone(), expected_input_data, pccs, )?; if attestation_type == AttestationType::GcpTdx { - self.gcp_provenance_checker.verify_provenance_sync("e)?; + self.gcp_provenance_checker.verify_provenance_sync(&verified.quote)?; } - measurements + verified } }; @@ -606,13 +661,13 @@ impl AttestationVerifier { .as_ref() .map(|evidence| evidence.platform.clone()); self.measurement_policy.check_measurement_with_gcp_cache( - &measurements, + &verified.measurements, platform_metadata.as_ref(), Some(&self.known_gcp_firmware), )?; tracing::debug!("Verification successful"); - Ok(Some(measurements)) + Ok(Some(verified)) } /// Whether we allow no remote attestation @@ -809,4 +864,47 @@ mod tests { assert!(result.is_ok(), "expected sync mock verification to succeed: {result:?}"); } + + /// On the fetching path, the reported bundle is the one the fetch + /// produced — the property that makes archiving it provenance rather + /// than a second, possibly different, copy. + #[tokio::test] + async fn verify_reports_the_collateral_the_fetch_produced() { + let input_data = [7u8; 64]; + let quote_bytes = dcap::create_dcap_attestation(input_data).unwrap(); + let quote = dcap_qvl::quote::Quote::parse("e_bytes).unwrap(); + let fmspc = hex::encode_upper(dcap_qvl::intel::quote_fmspc("e).unwrap()); + let ca = dcap_qvl::intel::quote_ca("e).unwrap().as_id_str(); + let attestation_evidence = AttestationEvidence { + quote: quote_bytes, + platform: mock_platform_metadata(AttestationType::DcapTdx).unwrap(), + }; + + let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig::default()).await.unwrap(); + let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); + + let verified = verifier + .verify_attestation(attestation_evidence.into(), input_data) + .await + .unwrap() + .expect("mock evidence carries an attestation"); + + // The second read is served from the PCCS cache, not a second fetch, + // so it yields the same bundle the verification consumed. That is what + // makes it a valid comparison here — and the reason a caller must not + // rely on the pattern in general, where a refresh in between would + // hand back a different bundle + let (served, _is_fresh) = verifier + .internal_pccs + .as_ref() + .unwrap() + .get_collateral( + fmspc, + ca, + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), + ) + .await + .unwrap(); + assert_eq!(verified.collateral.collateral, served); + } }