Skip to content

Let users trust a server certificate Android rejects - #41

Open
chamika wants to merge 4 commits into
mainfrom
add-certificate-trust-pinning
Open

Let users trust a server certificate Android rejects#41
chamika wants to merge 4 commits into
mainfrom
add-certificate-trust-pinning

Conversation

@chamika

@chamika chamika commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Problem

Android's system CA store is frozen at the OS release. A Polestar 4 on Android 12 cannot validate certificates chaining to Sectigo Public Server Authentication Root R46 — AOSP only added that root in Android 15, and Play can't backport it because the updatable CA store landed in Android 14.

Verified against AOSP (control root returns 200 on every tag, so the 404s are real):

Root A12 A13 A14 A15
Sectigo … Root R46
USERTrust RSA (control)
ISRG Root X1

Chrome works on the same device because it ships its own root store, which makes this look like an app bug. Reproduced byte-for-byte on an Android 12 emulator (zero Sectigo roots present) against the Crashlytics stack trace.

Approach

Pins the exact leaf certificate rather than disabling verification. Platform validation runs first and still rejects everything it normally would; only a SHA-256 fingerprint the user explicitly approved is accepted, so a machine-in-the-middle substituting its own certificate still fails. That matters for a head unit carrying an access token across untrusted hotspots.

Sign-in distinguishes a rejected certificate from an unreachable server, shows the fingerprint, and lets the user pin it.

A single OkHttpClient now backs the Jellyfin SDK, album art and playback, so an approved certificate applies to streaming, buffering and prefetch rather than only the login request. Playback moves from DefaultHttpDataSource to OkHttpDataSource to share that client — the old HttpURLConnection stack had no hook to pass an SSLSocketFactory through.

Pinned certificates are listed in Settings and removable individually.

Verification on Android 12

  • Trust dialog shows the correct fingerprint — matched openssl against the live server byte-for-byte
  • After accepting: ping succeeds, sign-in advances, QuickConnect returned a live code from the server
  • Declining pins nothing
  • Settings lists the pinned host and removal works
  • Instrumented test opens the media data source (the playback/prefetch path) both without a pin (fails with SSLHandshakeException) and with one (succeeds), built through the real production provider

Known gaps

  • The instrumented test hits a live server, so it is unsuitable for CI and breaks on certificate rotation. Worth swapping for MockWebServer with a self-signed cert.
  • Actual audio decoding over a pinned connection is unexercised — the test covers the TLS handshake and byte transfer, not a decoded track. Needs a login to confirm.
  • No regression pass against a normally-trusted server. The delegate-first design leaves that path unchanged, but it deserves a check before release.

Includes the v1.3.2(28) release commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YUCBnCgapXargAeGfmUHGE

chamika and others added 4 commits July 27, 2026 17:18
Android's system CA store is frozen at the OS release, so head units on
Android 12-14 reject certificates chaining to newer roots. A Polestar 4
(Android 12) cannot validate Sectigo Public Server Authentication Root R46,
which AOSP only added in Android 15, and Play cannot backport it because the
updatable CA store landed in Android 14. Chrome works because it ships its own
root store, which makes the failure look like an app bug.

Sign-in now distinguishes a rejected certificate from an unreachable server,
shows its SHA-256 fingerprint, and lets the user pin it.

This pins the exact leaf certificate rather than disabling verification.
Platform validation still runs first and still rejects everything it normally
would; only a fingerprint the user explicitly approved is accepted, so a
machine-in-the-middle substituting its own certificate still fails. That
matters for a head unit carrying an access token across untrusted hotspots.

A single OkHttpClient now backs the Jellyfin SDK, album art and playback, so an
approved certificate applies to streaming, buffering and prefetch rather than
only to the login request. Playback moves from DefaultHttpDataSource to
OkHttpDataSource to share that client.

Pinned certificates are listed in Settings and can be removed individually.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUCBnCgapXargAeGfmUHGE
Instrumented test opens the media data source against a server whose
certificate Android 12 rejects, asserting it fails without a pin and succeeds
with one. It builds the client through the real production provider and wraps
it as DashTuneMusicService does, so it covers the wiring rather than a copy.

Uses an unauthenticated Jellyfin endpoint so it needs no credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUCBnCgapXargAeGfmUHGE
Connect to servers whose HTTPS certificate your car does not recognise

Full release notes:

- Sign in to servers your car rejects with an SSL error. DashTune now shows the certificate details and lets you approve it, instead of only saying "Could not reach server"
- Approving a certificate applies everywhere — browsing, playback, buffering, offline downloads and album art — so music streams normally afterwards
- Certificates are matched exactly, so an approved server stays protected against interception on public networks
- Approved certificates are listed under Settings, where they can be removed at any time
pingServer returns PingResult rather than Boolean now, which broke compilation
of the existing tests. Adds coverage for the two new branches: reporting the
certificate when one is rejected, and falling back to unreachable when it
cannot be read back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUCBnCgapXargAeGfmUHGE
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.

1 participant