Skip to content

fix(transport): install Android trust manager for IP-literal TLS brokers#67

Merged
jamesarich merged 1 commit into
mainfrom
fix/android-tls-ip-literal-trust
Jun 22, 2026
Merged

fix(transport): install Android trust manager for IP-literal TLS brokers#67
jamesarich merged 1 commit into
mainfrom
fix/android-tls-ip-literal-trust

Conversation

@jamesarich

@jamesarich jamesarich commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Description

Connecting to a private MQTT broker over TLS by IP address fails on Android with a confusing platform error:

TLS handshake failed: Domain specific configurations require that hostname aware checkServerTrusted(X509Certificate[], String, String) is used

Why: ktor's CIO TLS engine only calls the 2-arg X509TrustManager.checkServerTrusted(chain, authType) (KTOR-2243). On Android, when the consuming app's network_security_config.xml contains any <domain-config> block, the platform swaps in a NetworkSecurityTrustManager that throws from the 2-arg overload — regardless of the target host — and demands the 3-arg hostname-aware overload.

5591751 already addressed this with HostnameAwareTrustManager, but only installed it when a TLS SNI server name was present. SNI is intentionally null for IP literals (RFC 6066 §3 forbids IPs in SNI), so configurePlatformTrust returned early and ktor fell back to the throwing platform default. A private broker reached by LAN IP — the common self-signed-cert setup — therefore still failed.

The defect was surfaced by the recent migration off OkHttp (whose trust manager calls the 3-arg overload) to ktor CIO. Diagnosis credit to @Olli-LUT on the linked issue.

This fix decouples the SNI server name (correctly null for IPs) from the trust-evaluation host (always the real host). The hostname-aware trust manager is now installed for IP-literal brokers too, routing through X509TrustManagerExtensions so the platform's domain-aware requirement is satisfied.

Note: this does not auto-trust self-signed certificates. After this fix an untrusted self-signed broker fails with a meaningful Trust anchor for certification path not found instead of the misleading internal error; to connect, the broker's CA must be installed in Android's user trust store.

Changes

All in the :transport-tcp module:

  • configurePlatformTrust(serverName: String?)configurePlatformTrust(host: String) across the expect and all three actuals (android / jvm / native). The non-null String is a compile-time guard against re-passing the nullable SNI value.
  • Android actual now installs HostnameAwareTrustManager for any non-blank host (IP literal or DNS name), not only when SNI is present.
  • TcpTransport call site passes endpoint.host for trust evaluation while still suppressing SNI for IPs.
  • Extracted sniServerName() / isIpLiteral() to top-level internal functions and added commonTest's TcpTransportTlsTest covering the SNI-vs-trust-host decoupling (IPv4/IPv6 suppress SNI; DNS keeps it).

Testing

  • spotlessCheck + detekt pass
  • :transport-tcp:allTests passes (JVM + macOS + iOS native), incl. new TcpTransportTlsTest (4/4)
  • :transport-tcp:compileCommonMainKotlinMetadata + Android target compile/assemble pass (expect/actual contract validated for all targets)
  • New/updated tests cover the changes

Related Issues

Addresses meshtastic/Meshtastic-Android#5894 (an app-side mqtt-client version bump is required to ship the fix to users).

The HostnameAwareTrustManager added in 5591751 was only installed when a TLS
SNI server name was present. SNI is intentionally null for IP literals (RFC
6066 §3 forbids IP literals in SNI), so configurePlatformTrust returned early
and ktor's CIO engine fell back to the platform default trust manager.

On Android, when network_security_config.xml contains any <domain-config>, the
platform's NetworkSecurityTrustManager throws from the 2-arg
checkServerTrusted(chain, authType) regardless of the target host and requires
the 3-arg hostname-aware overload (KTOR-2243). So a private broker reached by
LAN IP — the common self-signed-cert setup — still failed with "Domain specific
configurations require that hostname aware checkServerTrusted(...) is used".

Decouple the SNI server name from the trust-evaluation host: pass the real host
(IP literal or DNS name) to configurePlatformTrust, whose parameter is now a
non-null String — a compile-time guard against re-passing the nullable SNI
value. The HostnameAwareTrustManager is now installed for IP literals too,
routing through X509TrustManagerExtensions so the platform's domain-aware
requirement is satisfied. SNI stays suppressed for IPs via sniServerName().

Reported in meshtastic/Meshtastic-Android#5894.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich force-pushed the fix/android-tls-ip-literal-trust branch from 4621178 to 3f763be Compare June 22, 2026 12:15
@jamesarich jamesarich marked this pull request as ready for review June 22, 2026 12:34
@jamesarich jamesarich added this pull request to the merge queue Jun 22, 2026
Merged via the queue into main with commit e2974f4 Jun 22, 2026
21 of 23 checks passed
@jamesarich jamesarich deleted the fix/android-tls-ip-literal-trust branch June 22, 2026 12:46
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