From 604edc9f83243a5db7554d8559f00c75144d6a46 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 3 May 2026 07:35:23 -0500 Subject: [PATCH] feat(tls): honor SSL_CERT_FILE for corporate-CA / MITM proxies (#418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corporate users behind TLS-inspecting proxies (Zscaler, Netskope, Palo Alto, in-house mitmproxy fleets) need to add the proxy's intermediate CA to the trusted-roots set so the deepseek client doesn't fail with `unable to get local issuer certificate`. The reqwest builder already trusts the platform's system store via native-tls. This adds opt-in support for the conventional `SSL_CERT_FILE` env var so users can point at their own bundle: * New `add_extra_root_certs(builder, path)` helper reads the file, tries `Certificate::from_pem_bundle` (covers single-cert files too), falls back to `from_der` for binary cert files. * Wired into `build_http_client` when `SSL_CERT_FILE` is set and non-empty. Failures log a warning via the existing `logging::warn` channel and return the builder unchanged — the existing system trust still applies, so a malformed env var degrades gracefully instead of bricking the launch. * Each successful load logs `info` with the cert count so operators can confirm their bundle was picked up. Documented in `docs/CONFIGURATION.md`'s environment-variables list alongside the existing TLS-related notes. No new dependency — reqwest's `native-tls` feature already exposes `Certificate::from_pem_bundle` / `from_der`. --- CHANGELOG.md | 8 ++++++ crates/tui/src/client.rs | 60 ++++++++++++++++++++++++++++++++++++++++ docs/CONFIGURATION.md | 5 ++++ 3 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c084a6f..97339e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (e.g. user `"never"` → project `"on-request"` is allowed even though it loosens) stay v0.8.9 follow-up because they need a richer ordering check. +- **`SSL_CERT_FILE` honored in the HTTPS client** (#418) — corporate + proxy / TLS-inspecting MITM users can now point at their custom + CA bundle and have it added alongside the platform's system + trust store. Tries PEM-bundle parsing first (covers single-cert + files too), falls back to DER. Failures log a warning and + continue — the existing system roots still apply, so a + malformed env var won't bring down the launch. Documented in + `docs/CONFIGURATION.md`. - **Sub-agent role taxonomy expansion** (#404) — adds `Implementer` ("land this change with the minimum surrounding edit") and `Verifier` ("run the test suite, report pass/fail with evidence") diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index ea11996b..8fad4ea7 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -412,6 +412,53 @@ fn force_http1_from_env() -> bool { .is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on")) } +/// Read `SSL_CERT_FILE` and add its contents as extra root +/// certificates on the reqwest builder (#418). Tries the PEM-bundle +/// parser first (covers single-cert files too), then falls back to +/// DER. All failures log a warning and return the builder unchanged +/// so a malformed env var degrades gracefully. +fn add_extra_root_certs( + mut builder: reqwest::ClientBuilder, + cert_path: &str, +) -> reqwest::ClientBuilder { + let bytes = match std::fs::read(cert_path) { + Ok(b) => b, + Err(err) => { + logging::warn(format!( + "SSL_CERT_FILE={cert_path} could not be read: {err}" + )); + return builder; + } + }; + + // PEM bundle handles both single-cert and multi-cert files; try + // it first since `BEGIN CERTIFICATE` framing is the common case. + if let Ok(certs) = reqwest::Certificate::from_pem_bundle(&bytes) { + let added = certs.len(); + for cert in certs { + builder = builder.add_root_certificate(cert); + } + logging::info(format!( + "SSL_CERT_FILE={cert_path} loaded ({added} cert(s))" + )); + return builder; + } + + // Single-cert DER fallback. + match reqwest::Certificate::from_der(&bytes) { + Ok(cert) => { + builder = builder.add_root_certificate(cert); + logging::info(format!("SSL_CERT_FILE={cert_path} loaded (1 DER cert)")); + } + Err(err) => { + logging::warn(format!( + "SSL_CERT_FILE={cert_path} could not be parsed as PEM bundle or DER: {err}" + )); + } + } + builder +} + impl DeepSeekClient { /// Create a DeepSeek client from CLI configuration. pub fn new(config: &Config) -> Result { @@ -473,6 +520,19 @@ impl DeepSeekClient { logging::info("DEEPSEEK_FORCE_HTTP1=1 — pinning HTTP client to HTTP/1.1"); builder = builder.http1_only(); } + // #418: corporate-proxy / MITM-inspector CA support. When + // `SSL_CERT_FILE` is set, load the cert(s) it points at and + // add them as trusted roots alongside the platform's system + // store. We try PEM bundle first (the common case for + // multi-cert files), then fall back to single-cert PEM, then + // DER. Failures log a warning and continue — the existing + // system roots still apply, so a malformed env var won't + // bring down the launch. + if let Ok(cert_path) = std::env::var("SSL_CERT_FILE") + && !cert_path.is_empty() + { + builder = add_extra_root_certs(builder, &cert_path); + } builder.build().map_err(Into::into) } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index bf174b00..b3e3c0ff 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -154,6 +154,11 @@ These override config values: - `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and `fancy_animations = false` at startup, regardless of the saved settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)). +- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users + point this at a PEM bundle (or single DER cert) and the cert(s) + get added alongside the platform's system trust store. Failures + log a warning and continue — the existing system roots still + apply. ### Instruction sources (`instructions = [...]`, #454)