Skip to content

fix: trust user-installed CAs - #141

Merged
adibhanna merged 2 commits into
ZenNotes:mainfrom
lewxdev:feat/user-installed-ca-trust
Oct 28, 2026
Merged

adibhanna merged 2 commits into
ZenNotes:mainfrom
lewxdev:feat/user-installed-ca-trust

Conversation

@lewxdev

@lewxdev lewxdev commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Android rejects HTTPS connections to private-CA self-hosted servers even when the issuing CA is installed in the user's certificate store. Add the app-wide Network Security Configuration proposed in #140, trusting both system and user-installed CAs while retaining certificate-chain and hostname verification.

Reference the configuration from the main manifest and replace the debug-only resource so debug and release use the same policy. Preserve existing plain-HTTP self-hosting support with cleartextTrafficPermitted="true". No server hostname is hardcoded.

Validation on macOS with JDK 21 and Android SDK 36:

  • JavaScript tests, package boundary checks, TypeScript checks, and web build/Capacitor sync passed.
  • All 30 native unit tests, Android lint, and debug/release builds passed.
  • Compiled manifests and network security resources in both APKs were inspected to confirm system/user trust anchors and the shared policy.

No emulator or device connectivity test was performed. Private-server TLS, login, and sync remain unverified at runtime; no installed APK or app data was modified. Follow-up testing should confirm private-CA connectivity with the CA installed, rejection of untrusted/expired/wrong-host certificates, and continued public-CA HTTPS and HTTP self-hosting support.

Related to #140. This PR does not claim runtime validation.

…sted

ZenNotes#141 trusts the user certificate store app-wide so a self-hosted server
behind a private CA connects (ZenNotes#140). That store also holds any CA a
device manager pushes or a user is talked into installing, and with the
base policy alone such a CA could vouch for zennotes.org as well. On an
emulator with a throwaway CA in the user store and a proxy presenting a
zennotes.org certificate signed by it, the ZenNotes#141 build completed the
handshake and handed its Cloud API request to the proxy.

Cloud's hosts (zennotes.org and the legacy zennotes.laravel.cloud,
subdomains included) now get a domain-config that trusts the system
store alone and refuses plain HTTP: the trust Cloud had before ZenNotes#141.
Every other host keeps the PR's policy, system and user CAs with plain
HTTP allowed for LAN servers. The same interception now fails the
handshake ("Trust anchor for certification path not found"), while a
self-hosted server behind the private CA still connects and syncs, in a
debug build and in a release-signed build.

The debug-only manifest that pointed at the old debug resource is gone
too: the main manifest declares the one policy for every build type.

It does not cover the object-storage hosts of presigned uploads and
downloads. The service names them at run time, and they never receive
the account token (DirectUploadPlugin refuses an Authorization header
on a signed upload).

network-security-config.test.ts checks the policy against the Cloud URLs
in mobile-cloud-auth.ts, so a future host move (2.28 made one) cannot
leave Cloud on user trust unnoticed.
@adibhanna

Copy link
Copy Markdown
Contributor

Thanks @lewxdev, this is exactly the right fix for #140, and it ships in Android 1.1.50.

I tested it on an emulator against a self-hosted ZenNotes server behind a throwaway private CA, installed in the user certificate store. Before the change the app failed with "Trust anchor for certification path not found." With it, the app connects with the token and syncs both ways. It still rejects the server once the CA is removed, a certificate for the wrong host, and an expired certificate, and plain-HTTP servers on the LAN and public HTTPS keep working, in debug and in a release-signed build.

Before (1.1.49) After (this PR)
Before: Trust anchor for certification path not found After: a note on the private-CA server, edited from the phone

One follow-up commit sits on top of yours. Trusting the user store everywhere also let a user-installed CA vouch for zennotes.org: a proxy presenting such a certificate intercepted the app's Cloud requests. ZenNotes Cloud's hosts now trust only the system store and refuse plain HTTP, as before, while every other host keeps your policy, so private CAs work for self-hosted servers and Cloud can't be intercepted that way. The follow-up also drops the debug-only manifest that pointed at the old debug resource, and adds a test that keeps the Cloud hosts in the policy in step with the URLs the app uses.

Separately, the error in the "before" screenshot pointed Android users at an iPhone-only setting (Privacy & Security, Local Network). In 1.1.50 it says instead, for an https server, where to install your own certificate authority.

Your commit is merged into release/1.1.50; this PR will show as merged once that release lands on main.

@adibhanna
adibhanna merged commit a322253 into ZenNotes:main Oct 28, 2026
2 checks passed
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.

2 participants