fix(mobile): trust user-installed Android CA certificates#25731

merged7
main
fix/issue-5639
opened3d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 2d ago

Problem

Android rejects HTTPS environments signed by a CA installed in the Android user certificate store. On a Pixel 9 running Android API 36, Chrome trusts the same endpoint while the base T3 app fails environment discovery with a Transport error and a TLS CERTIFICATE_UNKNOWN alert.

Change

Generate an Android network security configuration that trusts system and user certificate stores, and reference it from the manifest. Preserve existing cleartext HTTP support in the configuration because Android ignores usesCleartextTraffic once a network security configuration is present. This applies user-installed CA trust app-wide and requires a new native Android client build.

Scope and approval

Fixes https://github.com/pingdotgg/t3code/issues/5639

GitCafe: #5639

Triaged bug via the automated issue flow. The change is limited to the existing Android network configuration plugin and its registration.

Verification

  • expo config --type introspect and expo prebuild --platform android --no-install: generated manifest references the XML configuration; generated XML trusts system and user roots and permits cleartext.
  • Packaged APK inspection confirms both manifest attributes and the compiled XML contents. Native artifact identity and inspection.
  • vp run check:changed: format, lint, both knip passes and both affected-package typechecks passed on final integrated head 235e42ec4a. The worktree is clean.
  • Android OS certificate trust requires a native runtime; proof uses the real app and TLS observations rather than a configuration-mirroring unit test.

Pixel 9, Android API 36, portrait 1080×2424, light appearance, font scale 1.0. Base source: 9bc0c33438; native fix: 853aceb5a1. Head native fingerprint e9cabfd8aa6930799024d4e47fefcd0c8220fc7d, APK SHA-256 643269934f0ae53037e5fcb094303b67cb121034a4262916a941dc7695c1dcd7. Captures use the debug development client.

Before (left): Chrome trusts the private CA, but base T3 fails discovery. After (right): rebuilt T3 connects and receives a fresh provider status response.

Before and after: Android private-CA environment connection

FixtureResult and runtime evidence
Base, private CA installedChrome succeeds; T3 discovery fails; TLS-observed retry.
Head, private CA installedHTTPS pairing, WSS 101 and response frames; fresh read and reconnect/read pass.
Head, private CA removedRetained reconnect rejects after process restart; fresh discovery rejects.
Head, system CA, private CA absentPairing and fresh read over HTTPS/WSS pass with only that route saved.
Head, cleartextHTTP pairing and fresh read over HTTP/WS pass with only that route saved.

Full evidence index and timelines, fixture matrix, sanitized transport observations, proxy driver, and reproduction drivers. Pairing credentials are masked in recordings.

No render paths changed; the comparison demonstrates connection behavior, with the changed connection state and provider response visible. Independent WSS rejection with the private CA absent could not be reached because HTTPS descriptor preflight rejects first. A first private-CA refresh encountered a transient SocketReadError; subsequent fresh reads and reconnect/read passed. A separate release APK was not run. iOS is unaffected by this Android-only plugin change.

After merging main, the primary agent repeated a provider refresh successfully on the isolated system-CA route. Final integrated client capture. The Android plugin/config bytes remain identical to the captured native-fix commit. Final integrated source: 235e42ec4a. Independent proof audit: sufficient.

Final scoped check log · Final cached native-fingerprint check.

Implementation: Claude Opus 5.5 through Claude Code; triage, integration and proof: GPT-6.1-sol through Codex in T3 Code.

Takeover

Taken over from @juliusmarminge's automated issue flow. The plugin shape was already right and is kept. The plugin no longer sets android:usesCleartextTraffic: with minSdk 24, Android ignores it once a network security config exists, and the config's base-config already permits cleartext (C1 below shows HTTP/WS still work). A short note in docs/user/remote-access.md explains installing a private CA on Android. No tests: a plugin unit test would only restate the XML. The proof is on-device. The "both manifest attributes" line above describes the original commit; only android:networkSecurityConfig remains.

Evidence

Real Android app on a pooled Pixel 9, Android 16 (API 36), portrait 1080×2424, light theme, font scale 1.0. Status-bar time is masked and pairing codes are redacted. Builds: base 71ba6b374c (native fingerprint ed36a9cc) vs head bbb9042e92 (0b110f6b). One isolated backend with synthetic data serves every row. The final head 9e3e0af976 adds only docs and a merge of main, with the same plugin and config.

