Skip to content

fix(desktop): trust OS root certificates (Watt Toolkit / corporate MITM)#516

Merged
rainxchzed merged 2 commits intomainfrom
fix/460-trust-os-root-store
May 5, 2026
Merged

fix(desktop): trust OS root certificates (Watt Toolkit / corporate MITM)#516
rainxchzed merged 2 commits intomainfrom
fix/460-trust-os-root-store

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 5, 2026

Closes #460

Summary

Desktop builds now trust certificates installed in the OS root store (Windows-ROOT keychain, macOS Keychain) in addition to the JVM-bundled cacerts. Lets users behind TLS-intercepting tools — Watt Toolkit, FastGithub, Fiddler, corporate MITM proxies — keep using the app without manually injecting CAs into the JDK with keytool.

Why

Watt Toolkit (and similar China-side accelerators) terminate TLS with their own self-signed CA. The browser works because the installer adds that CA to the Windows cert store. Our desktop client uses the JVM cacerts bundle instead — Watt's CA isn't there, every github.com request 502s on TLS handshake, even with the app's "no proxy" mode (Watt's hosts-file mode is system-wide).

Implementation

  • New OsTrustManager.kt (jvmMain) builds a CompositeX509TrustManager that delegates to the default JVM trust manager and the platform-native one (Windows-ROOT on Windows, KeychainStore on macOS). Either accepts → handshake succeeds.
  • HttpClientFactory.jvm.kt wires the composite into OkHttp via sslSocketFactory(...). Silently skipped on platforms where no OS keystore is available — default trust still applies.
  • Linux: skipped — most JDK distributions already merge /etc/ssl/certs into cacerts at install time. Users on minimal distros can still keytool -importcert.
  • No new permissions, no new dependencies, no behavior change for users who don't have custom CAs in their OS store.

