diff --git a/Cargo.lock b/Cargo.lock index 08abe81..2ed35aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,7 +341,7 @@ dependencies = [ "anyhow", "az-tdx-vtpm", "base64 0.22.1", - "dcap-qvl 0.3.12 (git+https://github.com/Phala-Network/dcap-qvl.git?rev=f1dcc65371e941a7b83e3234833d23a1fb232ab1)", + "dcap-qvl 0.5.2", "hex", "http 1.4.0", "mock-tdx", @@ -701,19 +701,20 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate", @@ -840,6 +841,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1000,6 +1012,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1252,8 +1274,9 @@ dependencies = [ [[package]] name = "dcap-qvl" -version = "0.3.12" -source = "git+https://github.com/Phala-Network/dcap-qvl.git?rev=f1dcc65371e941a7b83e3234833d23a1fb232ab1#f1dcc65371e941a7b83e3234833d23a1fb232ab1" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a14fb8954c867d6855e44d98eab18e769816357738406691ebe60d8fdd005d" dependencies = [ "anyhow", "asn1_der", @@ -1271,7 +1294,7 @@ dependencies = [ "p256", "parity-scale-codec", "pem", - "reqwest 0.12.28", + "reqwest 0.13.4", "ring", "rustls-pki-types", "scale-info", @@ -1459,7 +1482,7 @@ source = "git+https://github.com/Dstack-TEE/dstack.git?rev=07d2cf6bd376a3c56f855 dependencies = [ "anyhow", "cc-eventlog 0.5.11", - "dcap-qvl 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "dcap-qvl 0.3.12", "dstack-types", "errify", "ez-hash", @@ -1903,6 +1926,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -1968,6 +1992,30 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto 0.26.1", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + [[package]] name = "hickory-proto" version = "0.25.2" @@ -1993,6 +2041,26 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + [[package]] name = "hickory-resolver" version = "0.25.2" @@ -2001,7 +2069,7 @@ checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-proto", + "hickory-proto 0.25.2", "ipconfig", "moka", "once_cell", @@ -2014,6 +2082,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto 0.26.1", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -2340,6 +2434,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -2444,11 +2541,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2647,7 +2745,7 @@ name = "mock-tdx" version = "0.0.1" dependencies = [ "axum", - "dcap-qvl 0.3.12 (git+https://github.com/Phala-Network/dcap-qvl.git?rev=f1dcc65371e941a7b83e3234833d23a1fb232ab1)", + "dcap-qvl 0.5.2", "hex", "p256", "parity-scale-codec", @@ -2681,6 +2779,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nested-tls" version = "0.0.1" @@ -3005,7 +3109,8 @@ name = "pccs" version = "0.0.1" dependencies = [ "anyhow", - "dcap-qvl 0.3.12 (git+https://github.com/Phala-Network/dcap-qvl.git?rev=f1dcc65371e941a7b83e3234833d23a1fb232ab1)", + "dcap-qvl 0.5.2", + "futures-executor", "hex", "mock-tdx", "rcgen 0.14.7", @@ -3018,6 +3123,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "ureq", "x509-parser 0.18.1", ] @@ -3178,6 +3284,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -3313,7 +3430,7 @@ dependencies = [ "anyhow", "bon", "cc-eventlog 0.5.11", - "dcap-qvl 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "dcap-qvl 0.3.12", "dstack-attest", "dstack-types", "elliptic-curve", @@ -3369,6 +3486,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3407,6 +3535,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rayon" version = "1.11.0" @@ -3525,7 +3659,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "hickory-resolver", + "hickory-resolver 0.25.2", "http 1.4.0", "http-body", "http-body-util", @@ -3565,6 +3699,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures-core", + "hickory-resolver 0.26.1", "http 1.4.0", "http-body", "http-body-util", @@ -3573,6 +3708,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "once_cell", "percent-encoding", "pin-project-lite", "quinn", @@ -3744,7 +3880,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", @@ -3870,7 +4006,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4243,6 +4379,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -4855,9 +5012,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4868,23 +5025,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4892,9 +5045,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -4905,9 +5058,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -4948,9 +5101,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 7b56e67..5bf2756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,5 +26,5 @@ reqwest = { version = "0.13.4", default-features = false, features = ["rustls"] rustls = { version = "0.23.37", default-features = false, features = ["brotli"] } tokio = { version = "1.50.0", features = ["default"] } tokio-rustls = { version = "0.26.4", default-features = false } -dcap-qvl = { git = "https://github.com/Phala-Network/dcap-qvl.git", rev = "f1dcc65371e941a7b83e3234833d23a1fb232ab1" } +dcap-qvl = "0.5.2" pccs = { path = "crates/pccs" } diff --git a/crates/attestation/README.md b/crates/attestation/README.md index 5620969..f590264 100644 --- a/crates/attestation/README.md +++ b/crates/attestation/README.md @@ -12,14 +12,14 @@ This crate provides: ## Runtime Requirements -Verification uses the [`pccs`](../pccs) crate for collateral caching and -background refresh. As a result, constructing an `AttestationVerifier` with -PCCS enabled and calling verification APIs is expected to happen from within a -Tokio runtime and might panic if called outside of one. +DCAP attestation generation uses the [`pccs`](../pccs) crate to fetch and bundle +the collateral required to verify the quote. DCAP verification consumes this +bundled collateral, so verifier-side network fetching is not required for the +normal DCAP path. -Note that although some of the verification API methods are synchronous (for -example `verify_attestation_sync`), still their functionality depends on -Tokio-backed background tasks such as PCCS pre-warm and cache refresh. +Constructing generators that fetch DCAP collateral is expected to happen from +within a Tokio runtime because PCCS cache pre-warm and refresh are driven by +Tokio background tasks. ## Feature flags @@ -83,11 +83,12 @@ attempted. Alternatively, an external 'attestation provider service' URL can be provided which outsources the attestation generation to another process. -When verifying DCAP attestations, the Intel PCS is used to retrieve collateral -unless a PCCS URL is provided via a command line argument. If outdated TCB is -used, the quote will fail to verify. For special cases where outdated TCB -should be allowed, a custom override function can be passed when verifying which -may modify collateral before it is validated against the TCB. +When generating DCAP attestations, Intel PCS is used to retrieve collateral +unless a PCCS URL is configured. The quote and collateral are then serialized +together as the DCAP evidence. If outdated TCB is used, the quote will fail to +verify. For special cases where outdated TCB should be allowed, a custom +override function can be passed when verifying which may modify collateral +before it is validated against the TCB. ## Measurements File diff --git a/crates/attestation/src/azure/mod.rs b/crates/attestation/src/azure/mod.rs index 42c88ed..a5c50ac 100644 --- a/crates/attestation/src/azure/mod.rs +++ b/crates/attestation/src/azure/mod.rs @@ -623,6 +623,7 @@ pub enum MaaError { #[cfg(test)] mod test_utils { use base64::{Engine as _, engine::general_purpose::URL_SAFE as BASE64_URL_SAFE}; + use dcap_qvl::collateral::CollateralClient; use super::{AttestationDocument, create_azure_attestation}; use crate::dcap::PCS_URL; @@ -662,17 +663,11 @@ mod test_utils { assert!(intermediate_count > 0, "captured attestation should include AK intermediates"); let quote_bytes = BASE64_URL_SAFE.decode(&attestation_document.tdx_quote_base64).unwrap(); - let quote = dcap_qvl::quote::Quote::parse("e_bytes).unwrap(); - let ca = quote.ca().unwrap(); - let fmspc = hex::encode_upper(quote.fmspc().unwrap()); - let collateral = dcap_qvl::collateral::get_collateral_for_fmspc( - PCS_URL, - fmspc.clone(), - ca, - false, // TDX, not SGX. - ) - .await - .unwrap(); + let collateral = CollateralClient::with_default_http(PCS_URL) + .unwrap() + .fetch("e_bytes) + .await + .unwrap(); let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); @@ -698,7 +693,6 @@ mod test_utils { println!("wrote {}", attestation_path.display()); println!("wrote {}", collateral_path.display()); - println!("quote fmspc={fmspc} ca={ca}"); println!("ak_intermediate_certificates_pem entries={intermediate_count}"); } } diff --git a/crates/attestation/src/dcap.rs b/crates/attestation/src/dcap.rs index dca2c10..1874bf4 100644 --- a/crates/attestation/src/dcap.rs +++ b/crates/attestation/src/dcap.rs @@ -2,7 +2,7 @@ //! verification use dcap_qvl::{ QuoteCollateralV3, - collateral::get_collateral_for_fmspc, + collateral::CollateralClient, quote::{Quote, Report}, tcb_info::TcbInfo, }; @@ -11,7 +11,7 @@ use mock_tdx::generate_mock_tdx_quote; use pccs::{Pccs, PccsError}; use thiserror::Error; -use crate::{AttestationError, measurements::MultiMeasurements}; +use crate::{AttestationError, DcapWithCollateral, measurements::MultiMeasurements}; /// FMSPC with which to override TCB level checks on Azure (not used for GCP /// or other platforms) @@ -20,52 +20,82 @@ const AZURE_BAD_FMSPC: &str = "90C06F000000"; /// For fetching collateral directly from Intel, if no PCCS is specified pub const PCS_URL: &str = "https://api.trustedservices.intel.com"; -/// Generate a TDX quote -pub fn create_dcap_attestation(input_data: [u8; 64]) -> Result, AttestationError> { +/// Generate a TDX quote and bundle the collateral needed to verify it. +pub fn create_dcap_attestation( + input_data: [u8; 64], + pccs: Option<&Pccs>, +) -> Result, AttestationError> { let quote = generate_quote(input_data)?; tracing::info!("Generated TDX quote of {} bytes", quote.len()); - Ok(quote) + + let fallback_pccs; + let pccs = match pccs { + Some(pccs) => pccs, + None => { + fallback_pccs = Pccs::new_without_prewarm(None); + &fallback_pccs + } + }; + let collateral = pccs.get_collateral_for_quote_sync("e)?; + + Ok(serde_json::to_vec(&DcapWithCollateral { quote, collateral })?) +} + +pub fn quote_from_dcap_attestation(input: &[u8]) -> Result { + let evidence = parse_dcap_evidence(input)?; + Ok(Quote::parse(&evidence.quote)?) +} + +fn parse_dcap_evidence(input: &[u8]) -> Result { + serde_json::from_slice(input).map_err(DcapVerificationError::from) } /// Verify a DCAP TDX quote, and return the measurement values #[cfg(not(any(test, feature = "mock")))] +// Keep this async to preserve the public verifier API even though bundled +// collateral makes this implementation synchronous. +#[allow(clippy::unused_async)] pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - pccs: Option, + _pccs: Option, ) -> Result { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; - verify_dcap_attestation_with_given_timestamp( - input, + let evidence = parse_dcap_evidence(&input)?; + let quote = Quote::parse(&evidence.quote)?; + verify_dcap_attestation_with_collateral_and_timestamp( + evidence.quote, + quote, expected_input_data, - pccs, - None, + evidence.collateral, now, override_azure_outdated_tcb, ) - .await } /// Synchronous version - Verify a DCAP TDX quote, and return the /// measurement values /// -/// This relies on having DCAP collateral already present in the cache +/// This verifies the quote with the collateral bundled in the DCAP +/// evidence, so it does not fetch collateral from the verifier side. /// /// If possible, prefer the async version #[cfg(not(any(test, feature = "mock")))] pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], - pccs: Pccs, + _pccs: Pccs, ) -> Result { let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let override_azure_outdated_tcb = false; - verify_dcap_attestation_with_timestamp_sync( - input, + let evidence = parse_dcap_evidence(&input)?; + let quote = Quote::parse(&evidence.quote)?; + verify_dcap_attestation_with_collateral_and_timestamp( + evidence.quote, + quote, expected_input_data, - pccs, - None, + evidence.collateral, now, override_azure_outdated_tcb, ) @@ -74,7 +104,8 @@ pub fn verify_dcap_attestation_sync( /// Verify a DCAP TDX quote, and return the measurement values, providing a /// timestamp an optional pre-fetched collateral /// -/// This relies on having DCAP collateral already present in the cache +/// This helper accepts raw quote bytes. If collateral is not provided, it +/// fetches quote collateral via the supplied PCCS client. /// /// If possible, prefer the async version pub fn verify_dcap_attestation_with_timestamp_sync( @@ -87,13 +118,10 @@ pub fn verify_dcap_attestation_with_timestamp_sync( ) -> Result { let quote = Quote::parse(&input)?; - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = if let Some(given_collateral) = collateral { given_collateral } else { - pccs.get_collateral_sync(fmspc.clone(), ca, now)? + pccs.get_collateral_for_quote_sync(&input)? }; verify_dcap_attestation_with_collateral_and_timestamp( @@ -121,22 +149,12 @@ pub async fn verify_dcap_attestation_with_given_timestamp( ) -> Result { let quote = Quote::parse(&input)?; - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let collateral = if let Some(given_collateral) = collateral { given_collateral } else if let Some(ref pccs) = pccs_option { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc.clone(), ca, now).await?; - collateral + pccs.get_collateral_for_quote(&input).await? } else { - get_collateral_for_fmspc( - PCS_URL, - fmspc.clone(), - ca, - false, // Indicates not SGX - ) - .await? + CollateralClient::with_default_http(PCS_URL)?.fetch(&input).await? }; verify_dcap_attestation_with_collateral_and_timestamp( @@ -159,8 +177,6 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( ) -> Result { tracing::info!("Verifying DCAP attestation: {quote:?}"); - let fmspc = hex::encode_upper(quote.fmspc()?); - // Override outdated TCB only if we are on Azure and the FMSPC is known to // be outdated let override_outdated_tcb = if override_azure_outdated_tcb { @@ -190,7 +206,6 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( tracing::warn!( status = %verified_report.status, advisory_ids = ?verified_report.advisory_ids, - fmspc, "DCAP verification succeeded with non-UpToDate TCB status" ); } @@ -208,20 +223,11 @@ fn verify_dcap_attestation_with_collateral_and_timestamp( pub async fn verify_dcap_attestation( input: Vec, expected_input_data: [u8; 64], - pccs: Option, + _pccs: Option, ) -> Result { - let quote = Quote::parse(&input)?; - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); - let collateral = if let Some(ref pccs) = pccs { - let (collateral, _is_fresh) = pccs.get_collateral(fmspc, ca, now).await?; - collateral - } else { - mock_tdx::mock_collateral() - }; - let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; + let evidence = parse_dcap_evidence(&input)?; + let quote = Quote::parse(&evidence.quote)?; + let _collateral = evidence.collateral; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; if get_quote_input_data(quote.report) != expected_input_data { @@ -235,15 +241,11 @@ pub async fn verify_dcap_attestation( pub fn verify_dcap_attestation_sync( input: Vec, expected_input_data: [u8; 64], - pccs: Pccs, + _pccs: Pccs, ) -> Result { - let quote = Quote::parse(&input)?; - let ca = quote.ca()?; - let fmspc = hex::encode_upper(quote.fmspc()?); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); - let collateral = pccs.get_collateral_sync(fmspc, ca, now)?; - let verifier = mock_tdx::mock_dcap_verifier(); - verifier.verify(&input, &collateral, now)?; + let evidence = parse_dcap_evidence(&input)?; + let quote = Quote::parse(&evidence.quote)?; + let _collateral = evidence.collateral; let measurements = MultiMeasurements::from_dcap_qvl_quote("e)?; if get_quote_input_data(quote.report.clone()) != expected_input_data { @@ -290,6 +292,8 @@ pub enum DcapVerificationError { Pccs(#[from] PccsError), #[error("Timestamp exceeds i64 range")] TimeStampExceedsI64, + #[error("DCAP evidence JSON: {0}")] + SerdeJson(#[from] serde_json::Error), } #[cfg(test)] @@ -397,7 +401,7 @@ mod tests { .unwrap(); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_mock_dcap_verify_uses_pccs_when_provided() { let mock_pcs = spawn_mock_pcs_server(MockPcsConfig { include_fmspcs_listing: false, @@ -407,7 +411,7 @@ mod tests { .unwrap(); let pccs = Pccs::new(Some(mock_pcs.base_url.clone())); let expected_input_data = [0xA5; 64]; - let attestation_bytes = create_dcap_attestation(expected_input_data).unwrap(); + let attestation_bytes = create_dcap_attestation(expected_input_data, Some(&pccs)).unwrap(); let measurements = verify_dcap_attestation(attestation_bytes, expected_input_data, Some(pccs)) diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 73bc5ba..69d54aa 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -12,6 +12,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use dcap_qvl::QuoteCollateralV3; use measurements::MultiMeasurements; use parity_scale_codec::{Decode, Encode}; use pccs::{Pccs, PccsError}; @@ -33,6 +34,14 @@ pub struct AttestationExchangeMessage { pub attestation: Vec, } +/// DCAP attestation evidence bundled with the collateral required to verify +/// it. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DcapWithCollateral { + pub quote: Vec, + pub collateral: QuoteCollateralV3, +} + impl AttestationExchangeMessage { /// Create an empty attestation payload for the case that we are running /// in a non-confidential environment @@ -56,8 +65,7 @@ impl AttestationExchangeMessage { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - let quote = dcap_qvl::verify::Quote::parse(&self.attestation) - .map_err(DcapVerificationError::from)?; + let quote = dcap::quote_from_dcap_attestation(&self.attestation)?; Ok(Some(MultiMeasurements::from_dcap_qvl_quote("e)?)) } } @@ -143,6 +151,7 @@ impl Display for AttestationType { pub struct AttestationGenerator { pub attestation_type: AttestationType, attestation_provider_url: Option, + internal_pccs: Option, } impl AttestationGenerator { @@ -150,13 +159,35 @@ impl AttestationGenerator { pub fn new( attestation_type: AttestationType, attestation_provider_url: Option, + ) -> Result { + Self::new_with_pccs_url(attestation_type, attestation_provider_url, None) + } + + /// Create an attestation generator with given attestation type and a + /// PCCS URL to use when bundling DCAP collateral. + pub fn new_with_pccs_url( + attestation_type: AttestationType, + attestation_provider_url: Option, + pccs_url: Option, ) -> Result { // If an attestation provider is given, normalize the URL and check that it // looks like a local IP let attestation_provider_url = attestation_provider_url.map(map_attestation_provider_url).transpose()?; - Ok(Self { attestation_type, attestation_provider_url }) + let internal_pccs = if attestation_provider_url.is_some() { + None + } else { + match attestation_type { + AttestationType::DcapTdx | + AttestationType::GcpTdx | + AttestationType::QemuTdx | + AttestationType::AzureTdx => Some(Pccs::new_without_prewarm(pccs_url)), + AttestationType::None => None, + } + }; + + Ok(Self { attestation_type, attestation_provider_url, internal_pccs }) } /// Detect what confidential compute platform is present and create the @@ -165,9 +196,20 @@ impl AttestationGenerator { Self::new_with_detection(None, None) } + /// Detect what confidential compute platform is present and create the + /// appropriate attestation generator, using the provided PCCS URL for + /// bundled DCAP collateral. + pub fn detect_with_pccs_url(pccs_url: Option) -> Result { + Self::new_with_detection_and_pccs_url(None, None, pccs_url) + } + /// Do not generate attestations pub fn with_no_attestation() -> Self { - Self { attestation_type: AttestationType::None, attestation_provider_url: None } + Self { + attestation_type: AttestationType::None, + attestation_provider_url: None, + internal_pccs: None, + } } /// Create an [AttestationGenerator] detecting the attestation type if @@ -175,13 +217,27 @@ impl AttestationGenerator { pub fn new_with_detection( attestation_type_string: Option, attestation_provider_url: Option, + ) -> Result { + Self::new_with_detection_and_pccs_url( + attestation_type_string, + attestation_provider_url, + None, + ) + } + + /// Create an [AttestationGenerator] detecting the attestation type if + /// it is not given, with a PCCS URL for bundled DCAP collateral. + pub fn new_with_detection_and_pccs_url( + attestation_type_string: Option, + attestation_provider_url: Option, + pccs_url: Option, ) -> Result { if attestation_provider_url.is_some() { // If a remote provide is used, dont do detection let attestation_type = serde_json::from_value(serde_json::Value::String( attestation_type_string.ok_or(AttestationError::AttestationTypeNotGiven)?, ))?; - return Self::new(attestation_type, attestation_provider_url); + return Self::new_with_pccs_url(attestation_type, attestation_provider_url, pccs_url); }; let attestation_type_string = attestation_type_string.unwrap_or_else(|| "auto".to_string()); @@ -193,7 +249,7 @@ impl AttestationGenerator { }; tracing::info!("Local platform: {attestation_type}"); - Self::new(attestation_type, None) + Self::new_with_pccs_url(attestation_type, None, pccs_url) } /// Generate an attestation exchange message with given input data @@ -233,7 +289,7 @@ impl AttestationGenerator { } } AttestationType::DcapTdx | AttestationType::GcpTdx | AttestationType::QemuTdx => { - dcap::create_dcap_attestation(input_data) + dcap::create_dcap_attestation(input_data, self.internal_pccs.as_ref()) } } } @@ -688,12 +744,13 @@ mod tests { assert_eq!(wrapped.attestation, vec![9, 8]); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn mock_verifier_supports_sync_verification() { let input_data = [7u8; 64]; - let attestation = dcap::create_dcap_attestation(input_data).unwrap(); - let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig::default()).await.unwrap(); + let pccs = Pccs::new(Some(mock_pcs_server.base_url.clone())); + pccs.ready().await.unwrap(); + let attestation = dcap::create_dcap_attestation(input_data, Some(&pccs)).unwrap(); let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); if let Some(ref pccs) = verifier.internal_pccs { diff --git a/crates/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs index 1495611..5be2e11 100644 --- a/crates/attested-tls/src/lib.rs +++ b/crates/attested-tls/src/lib.rs @@ -1104,50 +1104,59 @@ mod tests { builder.finish().unwrap() } - #[tokio::test] + async fn mock_dcap_attestation_generator() -> AttestationGenerator { + let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig { + include_fmspcs_listing: false, + ..MockPcsConfig::default() + }) + .await + .unwrap(); + let base_url = mock_pcs_server.base_url.clone(); + std::mem::forget(mock_pcs_server); + AttestationGenerator::new_with_pccs_url(AttestationType::DcapTdx, None, Some(base_url)) + .unwrap() + } + + #[tokio::test(flavor = "multi_thread")] async fn certificate_resolver_creates_initial_certificate() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_key_pair(&key_pair) - .with_crypto_provider(provider) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_key_pair(&key_pair) + .with_crypto_provider(provider) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let certificate = resolver.state.certificate.read().unwrap(); assert_eq!(certificate.len(), 1); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn certificate_resolver_rejects_too_short_validity_duration() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let error = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider) - .with_key_pair(&key_pair) - .with_certificate_validity(CERTIFICATE_RENEWAL_RETRY_DELAY * 3) - .finish() - .unwrap_err(); + let error = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider) + .with_key_pair(&key_pair) + .with_certificate_validity(CERTIFICATE_RENEWAL_RETRY_DELAY * 3) + .finish() + .unwrap_err(); assert!(matches!(error, AttestedTlsError::InvalidCertificateValidityDuration { .. })); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn server_and_client_configs_complete_a_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); let server_name = "foo"; let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let resolver = AttestedCertificateResolver::build( server_name, - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), + mock_dcap_attestation_generator().await, ) .with_crypto_provider(provider.clone()) .with_key_pair(&key_pair) @@ -1183,7 +1192,7 @@ mod tests { assert!(!server.is_handshaking()); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn ca_signed_server_and_client_configs_complete_a_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); @@ -1193,7 +1202,7 @@ mod tests { let resolver = AttestedCertificateResolver::build( server_name, - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), + mock_dcap_attestation_generator().await, ) .with_crypto_provider(provider.clone()) .with_key_pair(&key_pair) @@ -1238,31 +1247,37 @@ mod tests { assert!(!server.is_handshaking()); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn certificate_is_renewed_before_expiry() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", AttestationGenerator::with_no_attestation()) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let initial_certificate = resolver.state.certificate.read().unwrap().first().unwrap().clone(); - tokio::time::sleep(renewal_delay(Duration::from_secs(4)) + Duration::from_secs(1)).await; - - let renewed_certificate = - resolver.state.certificate.read().unwrap().first().unwrap().clone(); + let renewed = tokio::time::timeout(Duration::from_secs(7), async { + loop { + let renewed_certificate = + resolver.state.certificate.read().unwrap().first().unwrap().clone(); + if initial_certificate.as_ref() != renewed_certificate.as_ref() { + break true; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .unwrap_or(false); - assert_ne!(initial_certificate.as_ref(), renewed_certificate.as_ref()); + assert!(renewed, "certificate was not renewed before the test timeout"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn server_and_client_configs_complete_a_mutual_auth_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); @@ -1270,7 +1285,7 @@ mod tests { let server_resolver = AttestedCertificateResolver::build( server_name, - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), + mock_dcap_attestation_generator().await, ) .with_crypto_provider(provider.clone()) .with_key_pair(&key_pair) @@ -1278,15 +1293,13 @@ mod tests { .finish() .unwrap(); - let client_resolver = AttestedCertificateResolver::build( - "client", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let client_resolver = + AttestedCertificateResolver::build("client", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let server_verifier = ready_mock_attested_verifier(None, provider.clone()).await; let client_verifier = ready_mock_attested_verifier(None, provider.clone()).await; @@ -1318,22 +1331,20 @@ mod tests { assert!(server.peer_certificates().is_some()); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn alternate_san_completes_a_handshake() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let subject = "foo"; let alternate_name = "bar"; - let resolver = AttestedCertificateResolver::build( - subject, - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_subject_alt_names(vec![alternate_name.to_string(), subject.to_string()]) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build(subject, mock_dcap_attestation_generator().await) + .with_subject_alt_names(vec![alternate_name.to_string(), subject.to_string()]) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let verifier = ready_mock_attested_verifier(None, provider.clone()).await; let server_config = ServerConfig::builder_with_provider(provider.clone()) @@ -1361,7 +1372,7 @@ mod tests { assert!(!server.is_handshaking()); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn malformed_certificate_returns_bad_encoding() { let provider: Arc = aws_lc_rs::default_provider().into(); let verifier = AttestedCertificateVerifier::build(AttestationVerifier::mock()) @@ -1380,7 +1391,7 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::BadEncoding)); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn certificate_without_attestation_extension_returns_bad_encoding() { let provider: Arc = aws_lc_rs::default_provider().into(); let cert = plain_self_signed_certificate("foo"); @@ -1402,19 +1413,17 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::BadEncoding),); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn private_ca_verifier_rejects_untrusted_self_signed_attested_server_cert() { let provider: Arc = aws_lc_rs::default_provider().into(); let ca = test_ca(); let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); let mut roots = RootCertStore::empty(); @@ -1436,19 +1445,17 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::UnknownIssuer)); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn private_ca_verifier_rejects_untrusted_self_signed_attested_client_cert() { let provider: Arc = aws_lc_rs::default_provider().into(); let ca = test_ca(); let ca_cert = CertificateDer::from_pem_slice(ca.pem_cert.as_bytes()).unwrap(); - let resolver = AttestedCertificateResolver::build( - "client", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("client", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); let mut roots = RootCertStore::empty(); @@ -1465,20 +1472,18 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::UnknownIssuer)); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn non_self_signed_attested_certificate_with_unknown_issuer_is_rejected() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_ca_cert(test_ca()) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_ca_cert(test_ca()) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let verifier = AttestedCertificateVerifier::build(AttestationVerifier::mock()) .with_crypto_provider(provider) .finish() @@ -1498,19 +1503,17 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::UnknownIssuer)); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn self_signed_attested_certificate_with_wrong_name_is_rejected() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let verifier = AttestedCertificateVerifier::build(AttestationVerifier::mock()) .with_crypto_provider(provider) .finish() @@ -1530,19 +1533,17 @@ mod tests { ),); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn self_signed_attested_certificate_with_allowed_pubkey_is_accepted() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig::default()).await.unwrap(); let verifier = AttestationVerifier::mock_with_pccs(mock_pcs_server.base_url.clone()); if let Some(ref pccs) = verifier.internal_pccs { @@ -1564,20 +1565,18 @@ mod tests { .unwrap(); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn self_signed_attested_certificate_with_not_allowed_pubkey_is_rejected() { let provider: Arc = aws_lc_rs::default_provider().into(); let trusted_key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); let presented_key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&presented_key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&presented_key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let verifier = AttestedCertificateVerifier::build(AttestationVerifier::mock()) .with_crypto_provider(provider) .with_allowed_leaf_cert_pubkey(&trusted_key_pair.public_key_der()) @@ -1595,19 +1594,17 @@ mod tests { assert_eq!(result.unwrap_err(), Error::InvalidCertificate(CertificateError::UnknownIssuer)); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn certificate_binding_changes_when_identity_changes() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let cert = resolver.state.certificate.read().unwrap(); let cert = @@ -1638,19 +1635,17 @@ mod tests { assert_ne!(original_report_data, replayed_report_data); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn attestation_rejection_returns_application_verification_failure() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let verifier = AttestedCertificateVerifier::build(AttestationVerifier::expect_none()) .with_crypto_provider(provider) .finish() @@ -1670,19 +1665,17 @@ mod tests { ); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn verifier_reuses_trusted_certificate_cache() { let provider: Arc = aws_lc_rs::default_provider().into(); let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); + let resolver = + AttestedCertificateResolver::build("foo", mock_dcap_attestation_generator().await) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .with_certificate_validity(Duration::from_secs(4)) + .finish() + .unwrap(); let mut verifier = ready_mock_attested_verifier(None, provider).await; let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); @@ -1714,78 +1707,6 @@ mod tests { .unwrap(); } - #[tokio::test(flavor = "multi_thread")] - async fn sync_verifier_cache_miss_fails_then_succeeds_after_background_fetch() { - let provider: Arc = aws_lc_rs::default_provider().into(); - let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - "foo", - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .with_certificate_validity(Duration::from_secs(4)) - .finish() - .unwrap(); - let cert = resolver.state.certificate.read().unwrap().first().unwrap().clone(); - - // Mock PCS is set up to not list the FMSPCs, meaning the pre-warm - // wont fetch anything - let mock_pcs = spawn_mock_pcs_server(MockPcsConfig { - include_fmspcs_listing: false, - ..MockPcsConfig::default() - }) - .await - .unwrap(); - - let verifier = AttestedCertificateVerifier::build(AttestationVerifier::mock_with_pccs( - mock_pcs.base_url.clone(), - )) - .with_crypto_provider(provider) - .finish() - .unwrap(); - - let first_result = verify_server_cert_direct( - &verifier, - &cert, - &ServerName::try_from("foo").unwrap(), - UnixTime::now(), - ); - - // Initially verification fails because the PCCS doesn't have the - // collateral associated with the quote - assert_eq!( - first_result.unwrap_err(), - Error::InvalidCertificate(CertificateError::ApplicationVerificationFailure) - ); - - // Now we wait a moment for the PCCS to fetch it in the background - for _ in 0..50 { - if verify_server_cert_direct( - &verifier, - &cert, - &ServerName::try_from("foo").unwrap(), - UnixTime::now(), - ) - .is_ok() - { - break; - } - tokio::time::sleep(Duration::from_millis(20)).await; - } - - // Now verification succeeds - verify_server_cert_direct( - &verifier, - &cert, - &ServerName::try_from("foo").unwrap(), - UnixTime::now(), - ) - .unwrap(); - assert_eq!(mock_pcs.tcb_call_count(), 1); - assert_eq!(mock_pcs.qe_call_count(), 1); - } - /// Helper to create a private certificate authority fn test_ca() -> CaCert { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); @@ -1816,7 +1737,15 @@ mod tests { } }) .await - .expect("TLS handshake timed out"); + .unwrap_or_else(|_| { + panic!( + "TLS handshake timed out: client_handshaking={}, server_handshaking={}, client_wants_write={}, server_wants_write={}", + client.is_handshaking(), + server.is_handshaking(), + client.wants_write(), + server.wants_write() + ) + }); } fn transfer_tls_client_to_server(client: &mut ClientConnection, server: &mut ServerConnection) { @@ -1830,8 +1759,11 @@ mod tests { return; } - server.read_tls(&mut Cursor::new(tls)).unwrap(); - server.process_new_packets().unwrap(); + let mut reader = Cursor::new(tls); + while (reader.position() as usize) < reader.get_ref().len() { + server.read_tls(&mut reader).unwrap(); + server.process_new_packets().unwrap(); + } } fn transfer_tls_server_to_client(server: &mut ServerConnection, client: &mut ClientConnection) { @@ -1845,7 +1777,10 @@ mod tests { return; } - client.read_tls(&mut Cursor::new(tls)).unwrap(); - client.process_new_packets().unwrap(); + let mut reader = Cursor::new(tls); + while (reader.position() as usize) < reader.get_ref().len() { + client.read_tls(&mut reader).unwrap(); + client.process_new_packets().unwrap(); + } } } diff --git a/crates/attested-tls/tests/nested_tls.rs b/crates/attested-tls/tests/nested_tls.rs index 7110e8f..4ed3a92 100644 --- a/crates/attested-tls/tests/nested_tls.rs +++ b/crates/attested-tls/tests/nested_tls.rs @@ -19,7 +19,19 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; async fn nested_tls_uses_attested_tls_for_inner_session() { let provider: Arc = aws_lc_rs::default_provider().into(); let (outer_server, outer_client) = plain_tls_config_pair(provider.clone()); - let inner_server = attested_server_config("localhost", provider.clone()); + let mock_pcs_server = spawn_mock_pcs_server(MockPcsConfig { + include_fmspcs_listing: false, + ..MockPcsConfig::default() + }) + .await + .unwrap(); + let attestation_generator = AttestationGenerator::new_with_pccs_url( + AttestationType::DcapTdx, + None, + Some(mock_pcs_server.base_url.clone()), + ) + .unwrap(); + let inner_server = attested_server_config("localhost", provider.clone(), attestation_generator); let inner_client = attested_client_config(provider.clone()).await; let acceptor = NestingTlsAcceptor::new(Arc::new(outer_server), Arc::new(inner_server)); @@ -84,16 +96,17 @@ fn plain_tls_config_pair(provider: Arc) -> (ServerConfig, Client /// Create attested server TLS config with mock DCAP attestation and /// self-signed certs -fn attested_server_config(server_name: &str, provider: Arc) -> ServerConfig { +fn attested_server_config( + server_name: &str, + provider: Arc, + attestation_generator: AttestationGenerator, +) -> ServerConfig { let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap(); - let resolver = AttestedCertificateResolver::build( - server_name, - AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(), - ) - .with_crypto_provider(provider.clone()) - .with_key_pair(&key_pair) - .finish() - .unwrap(); + let resolver = AttestedCertificateResolver::build(server_name, attestation_generator) + .with_crypto_provider(provider.clone()) + .with_key_pair(&key_pair) + .finish() + .unwrap(); ServerConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() diff --git a/crates/mock-tdx/assets/mock-dcap-collateral.yaml b/crates/mock-tdx/assets/mock-dcap-collateral.yaml index 65edd7b..69ea7af 100644 --- a/crates/mock-tdx/assets/mock-dcap-collateral.yaml +++ b/crates/mock-tdx/assets/mock-dcap-collateral.yaml @@ -1,14 +1,14 @@ pck_crl_issuer_chain: | -----BEGIN CERTIFICATE----- - MIIBlDCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu + MIIBkjCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu dGVsIFJvb3QgQ0EwHhcNMjUwMTAxMDAwMDAwWhcNNDUwMTAxMDAwMDAwWjAkMSIw IAYDVQQDDBlNb2NrIEludGVsIFRDQiBTaWduaW5nIENBMFkwEwYHKoZIzj0CAQYI KoZIzj0DAQcDQgAE1lqTl3yqPRsIGFL/V6eeRl8WYFdzBLrq1QXdOkhYnPNQGF6J U3LfYiHqOhN1V+Rz/dtnVfBb1QfDxTP86ckShaNjMGEwHwYDVR0jBBgwFoAUdoBa Y6aYDBgHVCShPzJ3LQLXxxswDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBS/y6K3 - QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kA - MEYCIQCvhCZKzOyaNkad7y1vBE4SKtT8nRZqCx/Y82ugmDoAjgIhAIs/9uHaNmOD - Uip8B/h+JVgIm8FoNs5EOc5D/PkyoEKk + QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cA + MEQCIAqjX358ogovm7xUGCyt2AYWWexu+2+v2+Z8In3g9SpIAiBKBbcJ9j7dHHwV + F8/SydqTkoyH7ZCntrAaDUj54WuTQQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBjDCCATKgAwIBAgIBATAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu @@ -17,36 +17,36 @@ pck_crl_issuer_chain: | BwNCAAQCF+YX8LZEOSgnj5aZnmmiOk8sFSvfbWzfZuW4AoLU7RlKfevLl3EtLdo8 qFqodlpW9F/HWFmWUvKJfGUwbleUo2MwYTAfBgNVHSMEGDAWgBR2gFpjppgMGAdU JKE/MnctAtfHGzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFHaAWmOmmAwYB1Qk - oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANOM - o5zM6NZ93Iewr2S2g0MiM+6mMJaJNDfY5pXp82amAiBXJ1pB709SgQCgRmICY6GJ - LsG1gRFnBX+0dG80hRXdPA== + oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgUkku + HlTnhfmK5YS0wNCWYOCSA+g1kw/lsvPU/KO4iwQCIQD0aqV+CVTm2hSCSdI5sg98 + xF93VZkGf9a4XeVi1V0yjg== -----END CERTIFICATE----- root_ca_crl: >- - 3081d5307d020101300a06082a8648ce3d040302301d311b301906035504030c124d6f636b20496e74656c20526f6f74204341170d3235303130313030303030305a170d3435303130313030303030305aa02f302d301f0603551d2304183016801476805a63a6980c18075424a13f32772d02d7c71b300a0603551d140403020101300a06082a8648ce3d0403020348003045022034caddb53533343cde3c792b6a4457ce1685d07fda266591d276774ace219a3f022100a26423311d592db905ef49ab329ffce8b1ef4e0e0fd05b56f4085789038b035b + 3081d5307d020101300a06082a8648ce3d040302301d311b301906035504030c124d6f636b20496e74656c20526f6f74204341170d3235303130313030303030305a170d3435303130313030303030305aa02f302d301f0603551d2304183016801476805a63a6980c18075424a13f32772d02d7c71b300a0603551d140403020101300a06082a8648ce3d0403020348003045022007a738fe4fe9989daed8e6c084d099635cd91d812d9b81f0be05fbf5dd92a6e102210081f3bb82e1d04b41d4bdf7733b89173bcdd2825fa393dada83a4285f609fef95 pck_crl: >- - 3081dc308184020101300a06082a8648ce3d04030230243122302006035504030c194d6f636b20496e74656c20544342205369676e696e67204341170d3235303130313030303030305a170d3435303130313030303030305aa02f302d301f0603551d23041830168014bfcba2b742a807bbb72b522fa4694c4604ff68eb300a0603551d140403020102300a06082a8648ce3d040302034700304402205062b6aee1fea13dea47a816f419df3da4af7f71a2a98887d027c72d983366f2022030f8baae33ab09b7d9826ad238761e6e365079671d1e1cb31ee1e339d8da4249 + 3081dd308184020101300a06082a8648ce3d04030230243122302006035504030c194d6f636b20496e74656c20544342205369676e696e67204341170d3235303130313030303030305a170d3435303130313030303030305aa02f302d301f0603551d23041830168014bfcba2b742a807bbb72b522fa4694c4604ff68eb300a0603551d140403020102300a06082a8648ce3d0403020348003045022068633fe78a94e35c027784f745543a6285206ef545221ddc2771c62b83f369c3022100e251447284890fef082fd031fbd8655c8330e919584d2f03e2d7cc9c9dfa5b1c tcb_info_issuer_chain: | -----BEGIN CERTIFICATE----- - MIIBlzCCATygAwIBAgIBAzAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlNb2NrIElu + MIIBljCCATygAwIBAgIBAzAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlNb2NrIElu dGVsIFRDQiBTaWduaW5nIENBMB4XDTI1MDEwMTAwMDAwMFoXDTQ1MDEwMTAwMDAw MFowIDEeMBwGA1UEAwwVTW9jayBJbnRlbCBUQ0IgU2lnbmVyMFkwEwYHKoZIzj0C AQYIKoZIzj0DAQcDQgAEUadYCDOJjqGxg8vXNQpAmQeMbvHB4Y6XDNdoMDXyXn0B EFInErC1p8/wgWhUhphKlOaDHtrEbnNg+p2DSnqBoaNjMGEwHwYDVR0jBBgwFoAU v8uit0KoB7u3K1IvpGlMRgT/aOswDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBST 7j5t5QAoWFiJAKg4c8ROKT5hpTAPBgNVHRMBAf8EBTADAQEAMAoGCCqGSM49BAMC - A0kAMEYCIQCAhGx8v+2u1fXhC8xMtzeouG654iUvC684nd3q7TBHMwIhAKqvK38E - Mu8JWo589cyxCqsAErRhSodsqUcW/MyDC0hL + A0gAMEUCICeLU1S5LkBKWI9V0Xe/Nzj8G/KdDhH3uAr60KlbUo8pAiEArCS0RMVX + A98R+Na8OVbu8PO3Oj7e1iP3S6HFXdtT9js= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- - MIIBlDCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu + MIIBkjCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu dGVsIFJvb3QgQ0EwHhcNMjUwMTAxMDAwMDAwWhcNNDUwMTAxMDAwMDAwWjAkMSIw IAYDVQQDDBlNb2NrIEludGVsIFRDQiBTaWduaW5nIENBMFkwEwYHKoZIzj0CAQYI KoZIzj0DAQcDQgAE1lqTl3yqPRsIGFL/V6eeRl8WYFdzBLrq1QXdOkhYnPNQGF6J U3LfYiHqOhN1V+Rz/dtnVfBb1QfDxTP86ckShaNjMGEwHwYDVR0jBBgwFoAUdoBa Y6aYDBgHVCShPzJ3LQLXxxswDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBS/y6K3 - QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kA - MEYCIQCvhCZKzOyaNkad7y1vBE4SKtT8nRZqCx/Y82ugmDoAjgIhAIs/9uHaNmOD - Uip8B/h+JVgIm8FoNs5EOc5D/PkyoEKk + QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cA + MEQCIAqjX358ogovm7xUGCyt2AYWWexu+2+v2+Z8In3g9SpIAiBKBbcJ9j7dHHwV + F8/SydqTkoyH7ZCntrAaDUj54WuTQQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBjDCCATKgAwIBAgIBATAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu @@ -55,35 +55,35 @@ tcb_info_issuer_chain: | BwNCAAQCF+YX8LZEOSgnj5aZnmmiOk8sFSvfbWzfZuW4AoLU7RlKfevLl3EtLdo8 qFqodlpW9F/HWFmWUvKJfGUwbleUo2MwYTAfBgNVHSMEGDAWgBR2gFpjppgMGAdU JKE/MnctAtfHGzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFHaAWmOmmAwYB1Qk - oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANOM - o5zM6NZ93Iewr2S2g0MiM+6mMJaJNDfY5pXp82amAiBXJ1pB709SgQCgRmICY6GJ - LsG1gRFnBX+0dG80hRXdPA== + oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgUkku + HlTnhfmK5YS0wNCWYOCSA+g1kw/lsvPU/KO4iwQCIQD0aqV+CVTm2hSCSdI5sg98 + xF93VZkGf9a4XeVi1V0yjg== -----END CERTIFICATE----- -tcb_info: "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2025-01-01T00:00:00Z\",\"nextUpdate\":\"2045-01-01T00:00:00Z\",\"fmspc\":\"00906EA10000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":1,\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":11},{\"svn\":11},{\"svn\":2},{\"svn\":2},{\"svn\":255},{\"svn\":1},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"tdxtcbcomponents\":[{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1}],\"pcesvn\":13},\"tcbDate\":\"2025-01-01T00:00:00Z\",\"tcbStatus\":\"UpToDate\",\"advisoryIDs\":[]}]}" +tcb_info: "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2025-01-01T00:00:00Z\",\"nextUpdate\":\"2045-01-01T00:00:00Z\",\"fmspc\":\"00906EA10000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":1,\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":11},{\"svn\":11},{\"svn\":2},{\"svn\":2},{\"svn\":255},{\"svn\":1},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"tdxtcbcomponents\":[{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1},{\"svn\":1}],\"pcesvn\":13},\"tcbDate\":\"2025-01-01T00:00:00Z\",\"tcbStatus\":\"UpToDate\",\"advisoryIDs\":[]}],\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"0000000000000000\"},\"tdxModuleIdentities\":[]}" tcb_info_signature: >- - 81deffe35b79b7d7cfa1b4f7a62cf2f661f7d47c1d53838f8c48199ebbd14af77605f8a9bf060aafc48624a5a70be20307bb9e622345fd59f40966967ff1bce1 + f91dd3fd877aa219b4c4e90de650f2617f4c2316fb1313bab55579bcf779b4977100958fc600191078e45ef39a595cda63961f2bf5fd9cc0da03874305dde185 qe_identity_issuer_chain: | -----BEGIN CERTIFICATE----- - MIIBlzCCATygAwIBAgIBAzAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlNb2NrIElu + MIIBljCCATygAwIBAgIBAzAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlNb2NrIElu dGVsIFRDQiBTaWduaW5nIENBMB4XDTI1MDEwMTAwMDAwMFoXDTQ1MDEwMTAwMDAw MFowIDEeMBwGA1UEAwwVTW9jayBJbnRlbCBUQ0IgU2lnbmVyMFkwEwYHKoZIzj0C AQYIKoZIzj0DAQcDQgAEUadYCDOJjqGxg8vXNQpAmQeMbvHB4Y6XDNdoMDXyXn0B EFInErC1p8/wgWhUhphKlOaDHtrEbnNg+p2DSnqBoaNjMGEwHwYDVR0jBBgwFoAU v8uit0KoB7u3K1IvpGlMRgT/aOswDgYDVR0PAQH/BAQDAgeAMB0GA1UdDgQWBBST 7j5t5QAoWFiJAKg4c8ROKT5hpTAPBgNVHRMBAf8EBTADAQEAMAoGCCqGSM49BAMC - A0kAMEYCIQCAhGx8v+2u1fXhC8xMtzeouG654iUvC684nd3q7TBHMwIhAKqvK38E - Mu8JWo589cyxCqsAErRhSodsqUcW/MyDC0hL + A0gAMEUCICeLU1S5LkBKWI9V0Xe/Nzj8G/KdDhH3uAr60KlbUo8pAiEArCS0RMVX + A98R+Na8OVbu8PO3Oj7e1iP3S6HFXdtT9js= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- - MIIBlDCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu + MIIBkjCCATmgAwIBAgIBAjAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu dGVsIFJvb3QgQ0EwHhcNMjUwMTAxMDAwMDAwWhcNNDUwMTAxMDAwMDAwWjAkMSIw IAYDVQQDDBlNb2NrIEludGVsIFRDQiBTaWduaW5nIENBMFkwEwYHKoZIzj0CAQYI KoZIzj0DAQcDQgAE1lqTl3yqPRsIGFL/V6eeRl8WYFdzBLrq1QXdOkhYnPNQGF6J U3LfYiHqOhN1V+Rz/dtnVfBb1QfDxTP86ckShaNjMGEwHwYDVR0jBBgwFoAUdoBa Y6aYDBgHVCShPzJ3LQLXxxswDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBS/y6K3 - QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kA - MEYCIQCvhCZKzOyaNkad7y1vBE4SKtT8nRZqCx/Y82ugmDoAjgIhAIs/9uHaNmOD - Uip8B/h+JVgIm8FoNs5EOc5D/PkyoEKk + QqgHu7crUi+kaUxGBP9o6zAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cA + MEQCIAqjX358ogovm7xUGCyt2AYWWexu+2+v2+Z8In3g9SpIAiBKBbcJ9j7dHHwV + F8/SydqTkoyH7ZCntrAaDUj54WuTQQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBjDCCATKgAwIBAgIBATAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu @@ -92,16 +92,16 @@ qe_identity_issuer_chain: | BwNCAAQCF+YX8LZEOSgnj5aZnmmiOk8sFSvfbWzfZuW4AoLU7RlKfevLl3EtLdo8 qFqodlpW9F/HWFmWUvKJfGUwbleUo2MwYTAfBgNVHSMEGDAWgBR2gFpjppgMGAdU JKE/MnctAtfHGzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFHaAWmOmmAwYB1Qk - oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANOM - o5zM6NZ93Iewr2S2g0MiM+6mMJaJNDfY5pXp82amAiBXJ1pB709SgQCgRmICY6GJ - LsG1gRFnBX+0dG80hRXdPA== + oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgUkku + HlTnhfmK5YS0wNCWYOCSA+g1kw/lsvPU/KO4iwQCIQD0aqV+CVTm2hSCSdI5sg98 + xF93VZkGf9a4XeVi1V0yjg== -----END CERTIFICATE----- qe_identity: "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2025-01-01T00:00:00Z\",\"nextUpdate\":\"2045-01-01T00:00:00Z\",\"tcbEvaluationDataNumber\":1,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"00000000000000000000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\",\"mrsigner\":\"5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":11},\"tcbDate\":\"2025-01-01T00:00:00Z\",\"tcbStatus\":\"UpToDate\",\"advisoryIDs\":[]}]}" qe_identity_signature: >- 7394339f635a123e047fb29ead5ce6faac737b5d648fb28b7b27d3e71829454bac1aff1d3c70b37bdca5bf2571d9c097ee58cf1c48693d844f5c4699d3c4f85b pck_certificate_chain: | -----BEGIN CERTIFICATE----- - MIIDZDCCAwqgAwIBAgIBBDAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu + MIIDYzCCAwqgAwIBAgIBBDAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu dGVsIFJvb3QgQ0EwHhcNMjUwMTAxMDAwMDAwWhcNNDUwMTAxMDAwMDAwWjAZMRcw FQYDVQQDDA5Nb2NrIEludGVsIFBDSzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA BFs2iQ2svXyalrt0oe4os9LXW3LgmiDvJc+Ob9ip8DUNDhS+2NRoKjTYNTi9/1uW @@ -117,9 +117,9 @@ pck_certificate_chain: | MBAGCyqGSIb4TQENAQIOAgEAMBAGCyqGSIb4TQENAQIPAgEAMBAGCyqGSIb4TQEN AQIQAgEAMBAGCyqGSIb4TQENAQIRAgENMB8GCyqGSIb4TQENAQISBBALCwIC/wEA AAAAAAAAAAAAMBAGCiqGSIb4TQENAQMEAgAAMBQGCiqGSIb4TQENAQQEBgCQbqEA - ADAPBgoqhkiG+E0BDQEFCgEAMAoGCCqGSM49BAMCA0gAMEUCID830FZbEZLj3Zwv - +45GtB9pkIWKWgKXr/582kNwIagiAiEAttIFwEKZhgyjPIWgQsa0g31aUvKgtl31 - 9CfxzKBt/Qs= + ADAPBgoqhkiG+E0BDQEFCgEAMAoGCCqGSM49BAMCA0cAMEQCIGdIHmfHjyGq+q8h + /dCMpJlGVvJ4/tp9w1akW7bory70AiBYLwQy6rc3gzwAJitNn+H6BDmQI+D8h8F/ + Gl4XfbjOFQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBjDCCATKgAwIBAgIBATAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu @@ -128,7 +128,7 @@ pck_certificate_chain: | BwNCAAQCF+YX8LZEOSgnj5aZnmmiOk8sFSvfbWzfZuW4AoLU7RlKfevLl3EtLdo8 qFqodlpW9F/HWFmWUvKJfGUwbleUo2MwYTAfBgNVHSMEGDAWgBR2gFpjppgMGAdU JKE/MnctAtfHGzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFHaAWmOmmAwYB1Qk - oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANOM - o5zM6NZ93Iewr2S2g0MiM+6mMJaJNDfY5pXp82amAiBXJ1pB709SgQCgRmICY6GJ - LsG1gRFnBX+0dG80hRXdPA== + oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgUkku + HlTnhfmK5YS0wNCWYOCSA+g1kw/lsvPU/KO4iwQCIQD0aqV+CVTm2hSCSdI5sg98 + xF93VZkGf9a4XeVi1V0yjg== -----END CERTIFICATE----- diff --git a/crates/mock-tdx/assets/mock-pck-chain.pem b/crates/mock-tdx/assets/mock-pck-chain.pem index b6b41c0..5eb4c69 100644 --- a/crates/mock-tdx/assets/mock-pck-chain.pem +++ b/crates/mock-tdx/assets/mock-pck-chain.pem @@ -1,5 +1,5 @@ -----BEGIN CERTIFICATE----- -MIIDZDCCAwqgAwIBAgIBBDAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu +MIIDYzCCAwqgAwIBAgIBBDAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu dGVsIFJvb3QgQ0EwHhcNMjUwMTAxMDAwMDAwWhcNNDUwMTAxMDAwMDAwWjAZMRcw FQYDVQQDDA5Nb2NrIEludGVsIFBDSzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA BFs2iQ2svXyalrt0oe4os9LXW3LgmiDvJc+Ob9ip8DUNDhS+2NRoKjTYNTi9/1uW @@ -15,9 +15,9 @@ SIb4TQENAQILAgEAMBAGCyqGSIb4TQENAQIMAgEAMBAGCyqGSIb4TQENAQINAgEA MBAGCyqGSIb4TQENAQIOAgEAMBAGCyqGSIb4TQENAQIPAgEAMBAGCyqGSIb4TQEN AQIQAgEAMBAGCyqGSIb4TQENAQIRAgENMB8GCyqGSIb4TQENAQISBBALCwIC/wEA AAAAAAAAAAAAMBAGCiqGSIb4TQENAQMEAgAAMBQGCiqGSIb4TQENAQQEBgCQbqEA -ADAPBgoqhkiG+E0BDQEFCgEAMAoGCCqGSM49BAMCA0gAMEUCID830FZbEZLj3Zwv -+45GtB9pkIWKWgKXr/582kNwIagiAiEAttIFwEKZhgyjPIWgQsa0g31aUvKgtl31 -9CfxzKBt/Qs= +ADAPBgoqhkiG+E0BDQEFCgEAMAoGCCqGSM49BAMCA0cAMEQCIGdIHmfHjyGq+q8h +/dCMpJlGVvJ4/tp9w1akW7bory70AiBYLwQy6rc3gzwAJitNn+H6BDmQI+D8h8F/ +Gl4XfbjOFQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBjDCCATKgAwIBAgIBATAKBggqhkjOPQQDAjAdMRswGQYDVQQDDBJNb2NrIElu @@ -26,7 +26,7 @@ GQYDVQQDDBJNb2NrIEludGVsIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMB BwNCAAQCF+YX8LZEOSgnj5aZnmmiOk8sFSvfbWzfZuW4AoLU7RlKfevLl3EtLdo8 qFqodlpW9F/HWFmWUvKJfGUwbleUo2MwYTAfBgNVHSMEGDAWgBR2gFpjppgMGAdU JKE/MnctAtfHGzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFHaAWmOmmAwYB1Qk -oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhANOM -o5zM6NZ93Iewr2S2g0MiM+6mMJaJNDfY5pXp82amAiBXJ1pB709SgQCgRmICY6GJ -LsG1gRFnBX+0dG80hRXdPA== +oT8ydy0C18cbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgUkku +HlTnhfmK5YS0wNCWYOCSA+g1kw/lsvPU/KO4iwQCIQD0aqV+CVTm2hSCSdI5sg98 +xF93VZkGf9a4XeVi1V0yjg== -----END CERTIFICATE----- diff --git a/crates/mock-tdx/assets/mock-root-ca.der b/crates/mock-tdx/assets/mock-root-ca.der index fa50252..538cfe9 100644 Binary files a/crates/mock-tdx/assets/mock-root-ca.der and b/crates/mock-tdx/assets/mock-root-ca.der differ diff --git a/crates/mock-tdx/src/lib.rs b/crates/mock-tdx/src/lib.rs index 50a2989..d77eadb 100644 --- a/crates/mock-tdx/src/lib.rs +++ b/crates/mock-tdx/src/lib.rs @@ -85,10 +85,7 @@ pub const MOCK_RTMR3: [u8; 48] = [0x80; 48]; /// Get a DCAP quote verifier with the mock PCK root-of-trust pub fn mock_dcap_verifier() -> dcap_qvl::verify::QuoteVerifier { - dcap_qvl::verify::QuoteVerifier::new( - EMBEDDED_ROOT_CA_DER.to_vec(), - dcap_qvl::verify::rustcrypto::backend(), - ) + dcap_qvl::verify::QuoteVerifier::new(EMBEDDED_ROOT_CA_DER.to_vec()) } /// Get mock collateral for verifying generated mock quotes @@ -256,9 +253,6 @@ mod tests { assert_eq!(quote.header.tee_type, TEE_TYPE_TDX); let collateral = mock_collateral(); - let tcb_info: TcbInfo = serde_json::from_str(&collateral.tcb_info).unwrap(); - assert_eq!(hex::encode_upper(quote.fmspc().unwrap()), tcb_info.fmspc); - assert_eq!(quote.ca().unwrap(), "processor"); let verifier = mock_dcap_verifier(); let verified = verifier.verify("e_bytes, &collateral, FIXTURE_TIME).unwrap(); @@ -296,7 +290,6 @@ mod tests { let quote_bytes = generate_mock_tdx_quote([0xEF; 64]).unwrap(); let quote = Quote::parse("e_bytes).unwrap(); - assert_eq!(hex::encode_upper(quote.fmspc().unwrap()), tcb_info.fmspc); assert_eq!(quote.header.pce_svn, tcb_info.tcb_levels[0].tcb.pce_svn); verifier.verify("e_bytes, &collateral, FIXTURE_TIME).unwrap(); diff --git a/crates/mock-tdx/src/main.rs b/crates/mock-tdx/src/main.rs index b730bfa..9d2c5f1 100644 --- a/crates/mock-tdx/src/main.rs +++ b/crates/mock-tdx/src/main.rs @@ -6,7 +6,7 @@ use std::{ use dcap_qvl::{ QuoteCollateralV3, intel::{PckExtension, parse_pck_extension}, - tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus}, + tcb_info::{Tcb, TcbComponents, TcbInfo, TcbLevel, TcbStatus, TdxModule}, }; use p256::{ SecretKey, @@ -277,6 +277,12 @@ fn mock_tcb_info( tcb_status: TcbStatus::UpToDate, advisory_ids: Vec::new(), }], + tdx_module: Some(TdxModule { + mrsigner: hex::encode_upper([0u8; 48]), + attributes: hex::encode_upper([0u8; 8]), + attributes_mask: hex::encode_upper([0u8; 8]), + }), + tdx_module_identities: Vec::new(), } } diff --git a/crates/pccs/Cargo.toml b/crates/pccs/Cargo.toml index 4a2e7d0..a209d41 100644 --- a/crates/pccs/Cargo.toml +++ b/crates/pccs/Cargo.toml @@ -18,6 +18,8 @@ hex = "0.4.3" anyhow = "1.0.100" reqwest = { workspace = true } x509-parser = "0.18.0" +futures-executor = "0.3.31" +ureq = "2.12.1" [dev-dependencies] rcgen = "0.14.5" diff --git a/crates/pccs/README.md b/crates/pccs/README.md index e1a3cfd..2848d6d 100644 --- a/crates/pccs/README.md +++ b/crates/pccs/README.md @@ -3,8 +3,8 @@ An internal Provisioning Certificate Caching Service implementation for DCAP collateral fetching and caching. -This crate is used by attestation verification code that needs Intel TDX/SGX -collateral such as TCB info, QE identity, and certificate revocation lists. +This crate is used by attestation code that needs Intel TDX/SGX collateral such +as TCB info, QE identity, and certificate revocation lists. It can: @@ -23,6 +23,6 @@ For Intel's terminology and architecture, see the Intel documentation for the This crate expects to be used from within a Tokio runtime. -The above applies even when calling synchronous-looking APIs such as -`get_collateral_sync()` because cache miss repair, proactive refresh, and -startup pre-warm are all driven by Tokio background tasks. +The above applies to startup pre-warm and proactive refresh. Synchronous cache +miss paths can fetch collateral directly, but they should be kept off hot +request paths unless the caller has a strict timeout. diff --git a/crates/pccs/src/lib.rs b/crates/pccs/src/lib.rs index 774443b..cb26452 100644 --- a/crates/pccs/src/lib.rs +++ b/crates/pccs/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, sync::{ Arc, RwLock, @@ -9,7 +9,14 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use dcap_qvl::{QuoteCollateralV3, collateral::get_collateral_for_fmspc, tcb_info::TcbInfo}; +use dcap_qvl::{ + QuoteCollateralV3, + collateral::CollateralClient, + configs::DefaultConfig, + http::{HttpClient, HttpResponse}, + tcb_info::TcbInfo, +}; +use futures_executor::block_on; use thiserror::Error; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokio::{ @@ -29,6 +36,9 @@ const REFRESH_RETRY_SECS: u64 = 60; /// How many collateral fetches to perform concurrently during initial /// pre-warm const STARTUP_PREWARM_CONCURRENCY: usize = 8; +/// Keep this short: sync collateral fetches may run on TLS handshake paths, +/// so a long timeout can stall handshake progress. +const SYNC_COLLATERAL_FETCH_TIMEOUT_SECS: u64 = 2; /// PCCS collateral cache with proactive background refresh #[derive(Clone)] @@ -37,8 +47,6 @@ pub struct Pccs { url: String, /// The internal cache cache: Arc>>, - /// Dedupes one-shot background refreshes for cache misses - pending_refreshes: Arc>>, /// The state of the initial pre-warm fetch prewarm_stats: Arc, /// Completion signal for startup pre-warm, shared across all clones @@ -84,7 +92,6 @@ impl Pccs { Self { url, cache: RwLock::new(HashMap::new()).into(), - pending_refreshes: RwLock::new(HashSet::new()).into(), prewarm_stats: Arc::new(PrewarmStats::default()), prewarm_outcome_tx: None, } @@ -151,15 +158,14 @@ impl Pccs { upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); } - self.ensure_refresh_task(&cache_key).await; + self.ensure_refresh_task(&cache_key); Ok((collateral, true)) } /// A synchronous method to get collateral from the cache. /// /// If the requested collateral is not present in the cache, this will - /// return an error rather than waiting to fetch it. But it does - /// begin fetching it in a background task. + /// fetch it synchronously and cache it before returning. /// /// If the collateral is out of date, this will log a warning and return /// it anyway on a best-effort basis. @@ -171,32 +177,68 @@ impl Pccs { ) -> Result { let now = i64::try_from(now).map_err(|_| PccsError::TimeStampExceedsI64)?; let cache_key = PccsInput::new(fmspc.clone(), ca); - let cache = self.cache.read().map_err(|_| PccsError::CachePoisoned)?; - if let Some(entry) = cache.get(&cache_key) { - if now >= entry.next_update { - let collateral = entry.collateral.clone(); - tracing::warn!( - fmspc, - next_update = entry.next_update, - now, - "Cached collateral expired" - ); - drop(cache); + { + let cache = self.cache.read().map_err(|_| PccsError::CachePoisoned)?; + if let Some(entry) = cache.get(&cache_key) { + if now >= entry.next_update { + let collateral = entry.collateral.clone(); + tracing::warn!( + fmspc, + next_update = entry.next_update, + now, + "Cached collateral expired" + ); + drop(cache); - // Start a background task to renew - let pccs = self.clone(); - tokio::spawn(async move { - pccs.ensure_refresh_task(&cache_key).await; - }); + // Start a background task to renew + let pccs = self.clone(); + tokio::spawn(async move { + pccs.ensure_refresh_task(&cache_key); + }); - return Ok(collateral); + return Ok(collateral); + } + return Ok(entry.collateral.clone()); } - Ok(entry.collateral.clone()) - } else { - drop(cache); - self.spawn_background_refresh_for_cache_miss(cache_key.clone()); - Err(PccsError::NoCollateralForFmspc(format!("{cache_key:?}"))) } + + let collateral = fetch_collateral_sync(&self.url, fmspc.clone(), ca)?; + let next_update = extract_next_update(&collateral, now)?; + + { + let mut cache = self.cache.write().map_err(|_| PccsError::CachePoisoned)?; + if let Some(existing) = cache.get(&cache_key) && + now < existing.next_update + { + return Ok(existing.collateral.clone()); + } + + upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); + } + self.ensure_refresh_task(&cache_key); + Ok(collateral) + } + + /// Fetches collateral for a quote using this PCCS/PCS URL. + pub async fn get_collateral_for_quote( + &self, + quote: &[u8], + ) -> Result { + CollateralClient::with_default_http(self.url.clone())? + .fetch(quote) + .await + .map_err(Into::into) + } + + /// Synchronously fetches collateral for a quote using this PCCS/PCS + /// URL. + pub fn get_collateral_for_quote_sync( + &self, + quote: &[u8], + ) -> Result { + let http = UreqHttp::new(); + let client = CollateralClient::::new(http, self.url.clone()); + block_on(async move { client.fetch(quote).await }).map_err(Into::into) } /// Fetches fresh collateral, overwrites cache, and ensures proactive @@ -215,13 +257,13 @@ impl Pccs { let mut cache = self.cache.write().map_err(|_| PccsError::CachePoisoned)?; upsert_cache_entry(&mut cache, cache_key.clone(), collateral.clone(), next_update); } - self.ensure_refresh_task(&cache_key).await; + self.ensure_refresh_task(&cache_key); 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) { + fn ensure_refresh_task(&self, cache_key: &PccsInput) { let Ok(mut cache) = self.cache.write() else { tracing::warn!("PCCS cache lock poisoned, cannot ensure refresh task"); return; @@ -241,46 +283,6 @@ impl Pccs { })); } - /// Starts a one-shot background fetch to populate a missing cache entry - fn spawn_background_refresh_for_cache_miss(&self, cache_key: PccsInput) { - { - let Ok(mut pending_refreshes) = self.pending_refreshes.write() else { - tracing::warn!("PCCS pending-refresh lock poisoned, cannot start sync refresh"); - return; - }; - if !pending_refreshes.insert(cache_key.clone()) { - return; - } - } - - let pccs = self.clone(); - tokio::spawn(async move { - let result = pccs - .refresh_collateral( - cache_key.fmspc.clone(), - ca_as_static(&cache_key.ca).expect("unsupported CA in pending refresh"), - ) - .await; - - if let Err(err) = result { - tracing::warn!( - fmspc = cache_key.fmspc, - ca = cache_key.ca, - error = %err, - "Sync-triggered PCCS cache repair failed" - ); - } - - // Always clear the dedupe marker so a later sync miss can - // retry if this repair attempt failed. - if let Ok(mut pending_refreshes) = pccs.pending_refreshes.write() { - pending_refreshes.remove(&cache_key); - } else { - tracing::warn!("PCCS pending-refresh lock poisoned during cleanup"); - } - }); - } - /// Pre-provisions TDX collateral for discovered FMSPC values to reduce /// hot-path fetches async fn startup_prewarm_all_tdx(&self) -> PrewarmOutcome { @@ -424,11 +426,57 @@ async fn fetch_collateral( fmspc: String, ca: &'static str, ) -> Result { - get_collateral_for_fmspc( - url, fmspc, ca, false, // Indicates not SGX - ) - .await - .map_err(Into::into) + CollateralClient::with_default_http(url.to_string())? + .fetch_for_fmspc_without_pck_chain(&fmspc, ca, false) + .await + .map_err(Into::into) +} + +struct UreqHttp { + agent: ureq::Agent, +} + +impl UreqHttp { + fn new() -> Self { + let agent = ureq::AgentBuilder::new() + .timeout(Duration::from_secs(SYNC_COLLATERAL_FETCH_TIMEOUT_SECS)) + .build(); + Self { agent } + } +} + +impl Clone for UreqHttp { + fn clone(&self) -> Self { + Self::new() + } +} + +impl HttpClient for UreqHttp { + async fn get(&self, url: &str) -> anyhow::Result { + let response = self.agent.get(url).call().map_err(|err| anyhow::anyhow!(err))?; + let status = response.status(); + let headers = response + .headers_names() + .into_iter() + .filter_map(|name| response.header(&name).map(|value| (name, value.to_string()))) + .collect(); + let mut body = Vec::new(); + let mut reader = response.into_reader(); + use std::io::Read; + reader.read_to_end(&mut body)?; + Ok(HttpResponse { status, headers, body }) + } +} + +fn fetch_collateral_sync( + url: &str, + fmspc: String, + ca: &'static str, +) -> Result { + let http = UreqHttp::new(); + let client = CollateralClient::::new(http, url.to_string()); + block_on(async move { client.fetch_for_fmspc_without_pck_chain(&fmspc, ca, false).await }) + .map_err(Into::into) } /// Extracts the earliest next update timestamp from collateral metadata @@ -718,8 +766,6 @@ pub enum PccsError { TimeStampExceedsI64, #[error("PCCS cache lock poisoned")] CachePoisoned, - #[error("No collateral in cache for FMSPC {0}")] - NoCollateralForFmspc(String), } #[cfg(test)] @@ -898,8 +944,8 @@ mod tests { assert!(matches!(ready_result, Err(PccsError::PrewarmDisabled))); } - #[tokio::test] - async fn test_get_collateral_sync_repairs_cache_miss_in_background() { + #[tokio::test(flavor = "multi_thread")] + async fn test_get_collateral_sync_repairs_cache_miss_inline() { let fmspc = mock_tdx_fmspc(); let mock = spawn_mock_pcs_server(MockPcsConfig { include_fmspcs_listing: false, @@ -914,18 +960,13 @@ mod tests { let pccs = Pccs::new_without_prewarm(Some(mock.base_url.clone())); let now = unix_now().unwrap() as u64; - let err = pccs.get_collateral_sync(fmspc.clone(), "processor", now); - assert!(matches!(err, Err(PccsError::NoCollateralForFmspc(_)))); - - for _ in 0..50 { - if pccs.get_collateral_sync(fmspc.clone(), "processor", now).is_ok() { - break; - } - tokio::time::sleep(Duration::from_millis(20)).await; - } + let collateral = pccs.get_collateral_sync(fmspc.clone(), "processor", now); + assert!(collateral.is_ok(), "expected sync miss fetch to populate cache: {collateral:?}"); + assert_eq!(mock.tcb_call_count(), 1); + assert_eq!(mock.qe_call_count(), 1); let collateral = pccs.get_collateral_sync(fmspc, "processor", now); - assert!(collateral.is_ok(), "expected sync miss repair to populate cache"); + assert!(collateral.is_ok(), "expected sync hit to use cache"); assert_eq!(mock.tcb_call_count(), 1); assert_eq!(mock.qe_call_count(), 1); }