RowBuildResultRecordingTranscript
B1: private CA in user storebaseRejected at TLS (certificate unknown), full error shownB1.mp4B1-transcript.txt
U1: same CAheadHTTPS pairing, WSS 101, live read; forced socket close, then automatic reconnect with a new ticket and upgradeU1.mp4U1-transcript.txt
N1: CA removed, app relaunchedheadSame endpoint rejected at TLSN1.mp4N1-transcript.txt
S1: public (Let's Encrypt) cert, no private CAheadHTTPS, WSS 101, live readS1.mp4S1-transcript.txt
C1: plain http://headHTTP, WS 101, live read, no TLS redirectC1.mp4C1-transcript.txt

Before (left, B1): the base app rejects the private-CA endpoint. After (right, U1): the head app is connected.

Before: base rejects private-CA endpoint. After: head connected

What else changed: no rendered UI code changed. The two sides differ only in connection outcome and the screen it leads to (pairing sheet vs environment details), endpoint text, and synthetic canary names. The private root was regenerated for the B1/N1 retakes; U1 used the original. N1 has two labelled cuts and rejects at the HTTPS preflight, so a separate WSS rejection is never attempted. All captures use development APKs. native-artifact-proof.txt shows the APK's merged manifest and its resolved system + user + cleartext policy, with no debug overrides.

Full evidence index with narrated timestamps · fixture matrix · scripts: generate-certs.sh, proxy.mjs, seed-and-canary.mjs, pair-row.mjs, prepare-public-cert.sh, http-observer.mjs

Verification log
  • Tests: 0 → 0 (none in the original; a plugin test would only mirror the XML constant).
  • Review round 1 (head 22b9965): Sol and Opus found no code defects; both were FIX_FIRST only because evidence was missing. Round 2 (head bbb9042, with evidence): Sol MERGE, Opus MERGE. Two docs notes (paragraph placement, "CA certificate" wording) were applied. Both reviewers flag app-wide TLS trust as the sensitive area.
  • Proof audit: pass 1 derived the five-row matrix. Pass 2 found B1 (error under the gesture bar) and N1 (ghosted and blank frames) INSUFFICIENT; both were re-recorded, and the recheck returned SUFFICIENT for all five rows.
  • expo prebuild is idempotent. Nothing under node_modules declares its own network security config. The native fingerprint includes the plugin, so the dev client rebuilds.
  • vp run check:changed passes on 9e3e0af976. CI: 31/31 green on 9e3e0af976. CodeRabbit: no blocking issue. Macroscope: needs human approval (trust boundary).
  • Unverified: release build on a physical device, and an API 24 device.
  • Takeover: Claude Opus 5.5 (lead, tests, implementation, review B) through Claude Code in T3 Code; GPT-6.1-sol (review A, proof audit, captures) through Codex in T3 Code.

🤖 Generated with Claude Code

Checks

22 successful · 16 skipped · 1 neutral
All checks passed
9e3e0af

Added 2 commits

  • Merge main into fix/issue-5639235e42e
  • fix(mobile): Android trusts user-installed CAs for HTTPS environments853aceb
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — Android now trusts user-installed CAs app-wide while preserving existing cleartext support. Human review must confirm that this broader default trust boundary is intended, rather than restricting private-CA trust to selected endpoints.

You can add or adjust custom eligibility rules. Learn more.

Synced from GitHub

Review in Change Stack →

No actionable comments were generated in the recent review.

External image Recent review info
External image Run configuration
External image Commits

Reviewing files that changed from the base of the PR and between 235e42ec4a001a223ac33c1f3a604dda800a9ded and 9e3e0af976da374c060164cdad51de2b61981a5f.

External image Files selected for processing: 3
  • apps/mobile/app.config.ts
  • apps/mobile/plugins/withAndroidNetworkSecurityConfig.cjs
  • docs/user/remote-access.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Walkthrough

The Android app now uses a network security configuration that permits cleartext traffic and trusts system and user-installed certificates. The Expo configuration applies the new plugin instead of the previous cleartext-only plugin. The remote-access guide explains how to install a private CA on Android.

Changes: Android network security

Layer / File(s)Summary
Define and apply Android network security
apps/mobile/plugins/withAndroidNetworkSecurityConfig.cjs
The plugin defines system and user trust anchors, updates the manifest to reference the network security configuration, and writes the XML resource.
Activate and document the network security plugin
apps/mobile/app.config.ts, apps/mobile/plugins/withAndroidCleartextTraffic.cjs, docs/user/remote-access.md
The Expo plugin list now uses the new network security plugin. The previous cleartext-only plugin is removed. The guide explains that Android users must install a private CA as a CA certificate.

Priority: External image Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: External image Minimal · up to 9e3e0

The Android configuration adds user-installed CA trust while retaining cleartext support. No merge-blocking issue was identified.

Pre-merge checks: Passed 4
Check nameStatusExplanation
Title checkPassedThe title clearly and concisely describes the primary change: enabling Android trust for user-installed CA certificates.
Linked Issues checkPassedIssue #5639 requires Android HTTPS traffic to trust user-installed CA certificates and retain cleartext support. withAndroidNetworkSecurityConfig.cjs writes network_security_config.xml with `syste…
Out of Scope Changes checkPassedThe changes remain within issue #5639. The implementation replaces the existing Android cleartext plugin with the Android network security plugin. The plugin changes only the Android manifest and XML …
Description checkPassedThe pull request description includes all required sections: Problem, Change, Scope and approval, and Verification. It identifies the linked issue, explains the focused Android configuration change an…
Full details: Linked Issues check

Explanation

Issue #5639 requires Android HTTPS traffic to trust user-installed CA certificates and retain cleartext support. withAndroidNetworkSecurityConfig.cjs writes network_security_config.xml with system and user trust anchors and cleartextTrafficPermitted="true". It sets android:networkSecurityConfig and registers the plugin in apps/mobile/app.config.ts. The PR reports generated-config checks, APK inspection, runtime private-CA pairing, and cleartext verification. The added Android guidance is consistent with the issue.

Full details: Out of Scope Changes check

Explanation

The changes remain within issue #5639. The implementation replaces the existing Android cleartext plugin with the Android network security plugin. The plugin changes only the Android manifest and XML resource. The added documentation addresses private-CA setup for the Android app. No unrelated platform or product behavior changes are shown.

Full details: Description check

Explanation

The pull request description includes all required sections: Problem, Change, Scope and approval, and Verification. It identifies the linked issue, explains the focused Android configuration change and its app-wide effect, documents validation commands and runtime evidence, includes before/after screenshots and recordings, and states known limitations and unverified cases.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Added 5 commits

  • fix(mobile): drop redundant cleartext manifest flag beside the network security config22b9965
  • docs(mobile): give the Android user-CA note its own paragraphbbb9042
  • docs(mobile): move the Android user-CA note to the LAN pairing section8f9d752
  • docs(mobile): name Android's CA certificate option9bcd819
  • Merge remote-tracking branch 'origin/main' into pr-257309e3e0af