Test plan

  • Windows + Watt Toolkit (any mode): app loads details / search without 502 on github.com.
  • Windows + no Watt Toolkit, no MITM: regression check — app behaves identically to before.
  • macOS + custom CA in Keychain (test via mkcert): app trusts intercepted github.com TLS.
  • macOS + no custom CA: regression check.
  • Linux: regression check (we don't add anything; default behavior preserved).
  • What's-new sheet on next 1.8.1 install shows the new bullet in device language.

Summary by CodeRabbit

  • Bug Fixes

    • Desktop now trusts operating system-installed root certificates, enabling third-party tools and proxies (such as Watt Toolkit, FastGithub, Fiddler, and corporate MITM proxies) to work without manual configuration.
  • Documentation

    • Updated release notes across 14 languages to reflect the above fix.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Walkthrough

The PR adds support for JVM platforms to trust OS-installed root certificates by integrating a composite TLS trust manager that combines platform defaults with OS-specific certificate stores (Windows-ROOT on Windows, KeychainStore on macOS). Release notes are updated across 14 localized versions documenting this feature fix.

Changes

OS Trust Chain Integration

Layer / File(s) Summary
Core Trust Manager Implementation
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt
Introduces buildOsTrustChainOrNull() which loads OS-specific (X509TrustManager via keystore selection based on OS), combines with platform default trust manager, and returns a CompositeX509TrustManager wrapping both. CompositeX509TrustManager delegates certificate validation to each manager in sequence until one accepts, or throws the last observed error.
HTTP Client Configuration
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt
OkHttp engine configuration now invokes buildOsTrustChainOrNull() and, when available, installs the resulting sslSocketFactory and trustManager into the client. Existing proxy authentication logic remains unmodified.

Release Notes Localization

Layer / File(s) Summary
Multi-language Documentation
core/presentation/src/commonMain/composeResources/files/whatsnew/*/16.json (14 locales: English, Arabic, Bengali, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Turkish, Simplified Chinese, +base)
Each locale's "FIXED" section in version 16 release notes is updated with a new bullet stating desktop now trusts OS-installed root certificates, enabling tools like Watt Toolkit, FastGithub, Fiddler, and corporate MITM proxies to work without manual keytool configuration.

Sequence Diagram

sequenceDiagram
    actor HC as HTTP Client<br/>Factory
    participant OTM as OS Trust<br/>Manager
    participant OSK as OS Keystore<br/>Detection
    participant DTM as Platform<br/>Default TM
    participant CTM as Composite<br/>TM
    participant SSL as SSL<br/>Context

    HC->>OTM: buildOsTrustChainOrNull()
    OTM->>OSK: Detect OS (Windows/macOS)<br/>Select Keystore Type
    OSK-->>OTM: Keystore Type
    par Load Trust Managers
        OTM->>DTM: Load platform default
        OTM->>OSK: Load OS-specific from<br/>selected keystore
    end
    DTM-->>OTM: Default TM
    OSK-->>OTM: OS TM (if available)
    alt Two or more TMs available
        OTM->>CTM: Create CompositeX509TrustManager
        CTM->>SSL: Initialize SSLContext('TLS')<br/>with composite
        SSL-->>OTM: Return (SSLSocketFactory,<br/>CompositeX509TrustManager)
        OTM-->>HC: OsTrustChain
    else Fewer than 2 TMs
        OTM-->>HC: null
    end
    HC->>HC: Apply to OkHttp engine config
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A curious bunny hops with glee,
Trust chains now dance where certs should be!
No keytool magic, no admin spell—
Just OS roots that work quite well.
Watt Toolkit, GitHub, all in sight,
The proxy paths are shining bright! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling OS root certificate trust for desktop to support TLS-intercepting tools like Watt Toolkit.
Linked Issues check ✅ Passed All coding objectives from issue #460 are met: OS root store trust implemented for desktop JVM via OsTrustManager and HttpClientFactory changes, with platform-specific handling (Windows-ROOT, KeychainStore on macOS, Linux skipped).
Out of Scope Changes check ✅ Passed All changes are in-scope: two new/modified Kotlin files implement core trust logic, and localized release notes document the fix across 12 languages for v1.8.1.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/460-trust-os-root-store

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt (2)

27-29: ⚡ Quick win

Narrow catch (_: Throwable) and surface failures.

Catching Throwable swallows OutOfMemoryError, InterruptedException (if it ever propagates as Throwable on this path) and ordinary programming bugs. In particular, in checkClientTrusted/checkServerTrusted you record any unchecked exception or Error as lastError and then, if every delegate fails, rethrow that Throwable from a method whose contract is throws CertificateException — a JSSE caller asserting catch (e: CertificateException) will not catch a wrapped RuntimeException and the failure mode becomes confusing.

Two suggestions:

  1. Narrow each catch to Exception (or CertificateException in the per-delegate trust loops) so genuine VM errors propagate.
  2. Wrap lastError as CertificateException(cause = lastError) when it isn't already a CertificateException, so the method stays contract-faithful.

A debug log (even at Logger.getLogger(...).fine(...)) on the swallow path would also save a lot of time the next time someone debugs why the OS chain "silently didn't load."

♻️ Proposed adjustment for the trust loops
     override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
-        var lastError: Throwable? = null
+        var lastError: CertificateException? = null
         for (tm in delegates) {
             try {
                 tm.checkServerTrusted(chain, authType)
                 return
-            } catch (t: Throwable) {
-                lastError = t
+            } catch (e: java.security.cert.CertificateException) {
+                lastError = e
             }
         }
         throw lastError ?: java.security.cert.CertificateException("No trust manager accepted the chain")
     }

Also applies to: 44-48, 50-56, 67-69, 80-82

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt`
around lines 27 - 29, In OsTrustManager.kt narrow the overly broad `catch (_:
Throwable)` blocks in the `checkClientTrusted` and `checkServerTrusted` delegate
loops to catch `Exception` (or specifically `CertificateException` for
per-delegate failures) so Errors like OOME/AssertionError propagate; when all
delegates fail, if `lastError` is not already a `CertificateException` wrap it
as `CertificateException(cause = lastError)` before rethrowing to preserve the
method contract; also add a fine/debug log on the swallow path (e.g. via
Logger.getLogger(...) .fine(...)) to record each delegated failure for
diagnostics.

58-89: 🏗️ Heavy lift

Consider implementing X509ExtendedTrustManager if delegate managers use it.

CompositeX509TrustManager only implements the base X509TrustManager interface. If any delegate is an X509ExtendedTrustManager, its socket/SSLEngine-aware overloads (which support endpoint identification during the SSL handshake) will not be invoked. For consistency with TLS best practices, the composite should implement X509ExtendedTrustManager and forward to the extended overloads when available, falling back to the legacy overloads otherwise.

This is a technical refinement for defense-in-depth on endpoint verification. OkHttp's hostname verification (via HostnameVerifier) still applies independently and will catch certificate mismatches; this change would ensure the SSL handshake layer also validates endpoint identity when the underlying trust managers support it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt`
around lines 58 - 89, CompositeX509TrustManager currently implements
X509TrustManager only; update it to implement X509ExtendedTrustManager and add
overrides for the socket/SSLEngine-aware methods (checkClientTrusted(chain,
authType, Socket) and checkClientTrusted(chain, authType, SSLEngine), and
similarly for checkServerTrusted) that forward to each delegate: if a delegate
is an X509ExtendedTrustManager call its extended method, otherwise fall back to
the legacy checkClientTrusted/checkServerTrusted; preserve the existing behavior
of returning on first success and collecting the last exception to throw, and
keep getAcceptedIssuers() as-is; add necessary imports for
X509ExtendedTrustManager, Socket, and SSLEngine.
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt (1)

17-25: 💤 Low value

Consider caching the OS trust chain across client builds.

buildOsTrustChainOrNull() performs OS-keystore I/O (loading Windows-ROOT / KeychainStore, building a TrustManagerFactory, initializing an SSLContext) on every call to createPlatformHttpClient. Each proxy setting toggle re-pays that cost, and OkHttp/Conscrypt actually prefer a single shared SSLSocketFactory instance per process for connection-pool reuse.

A top-level private val osTrustChain by lazy { buildOsTrustChainOrNull() } (or a memoized accessor in OsTrustManager.kt) would address both at once. The TLS configuration here doesn't change per ProxyConfig, so it's safe to share.

♻️ Sketch
+private val osTrustChain: OsTrustChain? by lazy { buildOsTrustChainOrNull() }
+
 actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient =
     HttpClient(OkHttp) {
         engine {
             config {
-                buildOsTrustChainOrNull()?.let { chain ->
+                osTrustChain?.let { chain ->
                     sslSocketFactory(chain.socketFactory, chain.trustManager)
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`
around lines 17 - 25, createPlatformHttpClient currently calls
buildOsTrustChainOrNull() on every client build which repeats OS keystore I/O
and prevents sharing of the SSLSocketFactory; change this to cache the result
(e.g. a top-level private val osTrustChain by lazy { buildOsTrustChainOrNull() }
or a memoized accessor in OsTrustManager) and then use that cached osTrustChain
in createPlatformHttpClient when calling sslSocketFactory(chain.socketFactory,
chain.trustManager); ensure the cached value remains nullable and preserved
across ProxyConfig-driven rebuilds so TLS configuration is shared process-wide.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`:
- Around line 17-25: createPlatformHttpClient currently calls
buildOsTrustChainOrNull() on every client build which repeats OS keystore I/O
and prevents sharing of the SSLSocketFactory; change this to cache the result
(e.g. a top-level private val osTrustChain by lazy { buildOsTrustChainOrNull() }
or a memoized accessor in OsTrustManager) and then use that cached osTrustChain
in createPlatformHttpClient when calling sslSocketFactory(chain.socketFactory,
chain.trustManager); ensure the cached value remains nullable and preserved
across ProxyConfig-driven rebuilds so TLS configuration is shared process-wide.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt`:
- Around line 27-29: In OsTrustManager.kt narrow the overly broad `catch (_:
Throwable)` blocks in the `checkClientTrusted` and `checkServerTrusted` delegate
loops to catch `Exception` (or specifically `CertificateException` for
per-delegate failures) so Errors like OOME/AssertionError propagate; when all
delegates fail, if `lastError` is not already a `CertificateException` wrap it
as `CertificateException(cause = lastError)` before rethrowing to preserve the
method contract; also add a fine/debug log on the swallow path (e.g. via
Logger.getLogger(...) .fine(...)) to record each delegated failure for
diagnostics.
- Around line 58-89: CompositeX509TrustManager currently implements
X509TrustManager only; update it to implement X509ExtendedTrustManager and add
overrides for the socket/SSLEngine-aware methods (checkClientTrusted(chain,
authType, Socket) and checkClientTrusted(chain, authType, SSLEngine), and
similarly for checkServerTrusted) that forward to each delegate: if a delegate
is an X509ExtendedTrustManager call its extended method, otherwise fall back to
the legacy checkClientTrusted/checkServerTrusted; preserve the existing behavior
of returning on first success and collecting the last exception to throw, and
keep getAcceptedIssuers() as-is; add necessary imports for
X509ExtendedTrustManager, Socket, and SSLEngine.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ae0aa08-4bec-4976-aae2-c5e9c3ea7d9a

📥 Commits

Reviewing files that changed from the base of the PR and between a902c84 and 7f95747.

📒 Files selected for processing (15)
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/OsTrustManager.kt
  • core/presentation/src/commonMain/composeResources/files/whatsnew/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json
  • core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json

@rainxchzed rainxchzed merged commit 89d2a1a into main May 5, 2026
1 check passed
@rainxchzed rainxchzed deleted the fix/460-trust-os-root-store branch May 5, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed to connect to the Github when using Watt Toolkit to accelerate

1 participant