diff --git a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayTls.kt b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayTls.kt index 063cda2a585a..ac646251e2ee 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayTls.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayTls.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.withContext import java.io.EOFException import java.net.ConnectException import java.net.InetSocketAddress +import java.net.Socket import java.net.SocketException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -19,11 +20,13 @@ import javax.net.ssl.HostnameVerifier import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SNIHostName import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine import javax.net.ssl.SSLException import javax.net.ssl.SSLParameters import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedTrustManager import javax.net.ssl.X509TrustManager /** TLS pinning inputs for a discovered or manually configured gateway endpoint. */ @@ -63,15 +66,27 @@ fun buildGatewayTlsConfig( onStore: ((String) -> Unit)? = null, ): GatewayTlsConfig? { if (params == null) return null + return buildGatewayTlsConfig( + params = params, + defaultTrust = defaultTrustManager(), + onStore = onStore, + ) +} + +internal fun buildGatewayTlsConfig( + params: GatewayTlsParams, + defaultTrust: X509TrustManager, + onStore: ((String) -> Unit)? = null, +): GatewayTlsConfig { val expected = params.expectedFingerprint ?.let(::normalizeGatewayTlsFingerprint) ?.takeIf { it.isNotBlank() } - val defaultTrust = defaultTrustManager() + val usesPlatformTrust = expected == null && !params.allowTOFU @SuppressLint("CustomX509TrustManager") val trustManager = - object : X509TrustManager { + object : X509ExtendedTrustManager() { override fun checkClientTrusted( chain: Array, authType: String, @@ -79,6 +94,30 @@ fun buildGatewayTlsConfig( defaultTrust.checkClientTrusted(chain, authType) } + override fun checkClientTrusted( + chain: Array, + authType: String, + socket: Socket, + ) { + if (defaultTrust is X509ExtendedTrustManager) { + defaultTrust.checkClientTrusted(chain, authType, socket) + } else { + checkClientTrusted(chain, authType) + } + } + + override fun checkClientTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) { + if (defaultTrust is X509ExtendedTrustManager) { + defaultTrust.checkClientTrusted(chain, authType, engine) + } else { + checkClientTrusted(chain, authType) + } + } + override fun checkServerTrusted( chain: Array, authType: String, @@ -101,6 +140,31 @@ fun buildGatewayTlsConfig( defaultTrust.checkServerTrusted(chain, authType) } + override fun checkServerTrusted( + chain: Array, + authType: String, + socket: Socket, + ) { + if (usesPlatformTrust && defaultTrust is X509ExtendedTrustManager) { + // Preserve the connected hostname for Android's domain-aware platform trust manager. + defaultTrust.checkServerTrusted(chain, authType, socket) + } else { + checkServerTrusted(chain, authType) + } + } + + override fun checkServerTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) { + if (usesPlatformTrust && defaultTrust is X509ExtendedTrustManager) { + defaultTrust.checkServerTrusted(chain, authType, engine) + } else { + checkServerTrusted(chain, authType) + } + } + override fun getAcceptedIssuers(): Array = defaultTrust.acceptedIssuers } @@ -147,12 +211,24 @@ internal suspend fun probeGatewayTlsFingerprint( val fingerprintRef = AtomicReference(null) val probeTrustManager = @SuppressLint("CustomX509TrustManager") - object : X509TrustManager { + object : X509ExtendedTrustManager() { override fun checkClientTrusted( chain: Array, authType: String, ): Unit = throw CertificateException("gateway TLS probe does not accept client certificates") + override fun checkClientTrusted( + chain: Array, + authType: String, + socket: Socket, + ) = checkClientTrusted(chain, authType) + + override fun checkClientTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) = checkClientTrusted(chain, authType) + override fun checkServerTrusted( chain: Array, authType: String, @@ -163,6 +239,18 @@ internal suspend fun probeGatewayTlsFingerprint( throw CertificateException("gateway TLS probe captured fingerprint") } + override fun checkServerTrusted( + chain: Array, + authType: String, + socket: Socket, + ) = checkServerTrusted(chain, authType) + + override fun checkServerTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) = checkServerTrusted(chain, authType) + override fun getAcceptedIssuers(): Array = emptyArray() } diff --git a/apps/android/app/src/main/res/xml/network_security_config.xml b/apps/android/app/src/main/res/xml/network_security_config.xml index 7ac5f5cdd7ba..3b0654b668c0 100644 --- a/apps/android/app/src/main/res/xml/network_security_config.xml +++ b/apps/android/app/src/main/res/xml/network_security_config.xml @@ -1,12 +1,5 @@ - + - - - openclaw.local - - - ts.net - diff --git a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewayTlsTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewayTlsTest.kt index b100e75b9ceb..7c0da9ca6d51 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewayTlsTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewayTlsTest.kt @@ -7,9 +7,43 @@ import java.net.InetAddress import java.net.ServerSocket import java.net.Socket import java.net.SocketException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager import kotlin.concurrent.thread class GatewayTlsTest { + @Test + fun buildGatewayTlsConfig_forwardsPlatformTrustWithSocketAndEngineContext() { + val defaultTrust = RecordingExtendedTrustManager() + val config = + buildGatewayTlsConfig( + params = + GatewayTlsParams( + required = true, + expectedFingerprint = null, + allowTOFU = false, + stableId = "gateway-1", + ), + defaultTrust = defaultTrust, + ) + val extendedTrust = config.trustManager as X509ExtendedTrustManager + + Socket().use { socket -> + extendedTrust.checkServerTrusted(emptyArray(), "RSA", socket) + } + extendedTrust.checkServerTrusted( + emptyArray(), + "RSA", + SSLContext.getDefault().createSSLEngine(), + ) + + assertEquals(1, defaultTrust.serverSocketCalls) + assertEquals(1, defaultTrust.serverEngineCalls) + assertEquals(0, defaultTrust.serverTwoArgumentCalls) + } + @Test fun probeGatewayTlsFingerprint_reportsHandshakeTimeoutAfterTcpConnect() = runBlocking { @@ -109,6 +143,54 @@ class GatewayTlsTest { } } + private class RecordingExtendedTrustManager : X509ExtendedTrustManager() { + var serverTwoArgumentCalls = 0 + var serverSocketCalls = 0 + var serverEngineCalls = 0 + + override fun checkClientTrusted( + chain: Array, + authType: String, + ) = Unit + + override fun checkClientTrusted( + chain: Array, + authType: String, + socket: Socket, + ) = Unit + + override fun checkClientTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) = Unit + + override fun checkServerTrusted( + chain: Array, + authType: String, + ) { + serverTwoArgumentCalls += 1 + } + + override fun checkServerTrusted( + chain: Array, + authType: String, + socket: Socket, + ) { + serverSocketCalls += 1 + } + + override fun checkServerTrusted( + chain: Array, + authType: String, + engine: SSLEngine, + ) { + serverEngineCalls += 1 + } + + override fun getAcceptedIssuers(): Array = emptyArray() + } + private companion object { const val LOOPBACK_HOST = "127.0.0.1" val LOOPBACK_ADDRESS: InetAddress = InetAddress.getByName(LOOPBACK_HOST)