Skip to content

Trust the OS store and SSL_CERT_FILE for HTTPS (#1107) - #1326

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/v5-tls-native-roots
Oct 9, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/v5-tls-native-roots

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1107

Summary

Every HTTPS client now trusts the bundled Mozilla (webpki) roots plus the platform trust store, so socket-patch works behind a TLS-inspecting corporate proxy whose CA the OS (or SSL_CERT_FILE) trusts. Certificate verification is never disabled.

Root cause

The workspace builds reqwest with rustls-tls only, which in reqwest 0.12 means the bundled webpki roots. No production client builder added any other root, so neither the OS store nor SSL_CERT_FILE / SSL_CERT_DIR was ever read. Every API, blob, registry, telemetry and self-update call failed with UnknownIssuer behind an inspecting proxy, while curl/npm/pip in the same environment worked.

Changes

  • New utils::http::client_builder() in core: the one constructor for production reqwest clients. It hands reqwest one shared rustls ClientConfig (built once per process; ring provider, TLS 1.2/1.3, HTTP/1.1 ALPN) whose verifier, BundledThenPlatform, runs full webpki verification against the bundled roots first and, only on UnknownIssuer, against the platform roots loaded by rustls-native-certs (macOS keychain, Windows store, Linux bundle; SSL_CERT_FILE / SSL_CERT_DIR when set).
    • Trust is the union of both sets. Every other verification failure (expiry, wrong host, bad signature) is returned unchanged.
    • The platform store is read lazily, once per process, and only by a run that meets a certificate the bundled roots don't know. A normal run pays nothing: the first push (which loaded the store eagerly) regressed the CI scan benchmark by 5–10 ms CPU, which is why it's built this way. Building a client now parses no roots at all (reqwest's default re-parses the webpki roots per client).
    • Unparsable native certificates are skipped (add_parsable_certificates); an empty or unreadable store leaves the bundled roots.
  • Routed through it: the authenticated API client and the plain proxy client (api/client.rs), telemetry, self-update metadata and download, and the vendored registry client (vendor/registry_fetch.rs, which also backs the upstream-restore client). The registry client's silent reqwest::Client::new() fallback (which would have bypassed the roots) is now an expect, matching the API client.
  • Dependencies: rustls-native-certs =0.8.4 (new: plus openssl-probe, security-framework on macOS, schannel on Windows); rustls (std, ring, tls12) and webpki-roots are the exact builds reqwest's rustls-tls already compiles. Dev-only: tokio-rustls (already in the lockfile, ring provider) for the local TLS server.
  • Docs: CLI_CONTRACT.md (next to the HTTPS_PROXY sentence) and docs/configuration.md state how trust roots are chosen.

Tests (per acceptance criterion)

  • Private CA via SSL_CERT_FILE completes a handshake with a local TLS server whose leaf it signed, and the platform store is read exactly once across two requests: utils::http::tests::client_trusts_a_private_ca_named_by_ssl_cert_file.
  • Without the CA the handshake fails with UnknownIssuer (verification stays on): client_without_the_private_ca_fails_with_unknown_issuer.
  • An unparsable root in the bundle is skipped, not fatal: unparsable_platform_roots_are_skipped_not_fatal.
  • reqwest accepts the shared config (it refuses a foreign rustls build at build()): production_builder_builds. Also checked live: the debug binary's get <uuid> --json against the real public proxy completes (webpki path).
  • Ratchet: no_client_is_built_outside_client_builder scans core/cli/node src (test modules excluded) for Client::builder( / Client::new( / ClientBuilder::new(. Red before the fix: it listed the 7 production sites (api client ×2, telemetry, update release/download, registry_fetch ×2); green after.
  • Default trust of public endpoints is unchanged: the bundled webpki roots are always tried first.
  • Fixtures: crates/socket-patch-core/tests/tls-ca/ (P-256 CA + 127.0.0.1 leaf, valid 100 years). Kept out of tests/fixtures/ because the VEX discovery golden corpus walks every directory there.

Commands run

  • cargo test -p socket-patch-core --all-features --no-fail-fast: all 39 test binaries green.
  • cargo test -p socket-patch-cli --all-features --test update --test get --test scan_api_retry_e2e --test self_update_e2e --test self_update_failures_e2e --test remove_rollback_api_overrides: 211 passed.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo fmt --all -- --check: only a pre-existing diff in patch/redirect/upstream/mod.rs (untouched here).

🤖 Generated with Claude Code


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every HTTPS call (patch API, public proxy, blob and registry fetches,
telemetry, self-update) trusted only the bundled Mozilla roots. Behind
a TLS-inspecting corporate proxy, whose private CA no setting could
add, every request failed with UnknownIssuer.

All production reqwest clients are now built by one constructor,
utils::http::client_builder(), which adds the platform trust store to
the bundled roots. rustls-native-certs reads the macOS keychain, the
Windows store or the Linux bundle, and honors SSL_CERT_FILE and
SSL_CERT_DIR. The store is read once per process and unusable roots
are skipped. Verification is never relaxed.

Tests run a local TLS server signed by a private CA: the handshake
succeeds only when SSL_CERT_FILE names that CA. A text ratchet keeps
new reqwest::Client constructors out of production code.

Fixes #1107

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@socket-security

socket-security Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​rustls-native-certs@​0.8.410010093100100

View full report

@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Trust platform and SSL_CERT_FILE roots for HTTPS (#1107) Trust the OS store and SSL_CERT_FILE for HTTPS (#1107) Oct 9, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 9, 2026 17:38
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review

@socket-security-staging

socket-security-staging Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​rustls-native-certs@​0.8.410010093100100

View full report

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c510914. Configure here.

Comment thread crates/socket-patch-core/src/utils/http.rs Fixed
CI's scan benchmark showed 5-10 ms more CPU per run: every process
read the whole platform store (hundreds of PEM files on Linux) and
every client re-parsed those roots.

client_builder() now hands reqwest one shared rustls config, built
once per process. Its verifier checks the bundled webpki roots first
and reads the platform store (or SSL_CERT_FILE / SSL_CERT_DIR) only
after an UnknownIssuer, then once per process. Other verification
errors are returned unchanged, so trust is still the union of both
sets and verification is never relaxed. Building a client no longer
parses any roots. The load-error log line flagged by CodeQL is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 9, 2026
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 9, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 442e1e972fe1.

  • CI: required checks ci-ok and clippy green; 7 check suites succeeded. 1 superseded workflow run(s) show as cancelled; the required gates passed on this head.
  • Mergeable against main, no CHANGELOG.md change.
  • Bugbot reviewed this head; no unresolved review threads.

Labeled Ready for review by the burn-down agent. Slack announcement pending (connector unavailable this run).


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) removed this pull request from the merge queue due to a manual request Oct 9, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit c6b0d99 Oct 9, 2026
25 of 37 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/v5-tls-native-roots branch October 9, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every HTTPS call fails behind a TLS-inspecting proxy because the clients trust only bundled webpki roots

3 participants