feat(net): support a custom corporate root CA for all outbound connections - #134
Merged
Merged
Conversation
Contributor
Coverage
Gate: passed - no coverage regression (epsilon 0.1 pp). |
pmaxhogan
force-pushed
the
feat/custom-root-ca
branch
from
July 20, 2026 17:59
af10e25 to
a49dfa1
Compare
pmaxhogan
enabled auto-merge (squash)
July 20, 2026 18:24
…tions Add an optional GlobalSettings.custom_root_ca_path (a PEM bundle) that is added to the system trust store for every outbound HTTP client in the workspace. A new leaf crate driven-tls hosts the single shared helper apply_custom_ca (reqwest 0.12) plus load/validate primitives; it sits below driven-net, driven-drive and src-tauri with no dependency cycle. Trust semantics: additive (add_root_certificate on top of the native roots - source-verified in reqwest 0.12 and 0.13), no verification bypass, and a missing/unreadable/unparseable/empty PEM fails the client build. Threaded into all 9 reqwest build sites plus the tauri-plugin-updater download client (reqwest 0.13) via configure_client. Settings UI: a CA path input with validate-on-save (cert count / parse error) via a new validate_custom_ca IPC command. Part of #34 (item 14, Corporate CA pinning) - references the V2 backlog epic without closing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
- normalize_ca_path: use Option::filter (clippy 1.97 manual_filter is deny). - Add fail-closed / parse coverage in the counted crates (driven-net, driven-drive, driven-cli) plus updater + telemetry sink tests so the regression-vs-main coverage gate holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
Bring the regression-vs-main coverage delta to ~neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
pmaxhogan
force-pushed
the
feat/custom-root-ca
branch
from
July 20, 2026 19:24
a49dfa1 to
ce6bf3c
Compare
…base Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
This was referenced Jul 20, 2026
pmaxhogan
added a commit
that referenced
this pull request
Jul 20, 2026
…145) Adds proxy support to every outbound HTTP client, completing the corporate-network story started by the custom-CA work (#134) through the same `driven-tls` seam. ## Modes (KV-stored `global` settings, serde-default - NO migration) - **system** (default): unchanged - reqwest env-proxy pickup (DESIGN 5.8.7). - **none**: explicit `.no_proxy()` everywhere - bypasses env proxies too. - **manual**: one proxy URL (`http://`, `https://`, `socks5://`, `socks5h://` - socks5h resolves DNS proxy-side); reqwest `socks` feature enabled on both workspace reqwest 0.12 and the updater's reqwest 0.13. - **pac**: PAC file URL or local path, compiled with an embedded pure-Rust JS engine (boa 0.20), evaluated per-URL via `Proxy::custom` with a 256-entry per-HOST LRU. Standard helpers implemented (isPlainHostName, dnsDomainIs, localHostOrDomainIs, dnsDomainLevels, shExpMatch, dnsResolve, isResolvable, myIpAddress, isInNet); date/time predicates (weekdayRange/dateRange/timeRange) are defined-but-false stubs. ## Semantics + documented caveats - CONFIG failures fail closed at settings-save AND client build (bad URL, unfetchable/uncompilable PAC): clients are never built silently unproxied. A corrupt stored mode string degrades to `system` (the historical default). - RUNTIME PAC eval errors (e.g. a DNS failure mid-eval) log a warning and go direct for that request - browser-standard PAC failure behavior. - The PAC realm is pure ECMAScript (no fetch/fs/process); DNS via the helpers only. There is deliberately NO JS execution watchdog yet (admin-configured input; LRU bounds eval count) - future hardening if untrusted PAC ever becomes possible. DNS helpers use OS-resolver timeouts. - Cache keys on host only: a PAC branching on scheme/path gets the per-host decision (endorsed pragmatic subset). - Updater: PAC evaluates per-URL there too via a version-neutral engine handle; CA certs + proxy fold into ONE configure_client closure (the plugin keeps only the last). - CLI: env proxy + `DRIVEN_PROXY_URL` (manual) only; PAC unsupported there. - deny.toml: `paste` (RUSTSEC-2024-0436, unmaintained build-time proc-macro via boa) ignored with justification; `lru` at 0.16.3+ (clears RUSTSEC-2026-0002). - New IPC `validate_proxy` + Settings UI proxy section (mode select, conditional inputs, inline validation), localized, vitest mount coverage. Refs #34 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
pmaxhogan
added a commit
that referenced
this pull request
Jul 24, 2026
🤖 I have created a release *beep* *boop* --- ## [2.1.0](v2.0.1...v2.1.0) (2026-07-24) ### Features * **core:** adaptive upload parallelism with throughput probe and disk-saturation gate ([#143](#143)) ([8ecced6](8ecced6)) * **core:** filesystem timestamp-granularity probe with ctime fallback and per-directory gitignore cascade ([#141](#141)) ([344262c](344262c)) * **drive:** support Google Shared Drive destinations end-to-end ([#142](#142)) ([d9c3161](d9c3161)) * **net:** native OS reachability backends with automatic fallback ([#138](#138)) ([319e85f](319e85f)) * **net:** SOCKS5 and PAC proxy support for all outbound connections ([#145](#145)) ([2f0b7d1](2f0b7d1)) * **net:** support a custom corporate root CA for all outbound connections ([#134](#134)) ([929e93d](929e93d)) * per-source toggle to back up OneDrive cloud-only placeholder files ([#133](#133)) ([6863ea3](6863ea3)) * **telemetry:** capture latency percentiles and add rollup query endpoint ([#132](#132)) ([4e9fde6](4e9fde6)) * **telemetry:** preview exactly what a telemetry ping sends ([#139](#139)) ([95fbd9a](95fbd9a)) ### Bug Fixes * **core:** commit file_state for a create that skipped post-upload so the next scan updates instead of re-creating ([#146](#146)) ([f5230d1](f5230d1)) * **deps:** bump tauri-winrt-notification to drop vulnerable quick-xml (closes [#89](#89)) ([#129](#129)) ([232fd8f](232fd8f)) * **telemetry:** exclude pre-schema rows from latency rollup ([#137](#137)) ([1ae6220](1ae6220)) * **ui:** add cursor pointer to buttons and link-buttons ([#136](#136)) ([dbd4809](dbd4809)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / why
Corporate and TLS-inspection networks terminate TLS with a private root CA that
is not in the public trust store, so every outbound HTTPS connection Driven
makes fails behind them. This adds an optional
custom_root_ca_pathglobalsetting (a PEM file that may hold multiple certs) whose certificates are added
to the system trust store for all outbound HTTP clients. Empty/unset = system
trust only (unchanged behaviour). Implements item 14 ("Corporate CA pinning") of
the V2 backlog, #34 (which DESIGN s5.8.7 had deferred).
Trust semantics (the security boundary)
of the OS/enterprise native roots via
add_root_certificate; it never replacesthem. Source-verified in both reqwest versions in the tree: 0.12 populates one
RootCertStorewith native + custom roots (async_impl/client.rs:687-714);0.13 (the updater plugin) uses
rustls_platform_verifier::Verifier::new_with_extra_roots(extra, platform)(
async_impl/client.rs:756-769). Because it is purely additive, failing toapply the CA can only make a connection stricter, never weaker - so the
security boundary is entirely "additive + no verification bypass", nothing else.
danger_accept_invalid_certs, notls_built_in_root_certs(false)/tls_certs_only, no hostname-verificationdisable. Grep-verified; the one helper crate (
driven-tls) documents this as alocked invariant at the call site.
unparseable / zero certs). This is a UX/correctness choice, not the security
boundary (additive trust cannot fail open): we would rather surface a broken
corporate-CA config than silently make requests the proxy will reject anyway.
Save-time validation (below) stops a broken path being persisted in the first
place. Note the best-effort paths (telemetry ping, Google userinfo, and the
updater when AppState is unavailable) deliberately proceed without on a load
error / missing state rather than hard-fail - they cannot weaken trust, so this
is a functionality edge case, not a fail-open.
The one shared helper + the 10 outbound sites
New leaf crate
driven-tls(depends only onreqwest+thiserror) hostsapply_custom_ca(builder, &CustomCaConfig)plusload_certificates/validate_ca_file. It is a leaf becausedriven-driveis itself a leaf thatdriven-coredepends on, so no pre-existing crate could host a helper reachableby all HTTP-using crates without a cycle.
The 9 reqwest build sites from the seams audit, each threaded via constructor/param
following that crate's existing config style:
driven-net)ReqwestBackend::new(ca)stores it; rebuilt on pool teardowndriven-net)driven-drivetoken_store)from_stored_refresh_token(.., &ca)driven-driveoauth)run_pkce_loopback_flow(.., &ca)driven-drive)with_default_clients(tokens, &ca)/new(.., &ca)driven-drive)src-tauri)HttpTelemetrySink::new(ca), resolved once at bootsrc-tauriaccounts)src-taurisettings)Plus a 10th path the seams list did not cover and the advisor flagged as the
one that matters most in a TLS-inspecting environment: tauri-plugin-updater's
own download client (the signed manifest + binary fetch). The plugin is on
reqwest 0.13 (workspace +
driven-tlsare 0.12), so it is wired through theplugin's
configure_clienthook using an aliased 0.13reqwest_updaterdep(already in the tree via the plugin) - the fallible PEM parse happens first in
run_check(fail-closed), then the pre-parsed certs are added inside theinfallible closure.
A hidden-constructor grep (
Client::new()/reqwest::get/Client::default())found no other outbound clients.
Restart vs hot-reload
Applied when each client is built. Long-lived clients (Drive metadata/stream,
OAuth refresh, network probes, telemetry sink) are built once at account assembly
/ boot, so a CA change takes effect when the account is next assembled - in
practice an app restart (the live
reconfigure_allpath only re-appliespacer/gates, it does not rebuild these clients). The per-operation clients (OAuth
consent, userinfo, releases check, updater) rebuild each call and pick up the
current setting on next use. The UI caption states "applies to new connections
after an app restart".
Settings UI + validation
GlobalSettings.custom_root_ca_path: Option<PathBuf>(storage#[serde(default)]for back-compat), DTO + patch (double-option) + TS mirrors.
validate_custom_ca(path)IPC command parses the PEM and returns the certcount or a parse error, for inline save-time feedback.
update_settingsalsovalidates on save, so a broken path cannot be persisted; a blank path clears
the setting.
Reviewer note:
validate_custom_ca(and the build-time re-read) read a plain,user-typed persisted path rather than going through the app's dialog-token
confinement model. This is intentional and low-risk: a CA path is an inherently
user-supplied setting, and the command returns only a cert count / parse status,
never file contents.
Tests
driven-tls: unit tests forapply_custom_ca/validate_ca_file- validsingle cert, multi-cert bundle, garbage (->
NoCertificates), missing (->Read), corrupt-body PEM, and theNoneno-op. (Additivity itself rests on thereqwest source read above - it needs a live handshake to test, so it is
asserted by source + the locked invariant comment, not a unit test.)
driven-drive: fail-closed wiring test (bad CA -> refresh-client build fails).src-tauri:normalize_ca_path,storage::Globalserde back-compat (pre-fieldblob ->
None), and thevalidate_custom_cacommand (cert count / blank /garbage).
no save + error, clear -> null patch).
cargo fmt --check,clippy --workspace --all-targets -D warnings,per-crate
cargo test(driven-tls/net/drive/app),cargo check --workspace;pnpm lint(0 errors),vue-tsc,prettier --check,vitest(265 pass).Part of #34 (does not close the tracking epic).
🤖 Generated with Claude Code