From fb422292002b140cd6756806f82344cd76f17df7 Mon Sep 17 00:00:00 2001 From: Diego Cavalcante Date: Sun, 2 Aug 2026 01:10:02 -0300 Subject: [PATCH] fix(net): honor SSL_CERT_FILE / SSL_CERT_DIR in the stealth client The #525 fix landed on the reqwest path (`client.rs`: `configured_root_paths()` -> `add_root_certificate`) but did not cover `wreq_client.rs`. The stealth client builds its `wreq::Client` without a certificate store, so those variables are silently ignored whenever `--stealth` is on. The consequence is inverted: the transport with the best TLS fingerprint is the only one that cannot reach a host behind a private or incomplete CA chain -- precisely the environments #525 described. Same binary on current main, same private-CA server, only the flag differs: SSL_CERT_FILE=ca.pem obscura fetch https://127.0.0.1:8443/ -> ok SSL_CERT_FILE=ca.pem obscura --stealth fetch https://127.0.0.1:8443/ -> fails `wreq` already exposes the hook -- `ClientBuilder::tls_cert_store(CertStore)`, and `CertStore::builder().set_default_paths()` reads both variables. It is simply never called. Two constraints, both of which fail silently if got wrong: * `tls_cert_store`, not `tls_options`. `emulation()` is documented as "This will overwrite the existing configuration" and internally calls `self.tls_options(emulation.tls_options)`; setting TLS options here would discard the Chrome fingerprint, which is the whole point of this client. `tls_cert_store` is a separate config field and composes with emulation. * Opt-in only. Supplying a store REPLACES the webpki roots (see `set_cert_store` in `tls/conn/ext.rs`), it does not add to them. Applying it unconditionally would break ordinary sites whenever the bundle is incomplete. With neither variable set, behaviour is byte-for-byte unchanged. Verified: with the variable, a private-CA host resolves under --stealth; without it, the same host still fails; with a deliberately EMPTY bundle, example.com fails -- which is what proves the store is actually applied rather than the bundle merely happening to be right. Refs #538, #525 --- crates/obscura-net/src/wreq_client.rs | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/obscura-net/src/wreq_client.rs b/crates/obscura-net/src/wreq_client.rs index bf56a9a..397e1c1 100644 --- a/crates/obscura-net/src/wreq_client.rs +++ b/crates/obscura-net/src/wreq_client.rs @@ -57,6 +57,39 @@ impl StealthHttpClient { .timeout(Duration::from_secs(30)) .redirect(wreq::redirect::Policy::none()); + // Honor SSL_CERT_FILE / SSL_CERT_DIR in the stealth client too. + // + // `client.rs` (the reqwest path) already reads these via `configured_root_paths()` and + // feeds `add_root_certificate`, so a private CA works there. This client did not, which + // made the *better-fingerprinted* transport the only one unable to reach hosts behind a + // private/national CA (measured against a Brazilian government portal whose leaf is + // issued by an ICP-Brasil intermediate: `--stealth` failed with CERTIFICATE_VERIFY_FAILED + // while the reqwest path, with SSL_CERT_FILE set, completed the handshake). + // + // Two deliberate constraints: + // + // 1. `tls_cert_store` is used, NOT `tls_options`. `emulation()` overwrites `tls_options` + // wholesale ("This will overwrite the existing configuration"), so setting TLS options + // here would silently discard the Chrome fingerprint — the whole point of this client. + // `tls_cert_store` is a separate field on the config and composes with emulation. + // + // 2. Opt-in only. Supplying a store REPLACES the webpki roots (see `set_cert_store` in + // `tls/conn/ext.rs`), it does not add to them. Applying it unconditionally would break + // every ordinary site whenever the bundle is incomplete. With neither variable set, + // behaviour is byte-for-byte what it was before. + if std::env::var_os("SSL_CERT_FILE").is_some() + || std::env::var_os("SSL_CERT_DIR").is_some() + { + match wreq::tls::trust::CertStore::builder().set_default_paths().build() { + Ok(store) => builder = builder.tls_cert_store(store), + Err(error) => tracing::warn!( + %error, + "SSL_CERT_FILE/SSL_CERT_DIR set but the certificate store failed to build; \ + continuing with the default roots" + ), + } + } + if let Some(proxy) = proxy_url { if let Ok(p) = wreq::Proxy::all(proxy) { builder = builder.proxy(p);