Skip to content

Fix/tls trust known cert hostname#126

Open
zerox80 wants to merge 3 commits intoopencloud-eu:mainfrom
zerox80:fix/tls-trust-known-cert-hostname
Open

Fix/tls trust known cert hostname#126
zerox80 wants to merge 3 commits intoopencloud-eu:mainfrom
zerox80:fix/tls-trust-known-cert-hostname

Conversation

@zerox80
Copy link
Copy Markdown
Contributor

@zerox80 zerox80 commented Apr 20, 2026

Fixes the regression from 0c72514, reported in #125.

The short version: 0c72514 (correctly) dropped the all-accepting HostnameVerifier that was effectively disabling hostname checks for every connection - a real MITM hole. Problem is, it also broke every self-hosted setup where the URL hostname doesn't match the cert's CN/SAN. Classic case from the issue: https://truenas:30259 with a self-signed TrueNAS cert that's issued for truenas.local or an IP. On 1.2.0 this silently worked because the check was a no-op; on 1.2.1 it's a dead end with no way forward.

Why it's a dead end, exactly

Walking the flow on 1.2.1 for the affected user:

  1. URL entered, handshake starts. Cert is unknown -> AdvancedX509TrustManager.checkServerTrusted() throws CertificateCombinedException
  2. First dialog ("Untrusted cert") shows up with Trust button, user clicks it, cert gets written to knownServers.bks
  3. onSavedCertificate() retries the request
  4. Second handshake: TrustManager is happy now. OkHttp's default HostnameVerifier kicks in and rejects truenas against the cert's SAN
  5. SSLPeerUnverifiedException -> wrapped into the new SSL_RECOVERABLE_PEER_UNVERIFIED code path
  6. Second dialog shows up. But newInstanceForFullSslError sees sslPeerUnverifiedException != null and sets m509Certificate = null, which in turn hides Cancel and relabels Trust as plain OK
  7. User clicks OK, mHandler == null -> onCancelCertificate() -> login aborted

Reinstall doesn't help: cert stays in the store so the retry hits step 4 immediately without ever going through the trust dialog.

The fix

New KnownServersHostnameVerifier. It runs OkHttp's default OkHostnameVerifier (RFC 2818/6125) first. If that passes, done. If it fails, it checks whether the peer cert is already in knownServers.bks. If yes, the hostname mismatch is accepted - the rationale being that the user has already actively opted into trusting exactly this certificate.

Per-scenario breakdown of what that does:

Scenario Before (1.2.1) After
Valid public cert, correct hostname OK OK (fast path, no change)
Valid public cert, wrong hostname Rejected Still rejected (not in store)
Self-signed, hostname mismatch, user accepted once Dead end Works (this is the fix)
Self-signed, hostname mismatch, user hasn't accepted Trust dialog -> dead end Trust dialog -> works after trust
Random hostile cert we've never seen Rejected Still rejected

Also added NetworkUtils.isCertInKnownServersStore() for the lookup.

Security note

The 1.2.0 bypass was blanket - every TLS connection effectively had hostname verification disabled. This one is gated per-certificate on explicit user trust: someone pulling off a MITM needs both a copy of a cert the user has already accepted and network position. For self-hosted users the effective risk is equivalent to 1.2.0, but without the global bypass for all TLS traffic to regular servers.

Dead code I left in place

The sslPeerUnverifiedException branch in RemoteOperationResult and the "hide Cancel + relabel OK" branch in SslUntrustedCertDialog are both unreachable in the normal flow now, because the HostnameVerifier handles the known-cert case before it ever bubbles up. I didn't rip them out in this PR - they still act as a safety net if a SSLPeerUnverifiedException ever comes through from a cert that isn't in the store (e.g. a misconfigured public server), so we'd still get a mapped result code and a sensible dialog instead of a generic network error. Happy to clean them up in a follow-up if you'd rather have lean code than defense in depth.

Tests

HttpClientTlsTest updated to match the new contract:

  • accepts user-trusted certificate despite hostname mismatch - the regression case, inverted from the previous "rejects" test
  • rejects certificate with hostname mismatch when not in known servers - guards the security property
  • wraps hostname mismatch as certificate combined exception - unchanged, still covers the fallback mapping

All three pass locally. Detekt and the app compile are also clean.

Not tested by me

I don't have a TrueNAS box handy, so I didn't run the full login end-to-end on a real device against the affected setup. Confirmation from the reporter in #125 would be appreciated before merging.

Closes #125.

@guruz
Copy link
Copy Markdown
Contributor

guruz commented Apr 20, 2026

I agree that this PR makes sense. Let's see..

@guruz guruz self-requested a review April 20, 2026 10:25
@guruz guruz self-assigned this Apr 20, 2026
@guruz
Copy link
Copy Markdown
Contributor

guruz commented Apr 20, 2026

@Scemp Could you try the APK from https://github.com/opencloud-eu/android/releases/tag/build-tester-apk-9 ?
This is a separate app build (OpenCloud QA) different from normal build.

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.

Certificate hostname mismatch error in v1.2.1 (works in v1.2.0)

2 participants