From 5d3c8ea325f937eb86cdf6a8e425fff8ab9a2730 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 11 Jun 2026 19:30:29 +0200 Subject: [PATCH 1/8] Basic GCP provenance check --- crates/attestation/src/lib.rs | 223 +++++++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 6 deletions(-) diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 73bc5ba..93308b3 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -12,16 +12,23 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use dcap_qvl::{intel, quote::Quote}; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use thiserror::Error; use crate::{dcap::DcapVerificationError, measurements::MeasurementPolicy}; /// Used in attestation type detection to check if we are on GCP const GCP_METADATA_API: &str = "http://metadata.google.internal"; +/// Public registry of GCP Confidential VM TDX PPIDs. +const GCP_PROVENANCE_REGISTRY_URL: &str = + "https://storage.googleapis.com/confidential-host-registry"; +/// GCP provenance documents are small JSON payloads. Cap reads defensively. +const GCP_PROVENANCE_DOCUMENT_MAX_BYTES: u64 = 16 * 1024; /// An attestation payload together with its type #[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] @@ -399,12 +406,17 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - dcap::verify_dcap_attestation( - attestation_exchange_message.attestation, + let attestation = attestation_exchange_message.attestation; + let measurements = dcap::verify_dcap_attestation( + attestation.clone(), expected_input_data, self.internal_pccs.clone(), ) - .await? + .await?; + if attestation_type == AttestationType::GcpTdx { + verify_gcp_provenance(attestation).await?; + } + measurements } }; @@ -455,17 +467,22 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::QemuTdx | AttestationType::GcpTdx => { + let attestation = attestation_exchange_message.attestation; #[cfg(any(test, feature = "mock"))] let pccs = self.internal_pccs.clone().unwrap_or_else(|| Pccs::new_without_prewarm(None)); #[cfg(not(any(test, feature = "mock")))] let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; - dcap::verify_dcap_attestation_sync( - attestation_exchange_message.attestation, + let measurements = dcap::verify_dcap_attestation_sync( + attestation.clone(), expected_input_data, pccs, - )? + )?; + if attestation_type == AttestationType::GcpTdx { + verify_gcp_provenance_sync(&attestation)?; + } + measurements } }; @@ -571,6 +588,101 @@ fn is_local_ip(ip: IpAddr) -> bool { } } +async fn verify_gcp_provenance(quote_bytes: Vec) -> Result<(), GcpProvenanceError> { + tokio::task::spawn_blocking(move || { + verify_gcp_provenance_with_registry_url_sync("e_bytes, GCP_PROVENANCE_REGISTRY_URL) + }) + .await + .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? +} + +fn verify_gcp_provenance_sync(quote_bytes: &[u8]) -> Result<(), GcpProvenanceError> { + verify_gcp_provenance_with_registry_url_sync(quote_bytes, GCP_PROVENANCE_REGISTRY_URL) +} + +fn verify_gcp_provenance_with_registry_url_sync( + quote_bytes: &[u8], + registry_url: &str, +) -> Result<(), GcpProvenanceError> { + let quote = + Quote::parse(quote_bytes).map_err(|err| GcpProvenanceError::Quote(err.to_string()))?; + + let ppid = extract_ppid_from_quote("e)?; + let provenance_url = format!("{}/{}", registry_url.trim_end_matches('/'), hex::encode(ppid)); + let document = fetch_gcp_provenance_document(&provenance_url)?; + validate_gcp_provenance_document(&document)?; + + Ok(()) +} + +fn extract_ppid_from_quote(quote: &Quote) -> Result, GcpProvenanceError> { + let cert_chain = intel::extract_cert_chain(quote) + .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; + let leaf = cert_chain.first().ok_or(GcpProvenanceError::NoPckCertificate)?; + let extension = intel::parse_pck_extension(leaf) + .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; + + if extension.ppid.is_empty() { + return Err(GcpProvenanceError::EmptyPpid); + } + + Ok(extension.ppid) +} + +fn fetch_gcp_provenance_document(url: &str) -> Result { + let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(2)).build(); + let response = + agent.get(url).call().map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; + + let mut limited_reader = response.into_reader().take(GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1); + let mut document = String::new(); + limited_reader + .read_to_string(&mut document) + .map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; + + if document.len() as u64 > GCP_PROVENANCE_DOCUMENT_MAX_BYTES { + return Err(GcpProvenanceError::DocumentTooLarge); + } + + Ok(document) +} + +fn validate_gcp_provenance_document(document: &str) -> Result<(), GcpProvenanceError> { + let value: Value = serde_json::from_str(document)?; + let object = value.as_object().ok_or(GcpProvenanceError::InvalidDocument)?; + + let has_zone = object.get("zone").and_then(Value::as_str).is_some_and(|zone| !zone.is_empty()); + let has_timestamp = object.get("timestamp").is_some_and(|timestamp| match timestamp { + Value::String(timestamp) => !timestamp.is_empty(), + Value::Number(_) => true, + _ => false, + }); + + if has_zone && has_timestamp { Ok(()) } else { Err(GcpProvenanceError::InvalidDocument) } +} + +#[derive(Error, Debug)] +pub enum GcpProvenanceError { + #[error("quote parse: {0}")] + Quote(String), + #[error("PCK certificate chain is empty")] + NoPckCertificate, + #[error("PPID is empty")] + EmptyPpid, + #[error("PPID extraction: {0}")] + PpidExtraction(String), + #[error("registry fetch: {0}")] + RegistryFetch(String), + #[error("provenance document is invalid")] + InvalidDocument, + #[error("provenance document exceeds maximum size")] + DocumentTooLarge, + #[error("provenance document JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("blocking task join: {0}")] + TaskJoin(String), +} + /// An error when generating or verifying an attestation #[derive(Error, Debug)] pub enum AttestationError { @@ -586,6 +698,8 @@ pub enum AttestationError { QuoteGeneration(#[from] tdx_attest::TdxAttestError), #[error("DCAP verification: {0}")] DcapVerification(#[from] DcapVerificationError), + #[error("GCP provenance: {0}")] + GcpProvenance(#[from] GcpProvenanceError), #[error("Attestation type not supported")] AttestationTypeNotSupported, #[error("Attestation type not accepted")] @@ -613,6 +727,12 @@ pub enum AttestationError { #[cfg(test)] mod tests { + use std::{ + io::{Read as StdRead, Write}, + net::SocketAddr, + thread, + }; + use mock_tdx::mock_pcs::{MockPcsConfig, spawn_mock_pcs_server}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, @@ -621,6 +741,8 @@ mod tests { use super::*; + const MOCK_PPID_HEX: &str = "d04ec06d4e6d92dc90d0ad3cf5ee2ddf"; + async fn spawn_test_attestation_provider_server(body: Vec) -> std::net::SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -643,6 +765,31 @@ mod tests { addr } + fn spawn_test_registry_server( + status: u16, + body: impl Into, + ) -> (SocketAddr, thread::JoinHandle) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let body = body.into(); + + let handle = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1024]; + let bytes_read = stream.read(&mut buf).unwrap(); + let request = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); + let status_text = if status == 200 { "OK" } else { "Not Found" }; + let response = format!( + "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + request + }); + + (addr, handle) + } + #[test] fn attestation_detection_does_not_panic() { // We dont enforce what platform the test is run on, only that the function @@ -655,6 +802,70 @@ mod tests { let _ = running_on_gcp(); } + #[test] + fn extracts_ppid_from_mock_tdx_quote() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let ppid = extract_ppid_from_quote("e).unwrap(); + + assert_eq!(hex::encode(ppid), MOCK_PPID_HEX); + } + + #[test] + fn gcp_provenance_check_fetches_registry_document_for_ppid() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let (addr, request_handle) = spawn_test_registry_server( + 200, + r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, + ); + + verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) + .unwrap(); + + let request = request_handle.join().unwrap(); + assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); + } + + #[test] + fn gcp_provenance_check_fails_closed_on_registry_miss() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let (addr, request_handle) = spawn_test_registry_server(404, "not found"); + + let err = + verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::RegistryFetch(_))); + } + + #[test] + fn gcp_provenance_check_fails_closed_on_invalid_document() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let (addr, request_handle) = spawn_test_registry_server(200, r#"{"zone":""}"#); + + let err = + verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::InvalidDocument)); + } + + #[test] + fn gcp_provenance_check_fails_closed_on_oversized_document() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let oversized_body = "x".repeat((GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1) as usize); + let (addr, request_handle) = spawn_test_registry_server(200, oversized_body); + + let err = + verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::DocumentTooLarge)); + } + #[tokio::test(flavor = "multi_thread")] async fn attestation_provider_response_is_wrapped_if_needed() { let input_data = [0u8; 64]; From 7e12021f933e930afd7aa3b99260431cdab3b25e Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 12 Jun 2026 08:02:11 +0200 Subject: [PATCH 2/8] Dont parse quote twice --- crates/attestation/src/dcap.rs | 35 +++--- crates/attestation/src/gcp.rs | 211 +++++++++++++++++++++++++++++++ crates/attestation/src/lib.rs | 218 +++------------------------------ 3 files changed, 243 insertions(+), 221 deletions(-) create mode 100644 crates/attestation/src/gcp.rs diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index dca2c10..29fdd15 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -33,7 +33,7 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result { +) -> Result<(MultiMeasurements, 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( @@ -58,7 +58,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result { +) -> Result<(MultiMeasurements, 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( @@ -84,7 +84,7 @@ pub fn verify_dcap_attestation_with_timestamp_sync( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote.ca()?; @@ -118,7 +118,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( collateral: Option, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote.ca()?; @@ -156,7 +156,7 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( collateral: QuoteCollateralV3, now: u64, override_azure_outdated_tcb: bool, -) -> Result { +) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { tracing::info!("Verifying DCAP attestation: {quote:?}"); let fmspc = hex::encode_upper(quote.fmspc()?); @@ -197,11 +197,11 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - if get_quote_input_data(quote.report) != expected_input_data { + if get_quote_input_data("e.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } - Ok(measurements) + Ok((measurements, quote)) } #[cfg(any(test, feature = "mock"))] @@ -209,7 +209,7 @@ pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], pccs: Option, -) -> Result { +) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote.ca()?; let fmspc = hex::encode_upper(quote.fmspc()?); @@ -224,11 +224,11 @@ pub async fn verify_dcap_attestation( verifier.verify(&input, &collateral, now)?; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - if get_quote_input_data(quote.report) != expected_input_data { + if get_quote_input_data("e.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } - Ok(measurements) + Ok((measurements, quote)) } #[cfg(any(test, feature = "mock"))] @@ -236,7 +236,7 @@ pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], pccs: Pccs, -) -> Result { +) -> Result<(MultiMeasurements, Quote), DcapVerificationError> { let quote = Quote::parse(&input)?; let ca = quote.ca()?; let fmspc = hex::encode_upper(quote.fmspc()?); @@ -246,10 +246,11 @@ pub fn verify_dcap_attestation_sync( verifier.verify(&input, &collateral, now)?; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; - if get_quote_input_data(quote.report.clone()) != expected_input_data { + if get_quote_input_data("e.report) != expected_input_data { return Err(DcapVerificationError::InputMismatch); } - Ok(measurements) + + Ok((measurements, quote)) } /// Create a mock quote for testing on non-confidential hardware @@ -267,7 +268,7 @@ fn generate_quote(input: [u8; 64]) -> Result, tdx_attest::TdxAttestError } /// Given a [Report] get the input data regardless of report type -pub fn get_quote_input_data(report: Report) -> [u8; 64] { +pub fn get_quote_input_data(report: &Report) -> [u8; 64] { match report { Report::TD10(r) => r.report_data, Report::TD15(r) => r.base.report_data, @@ -331,7 +332,7 @@ mod tests { 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( + 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, @@ -347,7 +348,7 @@ mod tests { .await .unwrap(); - let sync_measurements = verify_dcap_attestation_with_timestamp_sync( + 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, @@ -409,7 +410,7 @@ mod tests { let expected_input_data = [0xA5; 64]; let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap(); - let measurements = + let (measurements, _) = verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs)) .await .unwrap(); diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs new file mode 100644 index 0000000..57784a2 --- /dev/null +++ b/crates/attestation/src/gcp.rs @@ -0,0 +1,211 @@ +use std::{io::Read, time::Duration}; + +use dcap_qvl::{intel, quote::Quote}; +use serde_json::Value; +use thiserror::Error; + +/// Public registry of GCP Confidential VM TDX PPIDs +const GCP_PROVENANCE_REGISTRY_URL: &str = + "https://storage.googleapis.com/confidential-host-registry"; + +/// Maximum size in bytes of GCP provenance documents +const GCP_PROVENANCE_DOCUMENT_MAX_BYTES: u64 = 16 * 1024; + +/// Given a DCAP TDX quote, check if the associated PPID has a 'provenance +/// document' from GCP +pub(crate) async fn verify_provenance(quote: Quote) -> Result<(), GcpProvenanceError> { + tokio::task::spawn_blocking(move || { + verify_provenance_with_registry_url_sync("e, GCP_PROVENANCE_REGISTRY_URL) + }) + .await + .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? +} + +/// Given a DCAP TDX quote, check if the associated PPID has a 'provenance +/// document' from GCP +pub(crate) fn verify_provenance_sync(quote: &Quote) -> Result<(), GcpProvenanceError> { + verify_provenance_with_registry_url_sync(quote, GCP_PROVENANCE_REGISTRY_URL) +} + +fn verify_provenance_with_registry_url_sync( + quote: &Quote, + registry_url: &str, +) -> Result<(), GcpProvenanceError> { + let ppid = extract_ppid_from_quote(quote)?; + let provenance_url = format!("{}/{}", registry_url.trim_end_matches('/'), hex::encode(ppid)); + let document = fetch_provenance_document(&provenance_url)?; + validate_provenance_document(&document)?; + + Ok(()) +} + +fn extract_ppid_from_quote(quote: &Quote) -> Result, GcpProvenanceError> { + let cert_chain = intel::extract_cert_chain(quote) + .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; + let leaf = cert_chain.first().ok_or(GcpProvenanceError::NoPckCertificate)?; + let extension = intel::parse_pck_extension(leaf) + .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; + + if extension.ppid.is_empty() { + return Err(GcpProvenanceError::EmptyPpid); + } + + Ok(extension.ppid) +} + +fn fetch_provenance_document(url: &str) -> Result { + let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(2)).build(); + let response = + agent.get(url).call().map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; + + let mut limited_reader = response.into_reader().take(GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1); + let mut document = String::new(); + limited_reader + .read_to_string(&mut document) + .map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; + + if document.len() as u64 > GCP_PROVENANCE_DOCUMENT_MAX_BYTES { + return Err(GcpProvenanceError::DocumentTooLarge); + } + + Ok(document) +} + +fn validate_provenance_document(document: &str) -> Result<(), GcpProvenanceError> { + let value: Value = serde_json::from_str(document)?; + let object = value.as_object().ok_or(GcpProvenanceError::InvalidDocument)?; + + let has_zone = object.get("zone").and_then(Value::as_str).is_some_and(|zone| !zone.is_empty()); + let has_timestamp = object.get("timestamp").is_some_and(|timestamp| match timestamp { + Value::String(timestamp) => !timestamp.is_empty(), + Value::Number(_) => true, + _ => false, + }); + + if has_zone && has_timestamp { Ok(()) } else { Err(GcpProvenanceError::InvalidDocument) } +} + +#[derive(Error, Debug)] +pub enum GcpProvenanceError { + #[error("quote parse: {0}")] + Quote(String), + #[error("PCK certificate chain is empty")] + NoPckCertificate, + #[error("PPID is empty")] + EmptyPpid, + #[error("PPID extraction: {0}")] + PpidExtraction(String), + #[error("registry fetch: {0}")] + RegistryFetch(String), + #[error("provenance document is invalid")] + InvalidDocument, + #[error("provenance document exceeds maximum size")] + DocumentTooLarge, + #[error("provenance document JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("blocking task join: {0}")] + TaskJoin(String), +} + +#[cfg(test)] +mod tests { + use std::{ + io::{Read as _, Write as _}, + net::SocketAddr, + thread, + }; + + use super::*; + use crate::dcap; + + const MOCK_PPID_HEX: &str = "d04ec06d4e6d92dc90d0ad3cf5ee2ddf"; + + fn spawn_test_registry_server( + status: u16, + body: impl Into, + ) -> (SocketAddr, thread::JoinHandle) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let body = body.into(); + + let handle = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1024]; + let bytes_read = stream.read(&mut buf).unwrap(); + let request = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); + let status_text = if status == 200 { "OK" } else { "Not Found" }; + let response = format!( + "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + request + }); + + (addr, handle) + } + + #[test] + fn extracts_ppid_from_mock_tdx_quote() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let ppid = extract_ppid_from_quote("e).unwrap(); + + assert_eq!(hex::encode(ppid), MOCK_PPID_HEX); + } + + #[test] + fn provenance_check_fetches_registry_document_for_ppid() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let (addr, request_handle) = spawn_test_registry_server( + 200, + r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, + ); + + verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")).unwrap(); + + let request = request_handle.join().unwrap(); + assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); + } + + #[test] + fn provenance_check_fails_closed_on_registry_miss() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let (addr, request_handle) = spawn_test_registry_server(404, "not found"); + + let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::RegistryFetch(_))); + } + + #[test] + fn provenance_check_fails_closed_on_invalid_document() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let (addr, request_handle) = spawn_test_registry_server(200, r#"{"zone":""}"#); + + let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::InvalidDocument)); + } + + #[test] + fn provenance_check_fails_closed_on_oversized_document() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let oversized_body = "x".repeat((GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1) as usize); + let (addr, request_handle) = spawn_test_registry_server(200, oversized_body); + + let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .unwrap_err(); + + request_handle.join().unwrap(); + assert!(matches!(err, GcpProvenanceError::DocumentTooLarge)); + } +} diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 93308b3..cd7e9d0 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -3,6 +3,7 @@ #[cfg(feature = "azure")] pub mod azure; pub mod dcap; +mod gcp; pub mod measurements; use std::{ @@ -12,23 +13,20 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -use dcap_qvl::{intel, quote::Quote}; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use thiserror::Error; -use crate::{dcap::DcapVerificationError, measurements::MeasurementPolicy}; +use crate::{ + dcap::DcapVerificationError, + gcp::GcpProvenanceError, + measurements::MeasurementPolicy, +}; /// Used in attestation type detection to check if we are on GCP const GCP_METADATA_API: &str = "http://metadata.google.internal"; -/// Public registry of GCP Confidential VM TDX PPIDs. -const GCP_PROVENANCE_REGISTRY_URL: &str = - "https://storage.googleapis.com/confidential-host-registry"; -/// GCP provenance documents are small JSON payloads. Cap reads defensively. -const GCP_PROVENANCE_DOCUMENT_MAX_BYTES: u64 = 16 * 1024; /// An attestation payload together with its type #[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] @@ -407,15 +405,17 @@ impl AttestationVerifier { } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { let attestation = attestation_exchange_message.attestation; - let measurements = dcap::verify_dcap_attestation( + let (measurements, quote) = dcap::verify_dcap_attestation( attestation.clone(), expected_input_data, self.internal_pccs.clone(), ) .await?; + if attestation_type == AttestationType::GcpTdx { - verify_gcp_provenance(attestation).await?; + gcp::verify_provenance(quote).await?; } + measurements } }; @@ -474,14 +474,16 @@ impl AttestationVerifier { #[cfg(not(any(test, feature = "mock")))] let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; - let measurements = dcap::verify_dcap_attestation_sync( + let (measurements, quote) = dcap::verify_dcap_attestation_sync( attestation.clone(), expected_input_data, pccs, )?; + if attestation_type == AttestationType::GcpTdx { - verify_gcp_provenance_sync(&attestation)?; + gcp::verify_provenance_sync("e)?; } + measurements } }; @@ -588,101 +590,6 @@ fn is_local_ip(ip: IpAddr) -> bool { } } -async fn verify_gcp_provenance(quote_bytes: Vec) -> Result<(), GcpProvenanceError> { - tokio::task::spawn_blocking(move || { - verify_gcp_provenance_with_registry_url_sync("e_bytes, GCP_PROVENANCE_REGISTRY_URL) - }) - .await - .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? -} - -fn verify_gcp_provenance_sync(quote_bytes: &[u8]) -> Result<(), GcpProvenanceError> { - verify_gcp_provenance_with_registry_url_sync(quote_bytes, GCP_PROVENANCE_REGISTRY_URL) -} - -fn verify_gcp_provenance_with_registry_url_sync( - quote_bytes: &[u8], - registry_url: &str, -) -> Result<(), GcpProvenanceError> { - let quote = - Quote::parse(quote_bytes).map_err(|err| GcpProvenanceError::Quote(err.to_string()))?; - - let ppid = extract_ppid_from_quote("e)?; - let provenance_url = format!("{}/{}", registry_url.trim_end_matches('/'), hex::encode(ppid)); - let document = fetch_gcp_provenance_document(&provenance_url)?; - validate_gcp_provenance_document(&document)?; - - Ok(()) -} - -fn extract_ppid_from_quote(quote: &Quote) -> Result, GcpProvenanceError> { - let cert_chain = intel::extract_cert_chain(quote) - .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; - let leaf = cert_chain.first().ok_or(GcpProvenanceError::NoPckCertificate)?; - let extension = intel::parse_pck_extension(leaf) - .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; - - if extension.ppid.is_empty() { - return Err(GcpProvenanceError::EmptyPpid); - } - - Ok(extension.ppid) -} - -fn fetch_gcp_provenance_document(url: &str) -> Result { - let agent = ureq::AgentBuilder::new().timeout(Duration::from_secs(2)).build(); - let response = - agent.get(url).call().map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; - - let mut limited_reader = response.into_reader().take(GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1); - let mut document = String::new(); - limited_reader - .read_to_string(&mut document) - .map_err(|err| GcpProvenanceError::RegistryFetch(err.to_string()))?; - - if document.len() as u64 > GCP_PROVENANCE_DOCUMENT_MAX_BYTES { - return Err(GcpProvenanceError::DocumentTooLarge); - } - - Ok(document) -} - -fn validate_gcp_provenance_document(document: &str) -> Result<(), GcpProvenanceError> { - let value: Value = serde_json::from_str(document)?; - let object = value.as_object().ok_or(GcpProvenanceError::InvalidDocument)?; - - let has_zone = object.get("zone").and_then(Value::as_str).is_some_and(|zone| !zone.is_empty()); - let has_timestamp = object.get("timestamp").is_some_and(|timestamp| match timestamp { - Value::String(timestamp) => !timestamp.is_empty(), - Value::Number(_) => true, - _ => false, - }); - - if has_zone && has_timestamp { Ok(()) } else { Err(GcpProvenanceError::InvalidDocument) } -} - -#[derive(Error, Debug)] -pub enum GcpProvenanceError { - #[error("quote parse: {0}")] - Quote(String), - #[error("PCK certificate chain is empty")] - NoPckCertificate, - #[error("PPID is empty")] - EmptyPpid, - #[error("PPID extraction: {0}")] - PpidExtraction(String), - #[error("registry fetch: {0}")] - RegistryFetch(String), - #[error("provenance document is invalid")] - InvalidDocument, - #[error("provenance document exceeds maximum size")] - DocumentTooLarge, - #[error("provenance document JSON: {0}")] - Json(#[from] serde_json::Error), - #[error("blocking task join: {0}")] - TaskJoin(String), -} - /// An error when generating or verifying an attestation #[derive(Error, Debug)] pub enum AttestationError { @@ -727,12 +634,6 @@ pub enum AttestationError { #[cfg(test)] mod tests { - use std::{ - io::{Read as StdRead, Write}, - net::SocketAddr, - thread, - }; - use mock_tdx::mock_pcs::{MockPcsConfig, spawn_mock_pcs_server}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, @@ -741,8 +642,6 @@ mod tests { use super::*; - const MOCK_PPID_HEX: &str = "d04ec06d4e6d92dc90d0ad3cf5ee2ddf"; - async fn spawn_test_attestation_provider_server(body: Vec) -> std::net::SocketAddr { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -765,31 +664,6 @@ mod tests { addr } - fn spawn_test_registry_server( - status: u16, - body: impl Into, - ) -> (SocketAddr, thread::JoinHandle) { - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - let body = body.into(); - - let handle = thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - let mut buf = [0u8; 1024]; - let bytes_read = stream.read(&mut buf).unwrap(); - let request = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); - let status_text = if status == 200 { "OK" } else { "Not Found" }; - let response = format!( - "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - stream.write_all(response.as_bytes()).unwrap(); - request - }); - - (addr, handle) - } - #[test] fn attestation_detection_does_not_panic() { // We dont enforce what platform the test is run on, only that the function @@ -802,70 +676,6 @@ mod tests { let _ = running_on_gcp(); } - #[test] - fn extracts_ppid_from_mock_tdx_quote() { - let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); - let quote = Quote::parse(&attestation).unwrap(); - let ppid = extract_ppid_from_quote("e).unwrap(); - - assert_eq!(hex::encode(ppid), MOCK_PPID_HEX); - } - - #[test] - fn gcp_provenance_check_fetches_registry_document_for_ppid() { - let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); - let (addr, request_handle) = spawn_test_registry_server( - 200, - r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, - ); - - verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) - .unwrap(); - - let request = request_handle.join().unwrap(); - assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); - } - - #[test] - fn gcp_provenance_check_fails_closed_on_registry_miss() { - let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); - let (addr, request_handle) = spawn_test_registry_server(404, "not found"); - - let err = - verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) - .unwrap_err(); - - request_handle.join().unwrap(); - assert!(matches!(err, GcpProvenanceError::RegistryFetch(_))); - } - - #[test] - fn gcp_provenance_check_fails_closed_on_invalid_document() { - let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); - let (addr, request_handle) = spawn_test_registry_server(200, r#"{"zone":""}"#); - - let err = - verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) - .unwrap_err(); - - request_handle.join().unwrap(); - assert!(matches!(err, GcpProvenanceError::InvalidDocument)); - } - - #[test] - fn gcp_provenance_check_fails_closed_on_oversized_document() { - let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); - let oversized_body = "x".repeat((GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1) as usize); - let (addr, request_handle) = spawn_test_registry_server(200, oversized_body); - - let err = - verify_gcp_provenance_with_registry_url_sync(&attestation, &format!("http://{addr}")) - .unwrap_err(); - - request_handle.join().unwrap(); - assert!(matches!(err, GcpProvenanceError::DocumentTooLarge)); - } - #[tokio::test(flavor = "multi_thread")] async fn attestation_provider_response_is_wrapped_if_needed() { let input_data = [0u8; 64]; From a4ebbb20a4f6620aec22d4a2a87d900ed4bf9981 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 12 Jun 2026 08:59:27 +0200 Subject: [PATCH 3/8] Cache known GCP PPIDs --- crates/attestation/src/gcp.rs | 113 ++++++++++++++++++++++++++-------- crates/attestation/src/lib.rs | 12 +++- 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs index 57784a2..b3ec221 100644 --- a/crates/attestation/src/gcp.rs +++ b/crates/attestation/src/gcp.rs @@ -1,4 +1,9 @@ -use std::{io::Read, time::Duration}; +use std::{ + collections::HashSet, + io::Read, + sync::{Arc, RwLock}, + time::Duration, +}; use dcap_qvl::{intel, quote::Quote}; use serde_json::Value; @@ -11,32 +16,61 @@ const GCP_PROVENANCE_REGISTRY_URL: &str = /// Maximum size in bytes of GCP provenance documents const GCP_PROVENANCE_DOCUMENT_MAX_BYTES: u64 = 16 * 1024; -/// Given a DCAP TDX quote, check if the associated PPID has a 'provenance -/// document' from GCP -pub(crate) async fn verify_provenance(quote: Quote) -> Result<(), GcpProvenanceError> { - tokio::task::spawn_blocking(move || { - verify_provenance_with_registry_url_sync("e, GCP_PROVENANCE_REGISTRY_URL) - }) - .await - .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? +#[derive(Clone, Debug)] +pub(crate) struct GcpProvenanceChecker { + known_gcp_ppids: Arc>>>, } -/// Given a DCAP TDX quote, check if the associated PPID has a 'provenance -/// document' from GCP -pub(crate) fn verify_provenance_sync(quote: &Quote) -> Result<(), GcpProvenanceError> { - verify_provenance_with_registry_url_sync(quote, GCP_PROVENANCE_REGISTRY_URL) -} +impl GcpProvenanceChecker { + pub(crate) fn new() -> Self { + Self { known_gcp_ppids: Default::default() } + } -fn verify_provenance_with_registry_url_sync( - quote: &Quote, - registry_url: &str, -) -> Result<(), GcpProvenanceError> { - let ppid = extract_ppid_from_quote(quote)?; - let provenance_url = format!("{}/{}", registry_url.trim_end_matches('/'), hex::encode(ppid)); - let document = fetch_provenance_document(&provenance_url)?; - validate_provenance_document(&document)?; + /// Given a DCAP TDX quote, check if the associated PPID has a + /// 'provenance document' from GCP + pub(crate) async fn verify_provenance(&self, quote: Quote) -> Result<(), GcpProvenanceError> { + let checker = self.clone(); + tokio::task::spawn_blocking(move || { + checker.verify_provenance_with_registry_url_sync("e, GCP_PROVENANCE_REGISTRY_URL) + }) + .await + .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? + } + + /// Given a DCAP TDX quote, check if the associated PPID has a + /// 'provenance document' from GCP + pub(crate) fn verify_provenance_sync(&self, quote: &Quote) -> Result<(), GcpProvenanceError> { + self.verify_provenance_with_registry_url_sync(quote, GCP_PROVENANCE_REGISTRY_URL) + } - Ok(()) + fn verify_provenance_with_registry_url_sync( + &self, + quote: &Quote, + registry_url: &str, + ) -> Result<(), GcpProvenanceError> { + let ppid = extract_ppid_from_quote(quote)?; + { + let known_gcp_ppids = self + .known_gcp_ppids + .read() + .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?; + if known_gcp_ppids.contains(&ppid) { + return Ok(()); + } + } + + let provenance_url = + format!("{}/{}", registry_url.trim_end_matches('/'), hex::encode(&ppid)); + let document = fetch_provenance_document(&provenance_url)?; + validate_provenance_document(&document)?; + + self.known_gcp_ppids + .write() + .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))? + .insert(ppid); + + Ok(()) + } } fn extract_ppid_from_quote(quote: &Quote) -> Result, GcpProvenanceError> { @@ -103,6 +137,8 @@ pub enum GcpProvenanceError { DocumentTooLarge, #[error("provenance document JSON: {0}")] Json(#[from] serde_json::Error), + #[error("provenance cache lock: {0}")] + CacheLock(String), #[error("blocking task join: {0}")] TaskJoin(String), } @@ -163,7 +199,27 @@ mod tests { r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, ); - verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")).unwrap(); + GcpProvenanceChecker::new() + .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .unwrap(); + + let request = request_handle.join().unwrap(); + assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); + } + + #[test] + fn provenance_check_caches_known_gcp_ppids() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let (addr, request_handle) = spawn_test_registry_server( + 200, + r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, + ); + let checker = GcpProvenanceChecker::new(); + let registry_url = format!("http://{addr}"); + + checker.verify_provenance_with_registry_url_sync("e, ®istry_url).unwrap(); + checker.verify_provenance_with_registry_url_sync("e, ®istry_url).unwrap(); let request = request_handle.join().unwrap(); assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); @@ -175,7 +231,8 @@ mod tests { let quote = Quote::parse(&attestation).unwrap(); let (addr, request_handle) = spawn_test_registry_server(404, "not found"); - let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + let err = GcpProvenanceChecker::new() + .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) .unwrap_err(); request_handle.join().unwrap(); @@ -188,7 +245,8 @@ mod tests { let quote = Quote::parse(&attestation).unwrap(); let (addr, request_handle) = spawn_test_registry_server(200, r#"{"zone":""}"#); - let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + let err = GcpProvenanceChecker::new() + .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) .unwrap_err(); request_handle.join().unwrap(); @@ -202,7 +260,8 @@ mod tests { let oversized_body = "x".repeat((GCP_PROVENANCE_DOCUMENT_MAX_BYTES + 1) as usize); let (addr, request_handle) = spawn_test_registry_server(200, oversized_body); - let err = verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + let err = GcpProvenanceChecker::new() + .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) .unwrap_err(); request_handle.join().unwrap(); diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index cd7e9d0..d5615d7 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -21,7 +21,7 @@ use thiserror::Error; use crate::{ dcap::DcapVerificationError, - gcp::GcpProvenanceError, + gcp::{GcpProvenanceChecker, GcpProvenanceError}, measurements::MeasurementPolicy, }; @@ -289,6 +289,8 @@ pub struct AttestationVerifier { pub override_azure_outdated_tcb: bool, /// Internal cache for collateral pub internal_pccs: Option, + /// Internal cache for known GCP PPIDs + gcp_provenance_checker: GcpProvenanceChecker, } impl AttestationVerifier { @@ -304,6 +306,7 @@ impl AttestationVerifier { dump_dcap_quotes, override_azure_outdated_tcb, internal_pccs: Some(Pccs::new(pccs_url)), + gcp_provenance_checker: GcpProvenanceChecker::new(), } } @@ -316,6 +319,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: None, + gcp_provenance_checker: GcpProvenanceChecker::new(), } } @@ -328,6 +332,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: None, + gcp_provenance_checker: GcpProvenanceChecker::new(), } } @@ -340,6 +345,7 @@ impl AttestationVerifier { dump_dcap_quotes: false, override_azure_outdated_tcb: false, internal_pccs: Some(Pccs::new(Some(pccs_url))), + gcp_provenance_checker: GcpProvenanceChecker::new(), } } @@ -413,7 +419,7 @@ impl AttestationVerifier { .await?; if attestation_type == AttestationType::GcpTdx { - gcp::verify_provenance(quote).await?; + self.gcp_provenance_checker.verify_provenance(quote).await?; } measurements @@ -481,7 +487,7 @@ impl AttestationVerifier { )?; if attestation_type == AttestationType::GcpTdx { - gcp::verify_provenance_sync("e)?; + self.gcp_provenance_checker.verify_provenance_sync("e)?; } measurements From 9a4ea50475f48badedab86a86e174c4be47ffdcc Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 12 Jun 2026 09:20:11 +0200 Subject: [PATCH 4/8] Measurement checks should check attestation type --- crates/attestation/src/azure/mod.rs | 4 +- crates/attestation/src/dcap.rs | 6 ++- crates/attestation/src/lib.rs | 4 +- crates/attestation/src/measurements.rs | 71 +++++++++++++++++++++----- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index eb31173..967347f 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -758,7 +758,9 @@ mod tests { .unwrap(); assert_eq!(async_measurements, sync_measurements); - measurement_policy.check_measurement(&async_measurements).unwrap(); + measurement_policy + .check_measurement(AttestationType::AzureTdx, &async_measurements) + .unwrap(); } /// Verify a complete observed Azure attestation payload that includes diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index 29fdd15..96ae6e0 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -298,7 +298,7 @@ mod tests { use mock_tdx::{MockPcsConfig, spawn_mock_pcs_server}; use super::*; - use crate::measurements::MeasurementPolicy; + use crate::{AttestationType, measurements::MeasurementPolicy}; #[tokio::test] async fn test_dcap_verify() { @@ -364,7 +364,9 @@ mod tests { .unwrap(); assert_eq!(async_measurements, sync_measurements); - measurement_policy.check_measurement(&async_measurements).unwrap(); + measurement_policy + .check_measurement(AttestationType::DcapTdx, &async_measurements) + .unwrap(); } // This specifically tests a quote which has outdated TCB level from Azure diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index d5615d7..f7a5600 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -427,7 +427,7 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy.check_measurement(&measurements)?; + self.measurement_policy.check_measurement(attestation_type, &measurements)?; tracing::debug!("Verification successful"); Ok(Some(measurements)) @@ -495,7 +495,7 @@ impl AttestationVerifier { }; // Do a measurement / attestation type policy check - self.measurement_policy.check_measurement(&measurements)?; + self.measurement_policy.check_measurement(attestation_type, &measurements)?; tracing::debug!("Verification successful"); Ok(Some(measurements)) diff --git a/crates/attestation/src/measurements.rs b/crates/attestation/src/measurements.rs index db8c2c9..2da2352 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation/src/measurements.rs @@ -275,6 +275,8 @@ pub struct MeasurementRecord { /// An identifier, for example the name and version of the corresponding /// OS image pub measurement_id: String, + /// The attestation type this record accepts + pub attestation_type: AttestationType, /// The expected measurement register values pub measurements: ExpectedMeasurements, } @@ -283,6 +285,7 @@ impl MeasurementRecord { pub fn allow_no_attestation() -> Self { Self { measurement_id: "Allow no attestation".to_string(), + attestation_type: AttestationType::None, measurements: ExpectedMeasurements::NoAttestation, } } @@ -290,6 +293,7 @@ impl MeasurementRecord { pub fn allow_any_measurement(attestation_type: AttestationType) -> Self { Self { measurement_id: format!("Any measurement for {attestation_type}"), + attestation_type, measurements: match attestation_type { AttestationType::None => ExpectedMeasurements::NoAttestation, AttestationType::AzureTdx => ExpectedMeasurements::Azure(HashMap::new()), @@ -359,6 +363,7 @@ impl MeasurementPolicy { Self { accepted_measurements: vec![MeasurementRecord { measurement_id: "test".to_string(), + attestation_type: AttestationType::DcapTdx, measurements: ExpectedMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, vec![mock_tdx::MOCK_MRTD]), (DcapMeasurementRegister::RTMR0, vec![mock_tdx::MOCK_RTMR0]), @@ -374,10 +379,14 @@ impl MeasurementPolicy { /// they are acceptable pub fn check_measurement( &self, + attestation_type: AttestationType, measurements: &MultiMeasurements, ) -> Result<(), AttestationError> { if self.accepted_measurements.iter().any(|measurement_record| match measurements { MultiMeasurements::Dcap(dcap_measurements) => { + if measurement_record.attestation_type != attestation_type { + return false; + } if let ExpectedMeasurements::Dcap(expected) = &measurement_record.measurements { // All measurements in our policy must be given and must match for (k, v) in expected.iter() { @@ -391,6 +400,9 @@ impl MeasurementPolicy { false } MultiMeasurements::Azure(azure_measurements) => { + if measurement_record.attestation_type != attestation_type { + return false; + } if let ExpectedMeasurements::Azure(expected) = &measurement_record.measurements { for (k, v) in expected.iter() { match azure_measurements.get(k) { @@ -403,6 +415,9 @@ impl MeasurementPolicy { false } MultiMeasurements::NoAttestation => { + if measurement_record.attestation_type != attestation_type { + return false; + } matches!(measurement_record.measurements, ExpectedMeasurements::NoAttestation) } }) { @@ -547,6 +562,7 @@ impl MeasurementPolicy { measurement_policy.push(MeasurementRecord { measurement_id: record.measurement_id.unwrap_or_default(), + attestation_type, measurements: expected_measurements, }); } else { @@ -612,20 +628,27 @@ mod tests { // Will not match mock measurements assert!(matches!( - specific_measurements.check_measurement(&mock_dcap_measurements()).unwrap_err(), + specific_measurements + .check_measurement(AttestationType::DcapTdx, &mock_dcap_measurements()) + .unwrap_err(), AttestationError::MeasurementsNotAccepted )); // Will not match another attestation type assert!(matches!( - specific_measurements.check_measurement(&MultiMeasurements::NoAttestation).unwrap_err(), + specific_measurements + .check_measurement(AttestationType::None, &MultiMeasurements::NoAttestation) + .unwrap_err(), AttestationError::MeasurementsNotAccepted )); // A non-specific measurement fails assert!(matches!( specific_measurements - .check_measurement(&MultiMeasurements::Azure(HashMap::new())) + .check_measurement( + AttestationType::AzureTdx, + &MultiMeasurements::Azure(HashMap::new()) + ) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); @@ -638,17 +661,32 @@ mod tests { let allowed_attestation_type = MeasurementPolicy::from_file("test-assets/measurements_2.json".into()).await.unwrap(); - allowed_attestation_type.check_measurement(&mock_dcap_measurements()).unwrap(); + allowed_attestation_type + .check_measurement(AttestationType::DcapTdx, &mock_dcap_measurements()) + .unwrap(); // Will not match another attestation type assert!(matches!( allowed_attestation_type - .check_measurement(&MultiMeasurements::NoAttestation) + .check_measurement(AttestationType::None, &MultiMeasurements::NoAttestation) .unwrap_err(), AttestationError::MeasurementsNotAccepted )); } + #[test] + fn gcp_policy_rejects_dcap_labeled_measurements() { + let policy = MeasurementPolicy::single_attestation_type(AttestationType::GcpTdx); + let measurements = mock_dcap_measurements(); + + policy.check_measurement(AttestationType::GcpTdx, &measurements).unwrap(); + + assert!(matches!( + policy.check_measurement(AttestationType::DcapTdx, &measurements).unwrap_err(), + AttestationError::MeasurementsNotAccepted + )); + } + #[tokio::test] async fn test_buildernet_measurements() { // Refresh this fixture explicitly with: @@ -662,13 +700,20 @@ mod tests { assert!(!policy.accepted_measurements.is_empty()); assert!(matches!( - policy.check_measurement(&MultiMeasurements::NoAttestation).unwrap_err(), + policy + .check_measurement(AttestationType::None, &MultiMeasurements::NoAttestation) + .unwrap_err(), AttestationError::MeasurementsNotAccepted )); // A non-specific measurement fails assert!(matches!( - policy.check_measurement(&MultiMeasurements::Azure(HashMap::new())).unwrap_err(), + policy + .check_measurement( + AttestationType::AzureTdx, + &MultiMeasurements::Azure(HashMap::new()) + ) + .unwrap_err(), AttestationError::MeasurementsNotAccepted )); } @@ -724,17 +769,17 @@ mod tests { // First value should match let measurements1 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0u8; 48])])); - assert!(policy.check_measurement(&measurements1).is_ok()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements1).is_ok()); // Second value should also match let measurements2 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x11u8; 48])])); - assert!(policy.check_measurement(&measurements2).is_ok()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements2).is_ok()); // Different value should not match let measurements3 = MultiMeasurements::Dcap(HashMap::from([(DcapMeasurementRegister::MRTD, [0x22u8; 48])])); - assert!(policy.check_measurement(&measurements3).is_err()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements3).is_err()); } #[tokio::test] @@ -814,21 +859,21 @@ mod tests { (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x11u8; 48]), ])); - assert!(policy.check_measurement(&measurements1).is_ok()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements1).is_ok()); // Both match (single + second of any) let measurements2 = MultiMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x22u8; 48]), ])); - assert!(policy.check_measurement(&measurements2).is_ok()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements2).is_ok()); // Single matches but any doesn't let measurements3 = MultiMeasurements::Dcap(HashMap::from([ (DcapMeasurementRegister::MRTD, [0u8; 48]), (DcapMeasurementRegister::RTMR0, [0x33u8; 48]), ])); - assert!(policy.check_measurement(&measurements3).is_err()); + assert!(policy.check_measurement(AttestationType::DcapTdx, &measurements3).is_err()); } #[tokio::test] From cfcce9b0d2578fb81d4a9dab99806c5bb64a64d8 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 12 Jun 2026 09:28:08 +0200 Subject: [PATCH 5/8] Fix azure tests, and avoid unneeded cloning --- crates/attestation/src/azure/mod.rs | 2 +- crates/attestation/src/lib.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index 967347f..1fdd987 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -668,7 +668,7 @@ mod test_utils { #[cfg(test)] mod tests { use super::*; - use crate::measurements::MeasurementPolicy; + use crate::{AttestationType, measurements::MeasurementPolicy}; fn input_data_from_attestation(attestation_bytes: &[u8]) -> [u8; 64] { let attestation_document: AttestationDocument = diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index f7a5600..419c423 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -410,9 +410,8 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - let attestation = attestation_exchange_message.attestation; let (measurements, quote) = dcap::verify_dcap_attestation( - attestation.clone(), + attestation_exchange_message.attestation, expected_input_data, self.internal_pccs.clone(), ) @@ -473,7 +472,6 @@ impl AttestationVerifier { } } AttestationType::DcapTdx | AttestationType::QemuTdx | AttestationType::GcpTdx => { - let attestation = attestation_exchange_message.attestation; #[cfg(any(test, feature = "mock"))] let pccs = self.internal_pccs.clone().unwrap_or_else(|| Pccs::new_without_prewarm(None)); @@ -481,7 +479,7 @@ impl AttestationVerifier { let pccs = self.internal_pccs.clone().ok_or(AttestationError::NoPccs)?; let (measurements, quote) = dcap::verify_dcap_attestation_sync( - attestation.clone(), + attestation_exchange_message.attestation, expected_input_data, pccs, )?; From 5abd134b9bf7f9d800d7d2f7f0092dccade9b2cd Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 23 Jun 2026 09:33:34 +0200 Subject: [PATCH 6/8] Add test for PPID extraction from current DCAP fixture --- crates/attestation/src/gcp.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs index b3ec221..efb84db 100644 --- a/crates/attestation/src/gcp.rs +++ b/crates/attestation/src/gcp.rs @@ -190,6 +190,16 @@ mod tests { assert_eq!(hex::encode(ppid), MOCK_PPID_HEX); } + #[test] + fn extracts_ppid_from_fixture_dcap_quote() { + let attestation = include_bytes!("../test-assets/dcap-tdx-1766059550570652607"); + let quote = Quote::parse(attestation).unwrap(); + let ppid = extract_ppid_from_quote("e).unwrap(); + + assert_eq!(ppid.len(), 16); + assert!(!ppid.iter().all(|byte| *byte == 0)); + } + #[test] fn provenance_check_fetches_registry_document_for_ppid() { let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); From f2f479f08fc6bf151c3f5fdab3226e23bfdc529c Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 23 Jun 2026 10:26:07 +0200 Subject: [PATCH 7/8] Add expiry timestamp for cached PPIDs --- crates/attestation/src/gcp.rs | 145 ++++++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 14 deletions(-) diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs index efb84db..98c982d 100644 --- a/crates/attestation/src/gcp.rs +++ b/crates/attestation/src/gcp.rs @@ -1,8 +1,8 @@ use std::{ - collections::HashSet, + collections::HashMap, io::Read, sync::{Arc, RwLock}, - time::Duration, + time::{Duration, Instant}, }; use dcap_qvl::{intel, quote::Quote}; @@ -15,10 +15,15 @@ const GCP_PROVENANCE_REGISTRY_URL: &str = /// Maximum size in bytes of GCP provenance documents const GCP_PROVENANCE_DOCUMENT_MAX_BYTES: u64 = 16 * 1024; +/// How long a cached PPID remains trusted before revalidation +const GCP_PROVENANCE_CACHE_TTL: Duration = Duration::from_secs(7 * 24 * 60 * 60); +/// Checks PPIDs extracted from DCAP quotes against Googles public bucket, +/// to establish whether this is a GCP machine #[derive(Clone, Debug)] pub(crate) struct GcpProvenanceChecker { - known_gcp_ppids: Arc>>>, + /// Cached entries with retrieval timestamp + known_gcp_ppids: Arc, Instant>>>, } impl GcpProvenanceChecker { @@ -29,9 +34,14 @@ impl GcpProvenanceChecker { /// Given a DCAP TDX quote, check if the associated PPID has a /// 'provenance document' from GCP pub(crate) async fn verify_provenance(&self, quote: Quote) -> Result<(), GcpProvenanceError> { + let now = Instant::now(); let checker = self.clone(); tokio::task::spawn_blocking(move || { - checker.verify_provenance_with_registry_url_sync("e, GCP_PROVENANCE_REGISTRY_URL) + checker.verify_provenance_with_registry_url_sync_at( + "e, + GCP_PROVENANCE_REGISTRY_URL, + now, + ) }) .await .map_err(|err| GcpProvenanceError::TaskJoin(err.to_string()))? @@ -40,13 +50,18 @@ impl GcpProvenanceChecker { /// Given a DCAP TDX quote, check if the associated PPID has a /// 'provenance document' from GCP pub(crate) fn verify_provenance_sync(&self, quote: &Quote) -> Result<(), GcpProvenanceError> { - self.verify_provenance_with_registry_url_sync(quote, GCP_PROVENANCE_REGISTRY_URL) + self.verify_provenance_with_registry_url_sync_at( + quote, + GCP_PROVENANCE_REGISTRY_URL, + Instant::now(), + ) } - fn verify_provenance_with_registry_url_sync( + fn verify_provenance_with_registry_url_sync_at( &self, quote: &Quote, registry_url: &str, + now: Instant, ) -> Result<(), GcpProvenanceError> { let ppid = extract_ppid_from_quote(quote)?; { @@ -54,7 +69,24 @@ impl GcpProvenanceChecker { .known_gcp_ppids .read() .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?; - if known_gcp_ppids.contains(&ppid) { + if let Some(stored_at) = known_gcp_ppids.get(&ppid) && + is_cache_entry_fresh(*stored_at, now) + { + return Ok(()); + } + } + + { + let mut known_gcp_ppids = self + .known_gcp_ppids + .write() + .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))?; + if known_gcp_ppids + .get(&ppid) + .is_some_and(|stored_at| !is_cache_entry_fresh(*stored_at, now)) + { + known_gcp_ppids.remove(&ppid); + } else if known_gcp_ppids.contains_key(&ppid) { return Ok(()); } } @@ -67,12 +99,16 @@ impl GcpProvenanceChecker { self.known_gcp_ppids .write() .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))? - .insert(ppid); + .insert(ppid, now); Ok(()) } } +fn is_cache_entry_fresh(stored_at: Instant, now: Instant) -> bool { + now.checked_duration_since(stored_at).is_some_and(|age| age <= GCP_PROVENANCE_CACHE_TTL) +} + fn extract_ppid_from_quote(quote: &Quote) -> Result, GcpProvenanceError> { let cert_chain = intel::extract_cert_chain(quote) .map_err(|err| GcpProvenanceError::PpidExtraction(err.to_string()))?; @@ -149,6 +185,7 @@ mod tests { io::{Read as _, Write as _}, net::SocketAddr, thread, + time::{Duration, Instant}, }; use super::*; @@ -181,6 +218,36 @@ mod tests { (addr, handle) } + fn spawn_test_registry_server_n( + status: u16, + body: impl Into, + expected_requests: usize, + ) -> (SocketAddr, thread::JoinHandle>) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let body = body.into(); + + let handle = thread::spawn(move || { + let mut requests = Vec::with_capacity(expected_requests); + for _ in 0..expected_requests { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1024]; + let bytes_read = stream.read(&mut buf).unwrap(); + let request = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); + let status_text = if status == 200 { "OK" } else { "Not Found" }; + let response = format!( + "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).unwrap(); + requests.push(request); + } + requests + }); + + (addr, handle) + } + #[test] fn extracts_ppid_from_mock_tdx_quote() { let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); @@ -210,7 +277,11 @@ mod tests { ); GcpProvenanceChecker::new() - .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .verify_provenance_with_registry_url_sync_at( + "e, + &format!("http://{addr}"), + Instant::now(), + ) .unwrap(); let request = request_handle.join().unwrap(); @@ -228,13 +299,47 @@ mod tests { let checker = GcpProvenanceChecker::new(); let registry_url = format!("http://{addr}"); - checker.verify_provenance_with_registry_url_sync("e, ®istry_url).unwrap(); - checker.verify_provenance_with_registry_url_sync("e, ®istry_url).unwrap(); + checker + .verify_provenance_with_registry_url_sync_at("e, ®istry_url, Instant::now()) + .unwrap(); + checker + .verify_provenance_with_registry_url_sync_at("e, ®istry_url, Instant::now()) + .unwrap(); let request = request_handle.join().unwrap(); assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); } + #[test] + fn provenance_check_revalidates_stale_cached_ppids() { + let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); + let quote = Quote::parse(&attestation).unwrap(); + let (addr, request_handle) = spawn_test_registry_server_n( + 200, + r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, + 2, + ); + let checker = GcpProvenanceChecker::new(); + let registry_url = format!("http://{addr}"); + let cached_at = Instant::now(); + let stale_now = cached_at + GCP_PROVENANCE_CACHE_TTL + Duration::from_secs(1); + + checker + .verify_provenance_with_registry_url_sync_at("e, ®istry_url, cached_at) + .unwrap(); + checker + .verify_provenance_with_registry_url_sync_at("e, ®istry_url, stale_now) + .unwrap(); + + let requests = request_handle.join().unwrap(); + assert_eq!(requests.len(), 2); + assert!( + requests + .iter() + .all(|request| request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))) + ); + } + #[test] fn provenance_check_fails_closed_on_registry_miss() { let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); @@ -242,7 +347,11 @@ mod tests { let (addr, request_handle) = spawn_test_registry_server(404, "not found"); let err = GcpProvenanceChecker::new() - .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .verify_provenance_with_registry_url_sync_at( + "e, + &format!("http://{addr}"), + Instant::now(), + ) .unwrap_err(); request_handle.join().unwrap(); @@ -256,7 +365,11 @@ mod tests { let (addr, request_handle) = spawn_test_registry_server(200, r#"{"zone":""}"#); let err = GcpProvenanceChecker::new() - .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .verify_provenance_with_registry_url_sync_at( + "e, + &format!("http://{addr}"), + Instant::now(), + ) .unwrap_err(); request_handle.join().unwrap(); @@ -271,7 +384,11 @@ mod tests { let (addr, request_handle) = spawn_test_registry_server(200, oversized_body); let err = GcpProvenanceChecker::new() - .verify_provenance_with_registry_url_sync("e, &format!("http://{addr}")) + .verify_provenance_with_registry_url_sync_at( + "e, + &format!("http://{addr}"), + Instant::now(), + ) .unwrap_err(); request_handle.join().unwrap(); From 157bf2cd426d5d84caae96608b74e7425ed53846 Mon Sep 17 00:00:00 2001 From: peg Date: Tue, 23 Jun 2026 10:29:36 +0200 Subject: [PATCH 8/8] Improve timestamp check --- crates/attestation/src/gcp.rs | 56 +++++++---------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/crates/attestation/src/gcp.rs b/crates/attestation/src/gcp.rs index 98c982d..fe328dc 100644 --- a/crates/attestation/src/gcp.rs +++ b/crates/attestation/src/gcp.rs @@ -96,10 +96,11 @@ impl GcpProvenanceChecker { let document = fetch_provenance_document(&provenance_url)?; validate_provenance_document(&document)?; + let fetched_at = Instant::now(); self.known_gcp_ppids .write() .map_err(|err| GcpProvenanceError::CacheLock(err.to_string()))? - .insert(ppid, now); + .insert(ppid, fetched_at); Ok(()) } @@ -218,36 +219,6 @@ mod tests { (addr, handle) } - fn spawn_test_registry_server_n( - status: u16, - body: impl Into, - expected_requests: usize, - ) -> (SocketAddr, thread::JoinHandle>) { - let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); - let body = body.into(); - - let handle = thread::spawn(move || { - let mut requests = Vec::with_capacity(expected_requests); - for _ in 0..expected_requests { - let (mut stream, _) = listener.accept().unwrap(); - let mut buf = [0u8; 1024]; - let bytes_read = stream.read(&mut buf).unwrap(); - let request = String::from_utf8_lossy(&buf[..bytes_read]).to_string(); - let status_text = if status == 200 { "OK" } else { "Not Found" }; - let response = format!( - "HTTP/1.1 {status} {status_text}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - stream.write_all(response.as_bytes()).unwrap(); - requests.push(request); - } - requests - }); - - (addr, handle) - } - #[test] fn extracts_ppid_from_mock_tdx_quote() { let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); @@ -314,30 +285,23 @@ mod tests { fn provenance_check_revalidates_stale_cached_ppids() { let attestation = dcap::create_dcap_attestation([0u8; 64]).unwrap(); let quote = Quote::parse(&attestation).unwrap(); - let (addr, request_handle) = spawn_test_registry_server_n( + let (addr, request_handle) = spawn_test_registry_server( 200, r#"{"zone":"projects/test/zones/us-central1-a","timestamp":"2026-06-11T00:00:00Z"}"#, - 2, ); let checker = GcpProvenanceChecker::new(); let registry_url = format!("http://{addr}"); - let cached_at = Instant::now(); - let stale_now = cached_at + GCP_PROVENANCE_CACHE_TTL + Duration::from_secs(1); + let ppid = extract_ppid_from_quote("e).unwrap(); + let stale_at = Instant::now() - (GCP_PROVENANCE_CACHE_TTL + Duration::from_secs(1)); + + checker.known_gcp_ppids.write().unwrap().insert(ppid, stale_at); checker - .verify_provenance_with_registry_url_sync_at("e, ®istry_url, cached_at) - .unwrap(); - checker - .verify_provenance_with_registry_url_sync_at("e, ®istry_url, stale_now) + .verify_provenance_with_registry_url_sync_at("e, ®istry_url, Instant::now()) .unwrap(); - let requests = request_handle.join().unwrap(); - assert_eq!(requests.len(), 2); - assert!( - requests - .iter() - .all(|request| request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))) - ); + let request = request_handle.join().unwrap(); + assert!(request.starts_with(&format!("GET /{MOCK_PPID_HEX} HTTP/1.1"))); } #[test]