From a6605e27926ec1f239348f02bc350a80868b537c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 16:10:00 +0000 Subject: [PATCH] fix(quic): use hostname-aware trust manager on Android Android's RootTrustManager throws when an app has Network Security Config domain-specific entries and the 2-arg checkServerTrusted overload is used, crashing Nest room joins on relays covered by such config. Discover the 3-arg checkServerTrusted(chain, authType, hostname) overload via reflection and invoke it with the SNI host; fall back to the standard 2-arg form on plain JVM where that overload doesn't exist. --- .../quic/tls/JdkCertificateValidator.kt | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/tls/JdkCertificateValidator.kt b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/tls/JdkCertificateValidator.kt index b7fc7ba80..34ebd3855 100644 --- a/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/tls/JdkCertificateValidator.kt +++ b/quic/src/jvmAndroid/kotlin/com/vitorpamplona/quic/tls/JdkCertificateValidator.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quic.tls import com.vitorpamplona.quic.QuicCodecException import java.io.ByteArrayInputStream +import java.lang.reflect.InvocationTargetException import java.net.IDN import java.net.InetAddress import java.security.KeyStore @@ -83,7 +84,22 @@ class JdkCertificateValidator( // RFC 8422 ext, no dedicated TLS 1.3 string else -> "ECDHE_ECDSA" } - trustManager.checkServerTrusted(parsed.toTypedArray(), authType) + // Android's RootTrustManager rejects the 2-arg overload when the + // app has Network Security Config domain-specific entries and + // requires the hostname-aware 3-arg variant. That overload is + // Android-specific (not on standard X509TrustManager), so we + // discover it by reflection and fall back on plain JVM. + val chainArray = parsed.toTypedArray() + val hostnameAware = hostnameAwareCheckServerTrusted(trustManager) + if (hostnameAware != null) { + try { + hostnameAware.invoke(trustManager, chainArray, authType, expectedHost) + } catch (e: InvocationTargetException) { + throw e.cause ?: e + } + } else { + trustManager.checkServerTrusted(chainArray, authType) + } } catch (t: Throwable) { throw QuicCodecException("certificate chain validation failed: ${t.message}", t) } @@ -245,5 +261,17 @@ class JdkCertificateValidator( tmf.init(null as KeyStore?) return tmf.trustManagers.firstNotNullOf { it as? X509TrustManager } } + + private fun hostnameAwareCheckServerTrusted(tm: X509TrustManager): java.lang.reflect.Method? = + try { + tm.javaClass.getMethod( + "checkServerTrusted", + Array::class.java, + String::class.java, + String::class.java, + ) + } catch (_: NoSuchMethodException) { + null + } } }