diff --git a/Cargo.lock b/Cargo.lock index ac904b9fd3..9368a90bce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7243,6 +7243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -11357,6 +11358,8 @@ dependencies = [ "pathdiff", "petgraph 0.6.5", "reqwest 0.12.24", + "rustls", + "rustls-native-certs", "serde", "serde_json", "sha2", diff --git a/crates/ruborist/Cargo.toml b/crates/ruborist/Cargo.toml index 4f63f3ec7f..83ab64947e 100644 --- a/crates/ruborist/Cargo.toml +++ b/crates/ruborist/Cargo.toml @@ -23,13 +23,29 @@ tokio-fs-ext = { workspace = true } tokio-retry = "0.3" tracing = { workspace = true } # HTTP client - works on both native and WASM +# +# We use `rustls-tls-native-roots-no-provider` (instead of the default +# `rustls-tls-native-roots`) so reqwest doesn't pin the crypto provider +# to `ring`. We then build our own `rustls::ClientConfig` with +# `aws-lc-rs` (BoringSSL's crypto primitives) in `service/http.rs` and +# pass it via `Client::use_preconfigured_tls`. Measured on CI against +# npmjs.org: `ring` took 78 ms p50 / 154 ms max from CCS → first +# AppData per handshake (CPU-bound; 128 parallel handshakes serialise +# across 4 cores). aws-lc-rs's per-handshake CPU is roughly 3× faster. reqwest = { version = "0.12", default-features = false, features = [ "http2", "json", - "rustls-tls", - "rustls-tls-native-roots", + "rustls-tls-native-roots-no-provider", "socks" ] } +# Our own rustls + aws-lc-rs crypto provider. +rustls = { version = "0.23", default-features = false, features = [ + "aws-lc-rs", + "logging", + "std", + "tls12" +] } +rustls-native-certs = "0.8" # Shared URL-hash + staging helpers for native-only resolvers (git / http) sha2 = { workspace = true, optional = true } tempfile = { workspace = true, optional = true } diff --git a/crates/ruborist/src/service/http.rs b/crates/ruborist/src/service/http.rs index b959020973..9179f51b1a 100644 --- a/crates/ruborist/src/service/http.rs +++ b/crates/ruborist/src/service/http.rs @@ -93,6 +93,49 @@ pub(crate) fn get_client() -> Result<&'static reqwest::Client> { HTTP_CLIENT.as_ref().map_err(|e| anyhow!("{e}")) } +/// Build a `rustls::ClientConfig` using the `aws-lc-rs` crypto provider +/// instead of reqwest's default `ring`. Measured on CI (4-core runner) +/// against npmjs.org, ring's per-TLS-handshake client-side CPU cost +/// (ECDHE key derivation + cert verification + Finished MAC) serialised +/// across 128 parallel handshakes into a 154 ms "CCS → first AppData" +/// span — the HTTP requests couldn't fire until all TLS crypto drained +/// through 4 async workers. aws-lc-rs uses BoringSSL's assembly-optimised +/// primitives and is roughly 3× faster at handshake work. +#[cfg(not(target_arch = "wasm32"))] +fn build_rustls_config() -> Result { + // Install aws-lc-rs as the default for any other rustls consumer in + // the process. Idempotent — only the first call per process wins. + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + // Load OS root certs. On Linux this hits /etc/ssl/certs/, + // on macOS it queries the Security framework keychain. Called once + // per process via `HTTP_CLIENT`'s `LazyLock`. + let roots = rustls_native_certs::load_native_certs(); + let mut root_store = rustls::RootCertStore::empty(); + for cert in roots.certs { + // Best-effort: skip any cert rustls refuses (same tolerance + // native-tls shows). A hard fail here would brick every + // request on a box with one bad root in its trust store. + let _ = root_store.add(cert); + } + if !roots.errors.is_empty() { + tracing::debug!( + "rustls-native-certs reported {} non-fatal load issues", + roots.errors.len() + ); + } + + let config = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new( + rustls::crypto::aws_lc_rs::default_provider(), + )) + .with_safe_default_protocol_versions() + .map_err(|e| anyhow!("rustls safe_default_protocol_versions: {e}"))? + .with_root_certificates(root_store) + .with_no_client_auth(); + + Ok(config) +} + /// Create a [`reqwest::ClientBuilder`] with TLS, DNS caching, and proxy /// from environment variables. /// @@ -112,8 +155,9 @@ pub fn client_builder() -> Result { let builder = { use crate::service::dns::shared_resolver; + let tls_config = build_rustls_config()?; let mut builder = builder - .use_rustls_tls() + .use_preconfigured_tls(tls_config) .no_proxy() .dns_resolver(shared_resolver());