Skip to content

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

Merged
juliusmarminge merged 7 commits into
mainfrom
fix/issue-5639
Oct 28, 2026
Merged

juliusmarminge merged 7 commits into
mainfrom
fix/issue-5639

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

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 #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

Fixture Result and runtime evidence
Base, private CA installed Chrome succeeds; T3 discovery fails; TLS-observed retry.
Head, private CA installed HTTPS pairing, WSS 101 and response frames; fresh read and reconnect/read pass.
Head, private CA removed Retained reconnect rejects after process restart; fresh discovery rejects.
Head, system CA, private CA absent Pairing and fresh read over HTTPS/WSS pass with only that route saved.
Head, cleartext HTTP 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.

Row Build Result Recording Transcript
B1: private CA in user store base Rejected at TLS (certificate unknown), full error shown B1.mp4 B1-transcript.txt
U1: same CA head HTTPS pairing, WSS 101, live read; forced socket close, then automatic reconnect with a new ticket and upgrade U1.mp4 U1-transcript.txt
N1: CA removed, app relaunched head Same endpoint rejected at TLS N1.mp4 N1-transcript.txt
S1: public (Let's Encrypt) cert, no private CA head HTTPS, WSS 101, live read S1.mp4 S1-transcript.txt
C1: plain http:// head HTTP, WS 101, live read, no TLS redirect C1.mp4 C1-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 9e3e0af. CI: 31/31 green on 9e3e0af. 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

juliusmarminge and others added 2 commits October 27, 2026 00:45
Android apps targeting API 24+ ignore CAs in the user store, so an
environment behind a private CA that Chrome loads fine failed in Add
environment with a transport error. The cleartext plugin now also writes
a network security config that trusts system and user CAs and keeps
cleartext permitted, since Android ignores usesCleartextTraffic once a
config exists.

Fixes #5639

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. labels Oct 27, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 235e42e and 9e3e0af.

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: Normal

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

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: t3dotgg

Merge Risk: 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 name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: enabling Android trust for user-installed CA certificates.
Linked Issues check Passed Issue #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 check Passed 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 …
Description check Passed 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 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.

@juliusmarminge juliusmarminge added the takeover t3 automation: takeover label Oct 27, 2026
@juliusmarminge juliusmarminge self-assigned this Oct 27, 2026

Copy link
Copy Markdown
Member Author

Taking over this one

@juliusmarminge juliusmarminge added the pr:in-progress A lead is working on this PR label Oct 27, 2026
juliusmarminge and others added 5 commits October 27, 2026 21:45
…k security config

Android ignores android:usesCleartextTraffic on API 24+ once a network
security config is set, and minSdk is 24; the config's base-config already
permits cleartext. Document installing a private CA for the Android app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/user/remote-access.md
@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked and removed pr:in-progress A lead is working on this PR labels Oct 28, 2026
@juliusmarminge
juliusmarminge merged commit 36aea58 into main Oct 28, 2026
48 of 49 checks passed
@juliusmarminge
juliusmarminge deleted the fix/issue-5639 branch October 28, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. pr:needs-julius Needs Julius: route gate, unverifiable surface, or blocked size:M 30-99 changed lines (additions + deletions). takeover t3 automation: takeover vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Android app does not trust user-installed CA certificates

1 participant