From f74f944a4d3829179dab626f12e4c1594d8140a9 Mon Sep 17 00:00:00 2001 From: peg Date: Thu, 26 Feb 2026 15:21:28 +0100 Subject: [PATCH 1/6] Implement internal PCCS --- attested-tls/src/attestation/azure/mod.rs | 10 +-- attested-tls/src/attestation/dcap.rs | 58 +++++++++------- attested-tls/src/attestation/mod.rs | 33 ++++++--- attested-tls/src/attestation/pccs.rs | 82 +++++++++++++++++++++++ src/lib.rs | 7 +- src/main.rs | 10 +-- 6 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 attested-tls/src/attestation/pccs.rs diff --git a/attested-tls/src/attestation/azure/mod.rs b/attested-tls/src/attestation/azure/mod.rs index 63486b1..9187f52 100644 --- a/attested-tls/src/attestation/azure/mod.rs +++ b/attested-tls/src/attestation/azure/mod.rs @@ -13,7 +13,7 @@ use thiserror::Error; use x509_parser::prelude::*; use crate::attestation::{ - dcap::verify_dcap_attestation_with_given_timestamp, measurements::MultiMeasurements, + Pccs, dcap::verify_dcap_attestation_with_given_timestamp, measurements::MultiMeasurements, }; /// The attestation evidence payload that gets sent over the channel @@ -81,7 +81,7 @@ pub async fn create_azure_attestation(input_data: [u8; 64]) -> Result, M pub async fn verify_azure_attestation( input: Vec, expected_input_data: [u8; 64], - pccs_url: Option, + pccs: Pccs, override_azure_outdated_tcb: bool, ) -> Result { let now = std::time::SystemTime::now() @@ -92,7 +92,7 @@ pub async fn verify_azure_attestation( verify_azure_attestation_with_given_timestamp( input, expected_input_data, - pccs_url, + pccs, None, now, override_azure_outdated_tcb, @@ -105,7 +105,7 @@ pub async fn verify_azure_attestation( async fn verify_azure_attestation_with_given_timestamp( input: Vec, expected_input_data: [u8; 64], - pccs_url: Option, + pccs: Pccs, collateral: Option, now: u64, override_azure_outdated_tcb: bool, @@ -127,7 +127,7 @@ async fn verify_azure_attestation_with_given_timestamp( let _dcap_measurements = verify_dcap_attestation_with_given_timestamp( tdx_quote_bytes, expected_tdx_input_data, - pccs_url, + pccs, collateral, now, override_azure_outdated_tcb, diff --git a/attested-tls/src/attestation/dcap.rs b/attested-tls/src/attestation/dcap.rs index 2632557..083c5d9 100644 --- a/attested-tls/src/attestation/dcap.rs +++ b/attested-tls/src/attestation/dcap.rs @@ -1,10 +1,10 @@ //! Data Center Attestation Primitives (DCAP) evidence generation and verification +use crate::attestation::pccs::Pccs; use crate::attestation::{AttestationError, measurements::MultiMeasurements}; use configfs_tsm::QuoteGenerationError; use dcap_qvl::{ QuoteCollateralV3, - collateral::get_collateral_for_fmspc, quote::{Quote, Report}, tcb_info::TcbInfo, }; @@ -28,7 +28,7 @@ pub async fn create_dcap_attestation(input_data: [u8; 64]) -> Result, At pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - pccs_url: Option, + pccs: Pccs, ) -> Result { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? @@ -37,7 +37,7 @@ pub async fn verify_dcap_attestation( verify_dcap_attestation_with_given_timestamp( input, expected_input_data, - pccs_url, + pccs, None, now, override_azure_outdated_tcb, @@ -51,7 +51,7 @@ pub async fn verify_dcap_attestation( pub async fn verify_dcap_attestation_with_given_timestamp( input: Vec, expected_input_data: [u8; 64], - pccs_url: Option, + pccs: Pccs, collateral: Option, now: u64, override_azure_outdated_tcb: bool, @@ -78,26 +78,38 @@ pub async fn verify_dcap_attestation_with_given_timestamp( |tcb_info: TcbInfo| tcb_info }; - let collateral = match collateral { - Some(c) => c, + match collateral { + Some(given_collateral) => { + dcap_qvl::verify::verify_with_tcb_override( + &input, + &given_collateral, + now, + override_outdated_tcb, + )?; + } None => { - get_collateral_for_fmspc( - &pccs_url.clone().unwrap_or(PCS_URL.to_string()), - fmspc, - ca, - false, // Indicates not SGX - ) - .await? + let (collateral, is_fresh) = pccs.get_collateral(fmspc.clone(), ca).await?; + if let Err(e) = dcap_qvl::verify::verify_with_tcb_override( + &input, + &collateral, + now, + override_outdated_tcb, + ) { + if is_fresh { + return Err(e.into()); + } + tracing::warn!("Verification failed - trying with fresh collateral: {e}"); + let collateral = pccs.refresh_collateral(fmspc, ca).await?; + dcap_qvl::verify::verify_with_tcb_override( + &input, + &collateral, + now, + override_outdated_tcb, + )?; + } } }; - let _verified_report = dcap_qvl::verify::verify_with_tcb_override( - &input, - &collateral, - now, - override_outdated_tcb, - )?; - let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; if get_quote_input_data(quote.report) != expected_input_data { @@ -111,7 +123,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - _pccs_url: Option, + _pccs: Pccs, ) -> Result { // In tests we use mock quotes which will fail to verify let quote = tdx_quote::Quote::from_bytes(&input)?; @@ -211,7 +223,7 @@ mod tests { 37, 136, 57, 29, 25, 86, 182, 246, 70, 106, 216, 184, 220, 205, 85, 245, 114, 33, 173, 129, 180, 32, 247, 70, 250, 141, 176, 248, 99, 125, ], - None, + Pccs::new(None), Some(collateral), now, false, @@ -244,7 +256,7 @@ mod tests { 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, ], - None, + Pccs::new(None), Some(collateral), now, true, diff --git a/attested-tls/src/attestation/mod.rs b/attested-tls/src/attestation/mod.rs index f017f2b..311149d 100644 --- a/attested-tls/src/attestation/mod.rs +++ b/attested-tls/src/attestation/mod.rs @@ -4,6 +4,7 @@ pub mod azure; pub mod dcap; pub mod measurements; +pub(crate) mod pccs; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; @@ -16,7 +17,9 @@ use std::{ use thiserror::Error; -use crate::attestation::{dcap::DcapVerificationError, measurements::MeasurementPolicy}; +use crate::attestation::{ + dcap::DcapVerificationError, measurements::MeasurementPolicy, pccs::Pccs, +}; const GCP_METADATA_API: &str = "http://metadata.google.internal/computeMetadata/v1/project/project-id"; @@ -250,24 +253,36 @@ impl AttestationGenerator { pub struct AttestationVerifier { /// The measurement policy with accepted values and attestation types pub measurement_policy: MeasurementPolicy, - /// If this is empty, anything will be accepted - but measurements are always injected into HTTP - /// headers, so that they can be verified upstream - /// A PCCS service to use - defaults to Intel PCS - pub pccs_url: Option, /// Whether to log quotes to a file pub log_dcap_quote: bool, /// Whether to override outdated TCB when on Azure pub override_azure_outdated_tcb: bool, + /// Internal cache for collateral + internal_pccs: Pccs, } impl AttestationVerifier { + pub fn new( + measurement_policy: MeasurementPolicy, + pccs_url: Option, + log_dcap_quote: bool, + override_azure_outdated_tcb: bool, + ) -> Self { + Self { + measurement_policy, + log_dcap_quote, + override_azure_outdated_tcb, + internal_pccs: Pccs::new(pccs_url), + } + } + /// Create an [AttestationVerifier] which will allow no remote attestation pub fn expect_none() -> Self { Self { measurement_policy: MeasurementPolicy::expect_none(), - pccs_url: None, log_dcap_quote: false, override_azure_outdated_tcb: false, + internal_pccs: Pccs::new(None), } } @@ -276,9 +291,9 @@ impl AttestationVerifier { pub fn mock() -> Self { Self { measurement_policy: MeasurementPolicy::mock(), - pccs_url: None, log_dcap_quote: false, override_azure_outdated_tcb: false, + internal_pccs: Pccs::new(None), } } @@ -312,7 +327,7 @@ impl AttestationVerifier { azure::verify_azure_attestation( attestation_exchange_message.attestation, expected_input_data, - self.pccs_url.clone(), + self.internal_pccs.clone(), self.override_azure_outdated_tcb, ) .await? @@ -326,7 +341,7 @@ impl AttestationVerifier { dcap::verify_dcap_attestation( attestation_exchange_message.attestation, expected_input_data, - self.pccs_url.clone(), + self.internal_pccs.clone(), ) .await? } diff --git a/attested-tls/src/attestation/pccs.rs b/attested-tls/src/attestation/pccs.rs new file mode 100644 index 0000000..4502577 --- /dev/null +++ b/attested-tls/src/attestation/pccs.rs @@ -0,0 +1,82 @@ +use std::{collections::HashMap, sync::Arc}; + +use dcap_qvl::{QuoteCollateralV3, collateral::get_collateral_for_fmspc}; +use tokio::sync::RwLock; + +use crate::attestation::dcap::{DcapVerificationError, PCS_URL}; + +#[derive(Clone, Debug)] +pub struct Pccs { + pccs_url: String, + cache: Arc>>, +} + +impl Pccs { + pub fn new(pccs_url: Option) -> Self { + Self { + pccs_url: pccs_url.unwrap_or(PCS_URL.to_string()), + cache: RwLock::new(HashMap::new()).into(), + } + } + + pub async fn get_collateral( + &self, + fmspc: String, + ca: &'static str, + ) -> Result<(QuoteCollateralV3, bool), DcapVerificationError> { + let cache_key = PccsInput::new(fmspc.clone(), ca); + if let Some(collateral) = self.cache.read().await.get(&cache_key).cloned() { + return Ok((collateral, false)); + } + + let collateral = get_collateral_for_fmspc( + &self.pccs_url, + fmspc, + ca, + false, // Indicates not SGX + ) + .await?; + + let mut cache = self.cache.write().await; + let cached = cache + .entry(cache_key) + .or_insert_with(|| collateral.clone()) + .clone(); + Ok((cached, true)) + } + + pub async fn refresh_collateral( + &self, + fmspc: String, + ca: &'static str, + ) -> Result { + let collateral = get_collateral_for_fmspc( + &self.pccs_url, + fmspc.clone(), + ca, + false, // Indicates not SGX + ) + .await?; + + self.cache + .write() + .await + .insert(PccsInput::new(fmspc, ca), collateral.clone()); + Ok(collateral) + } +} + +#[derive(Debug, Hash, PartialEq, Eq)] +struct PccsInput { + fmspc: String, + ca: String, +} + +impl PccsInput { + fn new(fmspc: String, ca: &'static str) -> Self { + Self { + fmspc, + ca: ca.to_string(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 58e0ec9..d955651 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1217,12 +1217,7 @@ mod tests { .await .unwrap(); - let attestation_verifier = AttestationVerifier { - measurement_policy, - pccs_url: None, - log_dcap_quote: false, - override_azure_outdated_tcb: false, - }; + let attestation_verifier = AttestationVerifier::new(measurement_policy, None, false, false); let proxy_client_result = ProxyClient::new_with_tls_config( client_config, diff --git a/src/main.rs b/src/main.rs index d929778..73e802c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,12 +224,12 @@ async fn main() -> anyhow::Result<()> { } }; - let attestation_verifier = AttestationVerifier { + let attestation_verifier = AttestationVerifier::new( measurement_policy, - pccs_url: cli.pccs_url, - log_dcap_quote: cli.log_dcap_quote, - override_azure_outdated_tcb: cli.override_azure_outdated_tcb, - }; + cli.pccs_url, + cli.log_dcap_quote, + cli.override_azure_outdated_tcb, + ); match cli.command { CliCommand::Client { From baf3530a8085e5a28baade68fb060440cf7c19be Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 27 Feb 2026 08:30:12 +0100 Subject: [PATCH 2/6] Add proactive updating --- attested-tls/Cargo.toml | 2 +- attested-tls/src/attestation/dcap.rs | 92 ++++++-- attested-tls/src/attestation/pccs.rs | 312 ++++++++++++++++++++++++--- 3 files changed, 359 insertions(+), 47 deletions(-) diff --git a/attested-tls/Cargo.toml b/attested-tls/Cargo.toml index c1a3273..665bfda 100644 --- a/attested-tls/Cargo.toml +++ b/attested-tls/Cargo.toml @@ -34,7 +34,7 @@ parity-scale-codec = "3.7.5" openssl = "0.10.75" num-bigint = "0.4.6" webpki = { package = "rustls-webpki", version = "0.103.8" } -time = "0.3.44" +time = { version = "0.3.44", features = ["parsing"] } once_cell = "1.21.3" # Used for azure vTPM attestation support diff --git a/attested-tls/src/attestation/dcap.rs b/attested-tls/src/attestation/dcap.rs index 083c5d9..71f9881 100644 --- a/attested-tls/src/attestation/dcap.rs +++ b/attested-tls/src/attestation/dcap.rs @@ -7,6 +7,7 @@ use dcap_qvl::{ QuoteCollateralV3, quote::{Quote, Report}, tcb_info::TcbInfo, + verify::VerifiedReport, }; use thiserror::Error; @@ -58,6 +59,11 @@ pub async fn verify_dcap_attestation_with_given_timestamp( ) -> Result { let quote = Quote::parse(&input)?; tracing::info!("Verifying DCAP attestation: {quote:?}"); + let now_i64 = i64::try_from(now).map_err(|_| { + DcapVerificationError::PccsCollateralParse(format!( + "Timestamp {now} exceeds i64 range" + )) + })?; let ca = quote.ca()?; let fmspc = hex::encode_upper(quote.fmspc()?); @@ -80,32 +86,52 @@ pub async fn verify_dcap_attestation_with_given_timestamp( match collateral { Some(given_collateral) => { - dcap_qvl::verify::verify_with_tcb_override( + let verified_report = dcap_qvl::verify::verify_with_tcb_override( &input, &given_collateral, now, override_outdated_tcb, )?; + warn_if_non_uptodate(&verified_report, &fmspc, CollateralSource::Provided); } None => { - let (collateral, is_fresh) = pccs.get_collateral(fmspc.clone(), ca).await?; - if let Err(e) = dcap_qvl::verify::verify_with_tcb_override( + let (collateral, is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now_i64).await?; + let initial_source = if is_fresh { + CollateralSource::Fresh + } else { + CollateralSource::Cached + }; + let initial_verification = dcap_qvl::verify::verify_with_tcb_override( &input, &collateral, now, override_outdated_tcb, - ) { - if is_fresh { - return Err(e.into()); + ); + + match initial_verification { + Ok(verified_report) => { + warn_if_non_uptodate(&verified_report, &fmspc, initial_source); + } + Err(e) => { + if is_fresh { + return Err(e.into()); + } + tracing::warn!("Verification failed - trying with fresh collateral: {e}"); + let collateral = pccs + .refresh_collateral(fmspc.clone(), ca, now_i64) + .await?; + let verified_report = dcap_qvl::verify::verify_with_tcb_override( + &input, + &collateral, + now, + override_outdated_tcb, + )?; + warn_if_non_uptodate( + &verified_report, + &fmspc, + CollateralSource::RefreshedAfterFailure, + ); } - tracing::warn!("Verification failed - trying with fresh collateral: {e}"); - let collateral = pccs.refresh_collateral(fmspc, ca).await?; - dcap_qvl::verify::verify_with_tcb_override( - &input, - &collateral, - now, - override_outdated_tcb, - )?; } } }; @@ -173,11 +199,49 @@ pub enum DcapVerificationError { SystemTime(#[from] std::time::SystemTimeError), #[error("DCAP quote verification: {0}")] DcapQvl(#[from] anyhow::Error), + #[error("PCCS collateral parse error: {0}")] + PccsCollateralParse(String), + #[error("PCCS collateral expired: {0}")] + PccsCollateralExpired(String), #[cfg(any(test, feature = "mock"))] #[error("Quote parse: {0}")] QuoteParse(#[from] tdx_quote::QuoteParseError), } +/// Origin of collateral used for a verification attempt +#[derive(Clone, Copy, Debug)] +enum CollateralSource { + Provided, + Cached, + Fresh, + RefreshedAfterFailure, +} + +impl CollateralSource { + /// Returns a stable source label for structured logs + fn as_str(self) -> &'static str { + match self { + Self::Provided => "provided", + Self::Cached => "cached", + Self::Fresh => "fresh", + Self::RefreshedAfterFailure => "refreshed_after_failure", + } + } +} + +/// Logs a warning when verification succeeds with a non-UpToDate TCB status +fn warn_if_non_uptodate(report: &VerifiedReport, fmspc: &str, source: CollateralSource) { + if report.status != "UpToDate" { + tracing::warn!( + status = %report.status, + advisory_ids = ?report.advisory_ids, + fmspc, + collateral_source = source.as_str(), + "DCAP verification succeeded with non-UpToDate TCB status" + ); + } +} + #[cfg(test)] mod tests { use crate::attestation::measurements::MeasurementPolicy; diff --git a/attested-tls/src/attestation/pccs.rs b/attested-tls/src/attestation/pccs.rs index 4502577..ed5e68e 100644 --- a/attested-tls/src/attestation/pccs.rs +++ b/attested-tls/src/attestation/pccs.rs @@ -1,17 +1,40 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{Arc, Weak}, + time::{SystemTime, UNIX_EPOCH}, +}; -use dcap_qvl::{QuoteCollateralV3, collateral::get_collateral_for_fmspc}; -use tokio::sync::RwLock; +use dcap_qvl::{QuoteCollateralV3, collateral::get_collateral_for_fmspc, tcb_info::TcbInfo}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; +use tokio::{ + sync::RwLock, + task::JoinHandle, + time::{Duration, sleep}, +}; use crate::attestation::dcap::{DcapVerificationError, PCS_URL}; -#[derive(Clone, Debug)] +const REFRESH_MARGIN_SECS: i64 = 300; +const REFRESH_RETRY_SECS: u64 = 60; + +/// PCCS collateral cache with proactive background refresh +#[derive(Clone)] pub struct Pccs { pccs_url: String, - cache: Arc>>, + cache: Arc>>, +} + +impl std::fmt::Debug for Pccs { + /// Formats PCCS config for debug output without exposing cache internals + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Pccs") + .field("pccs_url", &self.pccs_url) + .finish_non_exhaustive() + } } impl Pccs { + /// Creates a new PCCS cache using the provided URL or Intel PCS default pub fn new(pccs_url: Option) -> Self { Self { pccs_url: pccs_url.unwrap_or(PCS_URL.to_string()), @@ -19,60 +42,93 @@ impl Pccs { } } + /// Returns collateral from cache when valid, otherwise fetches and caches fresh collateral pub async fn get_collateral( &self, fmspc: String, ca: &'static str, + now: i64, ) -> Result<(QuoteCollateralV3, bool), DcapVerificationError> { let cache_key = PccsInput::new(fmspc.clone(), ca); - if let Some(collateral) = self.cache.read().await.get(&cache_key).cloned() { - return Ok((collateral, false)); + + { + let cache = self.cache.read().await; + if let Some(entry) = cache.get(&cache_key) { + if now < entry.next_update { + return Ok((entry.collateral.clone(), false)); + } + tracing::warn!( + fmspc, + next_update = entry.next_update, + now, + "Cached collateral expired, refreshing from PCCS" + ); + } } - let collateral = get_collateral_for_fmspc( - &self.pccs_url, - fmspc, - ca, - false, // Indicates not SGX - ) - .await?; + let collateral = fetch_collateral(&self.pccs_url, fmspc.clone(), ca).await?; + let next_update = extract_next_update(&collateral, now)?; let mut cache = self.cache.write().await; - let cached = cache - .entry(cache_key) - .or_insert_with(|| collateral.clone()) - .clone(); - Ok((cached, true)) + if let Some(existing) = cache.get(&cache_key) { + if now < existing.next_update { + return Ok((existing.collateral.clone(), false)); + } + } + + upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); + drop(cache); + self.ensure_refresh_task(&cache_key).await; + Ok((collateral, true)) } + /// Fetches fresh collateral, overwrites cache, and ensures proactive refresh is scheduled pub async fn refresh_collateral( &self, fmspc: String, ca: &'static str, + now: i64, ) -> Result { - let collateral = get_collateral_for_fmspc( - &self.pccs_url, - fmspc.clone(), - ca, - false, // Indicates not SGX - ) - .await?; - - self.cache - .write() - .await - .insert(PccsInput::new(fmspc, ca), collateral.clone()); + let collateral = fetch_collateral(&self.pccs_url, fmspc.clone(), ca).await?; + let next_update = extract_next_update(&collateral, now)?; + let cache_key = PccsInput::new(fmspc, ca); + + { + let mut cache = self.cache.write().await; + upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); + } + self.ensure_refresh_task(&cache_key).await; Ok(collateral) } + + /// Starts a background refresh loop for a cache key when no task is active + async fn ensure_refresh_task(&self, cache_key: &PccsInput) { + let mut cache = self.cache.write().await; + let Some(entry) = cache.get_mut(cache_key) else { + return; + }; + if entry.refresh_task.is_some() { + return; + } + + let weak_cache = Arc::downgrade(&self.cache); + let key = cache_key.clone(); + let pccs_url = self.pccs_url.clone(); + entry.refresh_task = Some(tokio::spawn(async move { + refresh_loop(weak_cache, pccs_url, key).await; + })); + } } -#[derive(Debug, Hash, PartialEq, Eq)] +/// Cache key for PCCS collateral entries +#[derive(Clone, Debug, Hash, PartialEq, Eq)] struct PccsInput { fmspc: String, ca: String, } impl PccsInput { + /// Builds a cache key from FMSPC and CA identifier fn new(fmspc: String, ca: &'static str) -> Self { Self { fmspc, @@ -80,3 +136,195 @@ impl PccsInput { } } } + +/// Fetches collateral from PCCS for a given FMSPC and CA +async fn fetch_collateral( + pccs_url: &str, + fmspc: String, + ca: &'static str, +) -> Result { + get_collateral_for_fmspc( + pccs_url, + fmspc, + ca, + false, // Indicates not SGX + ) + .await + .map_err(Into::into) +} + +/// Extracts the earliest next update timestamp from collateral metadata +fn extract_next_update(collateral: &QuoteCollateralV3, now: i64) -> Result { + let tcb_info: TcbInfo = serde_json::from_str(&collateral.tcb_info).map_err(|e| { + DcapVerificationError::PccsCollateralParse(format!("Failed to parse TCB info JSON: {e}")) + })?; + let qe_identity: QeIdentityNextUpdate = + serde_json::from_str(&collateral.qe_identity).map_err(|e| { + DcapVerificationError::PccsCollateralParse(format!( + "Failed to parse QE identity JSON: {e}" + )) + })?; + + let tcb_next_update = parse_next_update("tcb_info.nextUpdate", &tcb_info.next_update)?; + let qe_next_update = parse_next_update("qe_identity.nextUpdate", &qe_identity.next_update)?; + let next_update = tcb_next_update.min(qe_next_update); + + if now >= next_update { + return Err(DcapVerificationError::PccsCollateralExpired(format!( + "Collateral expired (tcb_next_update={}, qe_next_update={}, now={now})", + tcb_info.next_update, qe_identity.next_update + ))); + } + + Ok(next_update) +} + +/// Parses an RFC3339 nextUpdate value into a unix timestamp +fn parse_next_update(field: &str, value: &str) -> Result { + OffsetDateTime::parse(value, &Rfc3339) + .map_err(|e| { + DcapVerificationError::PccsCollateralParse(format!( + "Failed to parse {field} as RFC3339: {e}" + )) + }) + .map(|parsed| parsed.unix_timestamp()) +} + +/// Returns current unix time in seconds +fn unix_now() -> Result { + Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64) +} + +/// Computes how many seconds to sleep before refresh should start +fn refresh_sleep_seconds(next_update: i64, now: i64) -> u64 { + let refresh_at = next_update - REFRESH_MARGIN_SECS; + if refresh_at <= now { + 0 + } else { + (refresh_at - now) as u64 + } +} + +/// Inserts or updates a cache entry while preserving any active refresh task +fn upsert_cache_entry( + cache: &mut HashMap, + key: PccsInput, + collateral: QuoteCollateralV3, + next_update: i64, +) { + match cache.get_mut(&key) { + Some(existing) => { + existing.collateral = collateral; + existing.next_update = next_update; + } + None => { + cache.insert( + key, + CacheEntry { + collateral, + next_update, + refresh_task: None, + }, + ); + } + } +} + +/// Converts CA identifier string into the expected static literal +fn ca_as_static(ca: &str) -> Option<&'static str> { + match ca { + "processor" => Some("processor"), + "platform" => Some("platform"), + _ => None, + } +} + +/// Background loop that refreshes collateral for a single cache key +async fn refresh_loop( + weak_cache: Weak>>, + pccs_url: String, + key: PccsInput, +) { + let Some(ca_static) = ca_as_static(&key.ca) else { + tracing::warn!(ca = key.ca, "Unsupported collateral CA value, refresh loop stopping"); + return; + }; + + loop { + let Some(cache) = weak_cache.upgrade() else { + return; + }; + let next_update = { + let cache_guard = cache.read().await; + let Some(entry) = cache_guard.get(&key) else { + return; + }; + entry.next_update + }; + + let now = match unix_now() { + Ok(now) => now, + Err(e) => { + tracing::warn!(error = %e, "Failed to read system time for PCCS refresh"); + sleep(Duration::from_secs(REFRESH_RETRY_SECS)).await; + continue; + } + }; + let sleep_secs = refresh_sleep_seconds(next_update, now); + sleep(Duration::from_secs(sleep_secs)).await; + + match fetch_collateral(&pccs_url, key.fmspc.clone(), ca_static).await { + Ok(collateral) => match extract_next_update(&collateral, now) { + Ok(new_next_update) => { + let Some(cache) = weak_cache.upgrade() else { + return; + }; + let mut cache_guard = cache.write().await; + let Some(entry) = cache_guard.get_mut(&key) else { + return; + }; + entry.collateral = collateral; + entry.next_update = new_next_update; + tracing::debug!( + fmspc = key.fmspc, + ca = key.ca, + next_update = new_next_update, + "Refreshed PCCS collateral in background" + ); + } + Err(e) => { + tracing::warn!( + fmspc = key.fmspc, + ca = key.ca, + error = %e, + "Fetched PCCS collateral but nextUpdate validation failed" + ); + sleep(Duration::from_secs(REFRESH_RETRY_SECS)).await; + } + }, + Err(e) => { + tracing::warn!( + fmspc = key.fmspc, + ca = key.ca, + error = %e, + "Background PCCS collateral refresh failed" + ); + sleep(Duration::from_secs(REFRESH_RETRY_SECS)).await; + } + } + } +} + +/// Cached collateral entry with refresh metadata +struct CacheEntry { + collateral: QuoteCollateralV3, + next_update: i64, + refresh_task: Option>, +} + +/// Minimal QE identity shape needed to read nextUpdate +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct QeIdentityNextUpdate { + next_update: String, +} From 49ec6ac11fc28a9d3192b1c7831591c7e55a8efd Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 27 Feb 2026 09:47:14 +0100 Subject: [PATCH 3/6] Add test mock PCS server --- Cargo.lock | 1 + attestation-provider-server/src/main.rs | 8 +- attested-tls/Cargo.toml | 1 + attested-tls/src/attestation/dcap.rs | 8 +- attested-tls/src/attestation/pccs.rs | 195 +++++++++++++++++++++++- attested-tls/src/lib.rs | 7 +- 6 files changed, 194 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60c1b6f..9f012d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "alloy-rpc-client", "alloy-transport-http", "anyhow", + "axum", "az-tdx-vtpm", "base64 0.22.1", "bytes", diff --git a/attestation-provider-server/src/main.rs b/attestation-provider-server/src/main.rs index b7c5c13..1f9b94f 100644 --- a/attestation-provider-server/src/main.rs +++ b/attestation-provider-server/src/main.rs @@ -94,12 +94,8 @@ async fn main() -> anyhow::Result<()> { None => MeasurementPolicy::accept_anything(), }; - let attestation_verifier = AttestationVerifier { - measurement_policy, - pccs_url: None, - log_dcap_quote: cli.log_dcap_quote, - override_azure_outdated_tcb: false, - }; + let attestation_verifier = + AttestationVerifier::new(measurement_policy, None, cli.log_dcap_quote, false); let attestation_message = attestation_provider_client(server_addr, attestation_verifier).await?; diff --git a/attested-tls/Cargo.toml b/attested-tls/Cargo.toml index 665bfda..4ba1926 100644 --- a/attested-tls/Cargo.toml +++ b/attested-tls/Cargo.toml @@ -60,6 +60,7 @@ rcgen = { version = "0.14.5", optional = true } tdx-quote = { version = "0.0.5", features = ["mock"], optional = true } [dev-dependencies] +axum = "0.8.6" rcgen = "0.14.5" tempfile = "3.23.0" tdx-quote = { version = "0.0.5", features = ["mock"] } diff --git a/attested-tls/src/attestation/dcap.rs b/attested-tls/src/attestation/dcap.rs index 71f9881..6d6ccad 100644 --- a/attested-tls/src/attestation/dcap.rs +++ b/attested-tls/src/attestation/dcap.rs @@ -60,9 +60,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( let quote = Quote::parse(&input)?; tracing::info!("Verifying DCAP attestation: {quote:?}"); let now_i64 = i64::try_from(now).map_err(|_| { - DcapVerificationError::PccsCollateralParse(format!( - "Timestamp {now} exceeds i64 range" - )) + DcapVerificationError::PccsCollateralParse(format!("Timestamp {now} exceeds i64 range")) })?; let ca = quote.ca()?; @@ -117,9 +115,7 @@ pub async fn verify_dcap_attestation_with_given_timestamp( return Err(e.into()); } tracing::warn!("Verification failed - trying with fresh collateral: {e}"); - let collateral = pccs - .refresh_collateral(fmspc.clone(), ca, now_i64) - .await?; + let collateral = pccs.refresh_collateral(fmspc.clone(), ca, now_i64).await?; let verified_report = dcap_qvl::verify::verify_with_tcb_override( &input, &collateral, diff --git a/attested-tls/src/attestation/pccs.rs b/attested-tls/src/attestation/pccs.rs index ed5e68e..8bb5df8 100644 --- a/attested-tls/src/attestation/pccs.rs +++ b/attested-tls/src/attestation/pccs.rs @@ -76,7 +76,12 @@ impl Pccs { } } - upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); + upsert_cache_entry( + &mut cache, + cache_key.clone(), + collateral.clone(), + next_update, + ); drop(cache); self.ensure_refresh_task(&cache_key).await; Ok((collateral, true)) @@ -95,7 +100,12 @@ impl Pccs { { let mut cache = self.cache.write().await; - upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); + upsert_cache_entry( + &mut cache, + cache_key.clone(), + collateral.clone(), + next_update, + ); } self.ensure_refresh_task(&cache_key).await; Ok(collateral) @@ -144,17 +154,17 @@ async fn fetch_collateral( ca: &'static str, ) -> Result { get_collateral_for_fmspc( - pccs_url, - fmspc, - ca, - false, // Indicates not SGX + pccs_url, fmspc, ca, false, // Indicates not SGX ) .await .map_err(Into::into) } /// Extracts the earliest next update timestamp from collateral metadata -fn extract_next_update(collateral: &QuoteCollateralV3, now: i64) -> Result { +fn extract_next_update( + collateral: &QuoteCollateralV3, + now: i64, +) -> Result { let tcb_info: TcbInfo = serde_json::from_str(&collateral.tcb_info).map_err(|e| { DcapVerificationError::PccsCollateralParse(format!("Failed to parse TCB info JSON: {e}")) })?; @@ -246,7 +256,10 @@ async fn refresh_loop( key: PccsInput, ) { let Some(ca_static) = ca_as_static(&key.ca) else { - tracing::warn!(ca = key.ca, "Unsupported collateral CA value, refresh loop stopping"); + tracing::warn!( + ca = key.ca, + "Unsupported collateral CA value, refresh loop stopping" + ); return; }; @@ -328,3 +341,169 @@ struct CacheEntry { struct QeIdentityNextUpdate { next_update: String, } + +#[cfg(test)] +mod tests { + use super::*; + + use axum::{ + Json, Router, + extract::{Query, State}, + response::IntoResponse, + routing::get, + }; + use dcap_qvl::QuoteCollateralV3; + use serde_json::{Value, json}; + use std::{collections::HashMap as StdHashMap, net::SocketAddr, sync::Arc}; + use tokio::{net::TcpListener, task::JoinHandle}; + + #[derive(Clone)] + struct MockPcsConfig { + fmspc: String, + ca: &'static str, + tcb_next_update: String, + qe_next_update: String, + } + + struct MockPcsServer { + base_url: String, + _task: JoinHandle<()>, + } + + impl Drop for MockPcsServer { + fn drop(&mut self) { + self._task.abort(); + } + } + + #[derive(Clone)] + struct MockPcsState { + fmspc: String, + ca: String, + tcb_response: Value, + qe_response: Value, + pck_crl: Vec, + pck_crl_issuer_chain: String, + tcb_issuer_chain: String, + qe_issuer_chain: String, + root_ca_crl_hex: String, + } + + async fn spawn_mock_pcs_server(config: MockPcsConfig) -> MockPcsServer { + let base_collateral: QuoteCollateralV3 = serde_json::from_slice(include_bytes!( + "../../test-assets/dcap-quote-collateral-00.json" + )) + .unwrap(); + + let mut tcb_info: Value = serde_json::from_str(&base_collateral.tcb_info).unwrap(); + tcb_info["nextUpdate"] = Value::String(config.tcb_next_update.clone()); + + let mut qe_identity: Value = serde_json::from_str(&base_collateral.qe_identity).unwrap(); + qe_identity["nextUpdate"] = Value::String(config.qe_next_update.clone()); + + let state = Arc::new(MockPcsState { + fmspc: config.fmspc, + ca: config.ca.to_string(), + tcb_response: json!({ + "tcbInfo": tcb_info, + "signature": hex::encode(&base_collateral.tcb_info_signature), + }), + qe_response: json!({ + "enclaveIdentity": qe_identity, + "signature": hex::encode(&base_collateral.qe_identity_signature), + }), + pck_crl: base_collateral.pck_crl, + pck_crl_issuer_chain: "mock-pck-crl-issuer-chain".to_string(), + tcb_issuer_chain: "mock-tcb-info-issuer-chain".to_string(), + qe_issuer_chain: "mock-qe-issuer-chain".to_string(), + root_ca_crl_hex: hex::encode(base_collateral.root_ca_crl), + }); + + let app = Router::new() + .route("/sgx/certification/v4/pckcrl", get(mock_pck_crl_handler)) + .route("/tdx/certification/v4/tcb", get(mock_tcb_handler)) + .route( + "/tdx/certification/v4/qe/identity", + get(mock_qe_identity_handler), + ) + .route( + "/sgx/certification/v4/rootcacrl", + get(mock_root_ca_crl_handler), + ) + .with_state(state); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr: SocketAddr = listener.local_addr().unwrap(); + let task = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + MockPcsServer { + base_url: format!("http://{addr}"), + _task: task, + } + } + + async fn mock_pck_crl_handler( + State(state): State>, + Query(params): Query>, + ) -> impl IntoResponse { + assert_eq!(params.get("ca"), Some(&state.ca)); + assert_eq!(params.get("encoding"), Some(&"der".to_string())); + ( + [( + "SGX-PCK-CRL-Issuer-Chain", + state.pck_crl_issuer_chain.clone(), + )], + state.pck_crl.clone(), + ) + } + + async fn mock_tcb_handler( + State(state): State>, + Query(params): Query>, + ) -> impl IntoResponse { + assert_eq!(params.get("fmspc"), Some(&state.fmspc)); + ( + [("SGX-TCB-Info-Issuer-Chain", state.tcb_issuer_chain.clone())], + Json(state.tcb_response.clone()), + ) + } + + async fn mock_qe_identity_handler( + State(state): State>, + Query(params): Query>, + ) -> impl IntoResponse { + assert_eq!(params.get("update"), Some(&"standard".to_string())); + ( + [( + "SGX-Enclave-Identity-Issuer-Chain", + state.qe_issuer_chain.clone(), + )], + Json(state.qe_response.clone()), + ) + } + + async fn mock_root_ca_crl_handler(State(state): State>) -> impl IntoResponse { + state.root_ca_crl_hex.clone() + } + + #[tokio::test] + async fn test_mock_pcs_server_helper_with_get_collateral() { + let mock = spawn_mock_pcs_server(MockPcsConfig { + fmspc: "00806F050000".to_string(), + ca: "processor", + tcb_next_update: "2999-01-01T00:00:00Z".to_string(), + qe_next_update: "2999-01-01T00:00:00Z".to_string(), + }) + .await; + + let pccs = Pccs::new(Some(mock.base_url.clone())); + let now = 1_700_000_000_i64; + let (_, is_fresh) = pccs + .get_collateral("00806F050000".to_string(), "processor", now) + .await + .unwrap(); + assert!(is_fresh); + } +} diff --git a/attested-tls/src/lib.rs b/attested-tls/src/lib.rs index dbd08a2..6cde85c 100644 --- a/attested-tls/src/lib.rs +++ b/attested-tls/src/lib.rs @@ -744,12 +744,7 @@ mod tests { .await .unwrap(); - let attestation_verifier = AttestationVerifier { - measurement_policy, - pccs_url: None, - log_dcap_quote: false, - override_azure_outdated_tcb: false, - }; + let attestation_verifier = AttestationVerifier::new(measurement_policy, None, false, false); let client = AttestedTlsClient::new_with_tls_config( client_config, From 4d3fedf87d76d9b9c4042c21064ad16c1cc1aa91 Mon Sep 17 00:00:00 2001 From: peg Date: Fri, 27 Feb 2026 22:01:28 +0100 Subject: [PATCH 4/6] Add test for PCCS --- attested-tls/Cargo.toml | 2 +- attested-tls/src/attestation/pccs.rs | 162 ++++++++++++++++++++++++--- 2 files changed, 145 insertions(+), 19 deletions(-) diff --git a/attested-tls/Cargo.toml b/attested-tls/Cargo.toml index 4ba1926..be6bba7 100644 --- a/attested-tls/Cargo.toml +++ b/attested-tls/Cargo.toml @@ -34,7 +34,7 @@ parity-scale-codec = "3.7.5" openssl = "0.10.75" num-bigint = "0.4.6" webpki = { package = "rustls-webpki", version = "0.103.8" } -time = { version = "0.3.44", features = ["parsing"] } +time = { version = "0.3.44", features = ["parsing", "formatting"] } once_cell = "1.21.3" # Used for azure vTPM attestation support diff --git a/attested-tls/src/attestation/pccs.rs b/attested-tls/src/attestation/pccs.rs index 8bb5df8..254908d 100644 --- a/attested-tls/src/attestation/pccs.rs +++ b/attested-tls/src/attestation/pccs.rs @@ -70,10 +70,10 @@ impl Pccs { let next_update = extract_next_update(&collateral, now)?; let mut cache = self.cache.write().await; - if let Some(existing) = cache.get(&cache_key) { - if now < existing.next_update { - return Ok((existing.collateral.clone(), false)); - } + if let Some(existing) = cache.get(&cache_key) + && now < existing.next_update + { + return Ok((existing.collateral.clone(), false)); } upsert_cache_entry( @@ -354,8 +354,15 @@ mod tests { }; use dcap_qvl::QuoteCollateralV3; use serde_json::{Value, json}; - use std::{collections::HashMap as StdHashMap, net::SocketAddr, sync::Arc}; - use tokio::{net::TcpListener, task::JoinHandle}; + use std::{ + collections::HashMap as StdHashMap, + net::SocketAddr, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + }; + use tokio::{net::TcpListener, task::JoinHandle, time::Duration}; #[derive(Clone)] struct MockPcsConfig { @@ -363,11 +370,15 @@ mod tests { ca: &'static str, tcb_next_update: String, qe_next_update: String, + refreshed_tcb_next_update: Option, + refreshed_qe_next_update: Option, } struct MockPcsServer { base_url: String, _task: JoinHandle<()>, + tcb_calls: Arc, + qe_calls: Arc, } impl Drop for MockPcsServer { @@ -376,17 +387,35 @@ mod tests { } } + impl MockPcsServer { + fn tcb_call_count(&self) -> usize { + self.tcb_calls.load(Ordering::SeqCst) + } + + fn qe_call_count(&self) -> usize { + self.qe_calls.load(Ordering::SeqCst) + } + } + #[derive(Clone)] struct MockPcsState { fmspc: String, ca: String, - tcb_response: Value, - qe_response: Value, + base_tcb_info: Value, + base_qe_identity: Value, + tcb_signature_hex: String, + qe_signature_hex: String, + tcb_next_update: String, + qe_next_update: String, + refreshed_tcb_next_update: Option, + refreshed_qe_next_update: Option, pck_crl: Vec, pck_crl_issuer_chain: String, tcb_issuer_chain: String, qe_issuer_chain: String, root_ca_crl_hex: String, + tcb_calls: Arc, + qe_calls: Arc, } async fn spawn_mock_pcs_server(config: MockPcsConfig) -> MockPcsServer { @@ -401,22 +430,26 @@ mod tests { let mut qe_identity: Value = serde_json::from_str(&base_collateral.qe_identity).unwrap(); qe_identity["nextUpdate"] = Value::String(config.qe_next_update.clone()); + let tcb_calls = Arc::new(AtomicUsize::new(0)); + let qe_calls = Arc::new(AtomicUsize::new(0)); let state = Arc::new(MockPcsState { fmspc: config.fmspc, ca: config.ca.to_string(), - tcb_response: json!({ - "tcbInfo": tcb_info, - "signature": hex::encode(&base_collateral.tcb_info_signature), - }), - qe_response: json!({ - "enclaveIdentity": qe_identity, - "signature": hex::encode(&base_collateral.qe_identity_signature), - }), + base_tcb_info: tcb_info, + base_qe_identity: qe_identity, + tcb_signature_hex: hex::encode(&base_collateral.tcb_info_signature), + qe_signature_hex: hex::encode(&base_collateral.qe_identity_signature), + tcb_next_update: config.tcb_next_update, + qe_next_update: config.qe_next_update, + refreshed_tcb_next_update: config.refreshed_tcb_next_update, + refreshed_qe_next_update: config.refreshed_qe_next_update, pck_crl: base_collateral.pck_crl, pck_crl_issuer_chain: "mock-pck-crl-issuer-chain".to_string(), tcb_issuer_chain: "mock-tcb-info-issuer-chain".to_string(), qe_issuer_chain: "mock-qe-issuer-chain".to_string(), root_ca_crl_hex: hex::encode(base_collateral.root_ca_crl), + tcb_calls: tcb_calls.clone(), + qe_calls: qe_calls.clone(), }); let app = Router::new() @@ -441,6 +474,8 @@ mod tests { MockPcsServer { base_url: format!("http://{addr}"), _task: task, + tcb_calls, + qe_calls, } } @@ -464,9 +499,23 @@ mod tests { Query(params): Query>, ) -> impl IntoResponse { assert_eq!(params.get("fmspc"), Some(&state.fmspc)); + let call_number = state.tcb_calls.fetch_add(1, Ordering::SeqCst) + 1; + let mut tcb_info = state.base_tcb_info.clone(); + let next_update = if call_number == 1 { + state.tcb_next_update.clone() + } else { + state + .refreshed_tcb_next_update + .clone() + .unwrap_or_else(|| state.tcb_next_update.clone()) + }; + tcb_info["nextUpdate"] = Value::String(next_update); ( [("SGX-TCB-Info-Issuer-Chain", state.tcb_issuer_chain.clone())], - Json(state.tcb_response.clone()), + Json(json!({ + "tcbInfo": tcb_info, + "signature": state.tcb_signature_hex, + })), ) } @@ -475,12 +524,26 @@ mod tests { Query(params): Query>, ) -> impl IntoResponse { assert_eq!(params.get("update"), Some(&"standard".to_string())); + let call_number = state.qe_calls.fetch_add(1, Ordering::SeqCst) + 1; + let mut qe_identity = state.base_qe_identity.clone(); + let next_update = if call_number == 1 { + state.qe_next_update.clone() + } else { + state + .refreshed_qe_next_update + .clone() + .unwrap_or_else(|| state.qe_next_update.clone()) + }; + qe_identity["nextUpdate"] = Value::String(next_update); ( [( "SGX-Enclave-Identity-Issuer-Chain", state.qe_issuer_chain.clone(), )], - Json(state.qe_response.clone()), + Json(json!({ + "enclaveIdentity": qe_identity, + "signature": state.qe_signature_hex, + })), ) } @@ -495,6 +558,8 @@ mod tests { ca: "processor", tcb_next_update: "2999-01-01T00:00:00Z".to_string(), qe_next_update: "2999-01-01T00:00:00Z".to_string(), + refreshed_tcb_next_update: None, + refreshed_qe_next_update: None, }) .await; @@ -506,4 +571,65 @@ mod tests { .unwrap(); assert!(is_fresh); } + + #[tokio::test] + async fn test_proactive_refresh_updates_cached_entry() { + let initial_now = unix_now().unwrap(); + let initial_next_update = OffsetDateTime::from_unix_timestamp(initial_now + 2) + .unwrap() + .format(&Rfc3339) + .unwrap(); + let refreshed_next_update = OffsetDateTime::from_unix_timestamp(initial_now + 3600) + .unwrap() + .format(&Rfc3339) + .unwrap(); + + let mock = spawn_mock_pcs_server(MockPcsConfig { + fmspc: "00806F050000".to_string(), + ca: "processor", + tcb_next_update: initial_next_update.clone(), + qe_next_update: initial_next_update, + refreshed_tcb_next_update: Some(refreshed_next_update.clone()), + refreshed_qe_next_update: Some(refreshed_next_update), + }) + .await; + + let pccs = Pccs::new(Some(mock.base_url.clone())); + let (_, is_fresh) = pccs + .get_collateral("00806F050000".to_string(), "processor", initial_now) + .await + .unwrap(); + assert!(is_fresh); + assert_eq!(mock.tcb_call_count(), 1); + assert_eq!(mock.qe_call_count(), 1); + + for _ in 0..60 { + if mock.tcb_call_count() >= 2 && mock.qe_call_count() >= 2 { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + assert!( + mock.tcb_call_count() >= 2, + "expected proactive TCB refresh to run" + ); + assert!( + mock.qe_call_count() >= 2, + "expected proactive QE identity refresh to run" + ); + + let before_check_calls = mock.tcb_call_count(); + let now_after_background = unix_now().unwrap(); + let (_, is_fresh_again) = pccs + .get_collateral( + "00806F050000".to_string(), + "processor", + now_after_background, + ) + .await + .unwrap(); + assert!(!is_fresh_again); + assert_eq!(mock.tcb_call_count(), before_check_calls); + } } From 0b381c41d735ed4e04232c0064293f622f9f9619 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 2 Mar 2026 11:32:37 +0100 Subject: [PATCH 5/6] Fix for azure feature --- attested-tls/src/attestation/azure/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/attested-tls/src/attestation/azure/mod.rs b/attested-tls/src/attestation/azure/mod.rs index 9187f52..cd32816 100644 --- a/attested-tls/src/attestation/azure/mod.rs +++ b/attested-tls/src/attestation/azure/mod.rs @@ -418,8 +418,8 @@ mod tests { let measurements = verify_azure_attestation_with_given_timestamp( attestation_bytes.to_vec(), [0; 64], // Input data - None, - collateral, + Pccs::new(None), + Some(collateral), now, false, ) @@ -445,7 +445,7 @@ mod tests { let err = verify_azure_attestation_with_given_timestamp( attestation_bytes.to_vec(), expected_input_data, - None, + Pccs::new(None), Some(collateral), now, false, From 62111702b5bdd6199dbe23e445611d1c39eb7764 Mon Sep 17 00:00:00 2001 From: peg Date: Mon, 2 Mar 2026 12:47:55 +0100 Subject: [PATCH 6/6] Improve testing --- attested-tls/src/attestation/pccs.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/attested-tls/src/attestation/pccs.rs b/attested-tls/src/attestation/pccs.rs index 254908d..be2837d 100644 --- a/attested-tls/src/attestation/pccs.rs +++ b/attested-tls/src/attestation/pccs.rs @@ -603,6 +603,14 @@ mod tests { assert_eq!(mock.tcb_call_count(), 1); assert_eq!(mock.qe_call_count(), 1); + let (_, is_fresh_second) = pccs + .get_collateral("00806F050000".to_string(), "processor", initial_now) + .await + .unwrap(); + assert!(!is_fresh_second); + assert_eq!(mock.tcb_call_count(), 1); + assert_eq!(mock.qe_call_count(), 1); + for _ in 0..60 { if mock.tcb_call_count() >= 2 && mock.qe_call_count() >= 2 { break;