From 4a698c1062ccda006bddfc91210a13935a63af8e Mon Sep 17 00:00:00 2001 From: Samuel Laferriere <9342524+samlaf@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:51:54 -0400 Subject: [PATCH] pccs: report a missing rustls crypto provider instead of panicking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace pins reqwest with rustls-no-provider, so building a reqwest::Client requires the application to have installed a process-level rustls crypto provider. That requirement was documented nowhere and enforced only by the #[cfg(test)] install helpers, and the one client pccs builds itself lives in fetch_fmspcs — called from the pre-warm task Pccs::new spawns in its constructor. An application that never installed a provider therefore got a panic inside a detached task it cannot catch, dumping a full backtrace on every Pccs construction. That panic was worse than log noise: - Pccs::ready() deadlocked: the task died before finish_prewarm, so the outcome channel never resolved — and it cannot even close, because the Pccs instance being awaited holds the sender alive. An application doing the responsible thing and waiting for the cache before serving hung forever, with no error and no timeout. - Under panic = "abort" the same code killed the whole process, so the library's failure mode ranged from invisible to fatal depending on the consumer's build profile. The blast radius was otherwise confined to the pre-warm: on-demand collateral fetches and background refreshes go through dcap-qvl, whose client bundles its own TLS provider, so verification kept working while the FMSPC-discovery step died silently. Check for the provider before building the client and return a new PccsError::MissingCryptoProvider naming the fix. The pre-warm then degrades through its existing failed-fetch path — one warning line stating the consequence (no warm cache, collateral fetched on demand) and the remedy — and ready() surfaces the same error immediately, so applications that consider a warm cache mandatory get fail-fast semantics. The precondition is now documented on Pccs. Deliberately not chosen: installing aws-lc-rs as a silent fallback when no provider is set. That would force the aws-lc-rs build dependency on every consumer — the choice rustls-no-provider exists to leave with the application — and an application installing its own provider late with install_default().unwrap() could lose the race against the pre-warm task. The rustls dependency added here carries no provider feature: only the process-default lookup is used. --- crates/pccs/Cargo.toml | 5 +++++ crates/pccs/src/lib.rs | 26 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/pccs/Cargo.toml b/crates/pccs/Cargo.toml index 9ea11a2..ac4f30d 100644 --- a/crates/pccs/Cargo.toml +++ b/crates/pccs/Cargo.toml @@ -17,6 +17,9 @@ serde_json = "1.0.145" hex = "0.4.3" anyhow = "1.0.100" reqwest = { workspace = true } +# No provider feature: only the process-default lookup is needed here — which +# crypto provider backs it stays the application's choice. +rustls = { workspace = true, default-features = false } x509-parser = "0.18.0" [dev-dependencies] @@ -24,4 +27,6 @@ rcgen = "0.14.5" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt"] } serde-saphyr = "0.0.22" mock-tdx = { workspace = true } +# Tests exercise real TLS fetches against local mock servers, so they install +# a concrete provider. rustls = { workspace = true, default-features = false, features = ["aws_lc_rs"] } diff --git a/crates/pccs/src/lib.rs b/crates/pccs/src/lib.rs index ae8ee81..2945104 100644 --- a/crates/pccs/src/lib.rs +++ b/crates/pccs/src/lib.rs @@ -43,6 +43,13 @@ const REFRESH_RETRY_SECS: u64 = 60; const STARTUP_PREWARM_CONCURRENCY: usize = 8; /// PCCS collateral cache with proactive background refresh +/// +/// Fetching runs over rustls-backed HTTP, so the application must install a +/// process-level rustls [crypto provider] before collateral can be fetched, +/// e.g. `rustls::crypto::aws_lc_rs::default_provider().install_default()`. +/// Without one, fetches fail with [`PccsError::MissingCryptoProvider`]. +/// +/// [crypto provider]: https://github.com/rustls/rustls#cryptography-providers #[derive(Clone)] pub struct Pccs { /// The URL of the service used to fetch collateral (PCS / PCCS) @@ -300,7 +307,11 @@ impl Pccs { let fmspcs = match self.fetch_fmspcs().await { Ok(fmspcs) => fmspcs, Err(e) => { - tracing::warn!(error = %e, "Failed to fetch FMSPC list for startup pre-provision"); + tracing::warn!( + error = %e, + "Failed to fetch FMSPC list for startup pre-provision; continuing \ + without a warm cache — collateral is fetched on demand" + ); return PrewarmOutcome::Failed(format!( "Failed to fetch FMSPC list for prewarm: {e}" )); @@ -392,6 +403,13 @@ impl Pccs { #[cfg(test)] install_test_crypto_provider(); + // Without a process-level crypto provider, building the reqwest + // client panics; error instead, so the pre-warm task logs a warning + // rather than dumping a panic backtrace. + if rustls::crypto::CryptoProvider::get_default().is_none() { + return Err(PccsError::MissingCryptoProvider); + } + let url = format!("{}/sgx/certification/v4/fmspcs", self.url); let client = reqwest::Client::builder().timeout(Duration::from_secs(15)).build()?; let response = client.get(&url).send().await?; @@ -719,6 +737,12 @@ pub enum PccsError { SystemTime(#[from] std::time::SystemTimeError), #[error("HTTP client: {0}")] Reqwest(#[from] reqwest::Error), + #[error( + "no process-level rustls crypto provider is installed; install one at application \ + startup, e.g. `rustls::crypto::aws_lc_rs::default_provider().install_default()` — \ + see https://github.com/rustls/rustls#cryptography-providers" + )] + MissingCryptoProvider, #[error("Failed to fetch FMSPC: {0}")] FmspcFetch(reqwest::StatusCode), #[error("JSON: {0}")]