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
This commit is contained in:
Diego Cavalcante committed 2026-08-02 01:10:02 -03:00
1 parent eab2a5eea1
commit fb42229200
1 file changed
+33
+33
View File
@@ -57,6 +57,39 @@ impl StealthHttpClient {
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.redirect(wreq::redirect::Policy::none()); .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 Some(proxy) = proxy_url {
if let Ok(p) = wreq::Proxy::all(proxy) { if let Ok(p) = wreq::Proxy::all(proxy) {
builder = builder.proxy(p); builder = builder.proxy(p);