mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 01:46:10 +00:00
fix(android): preserve TLS hostname context (#105072)
* fix(android): preserve TLS hostname context Co-authored-by: whisky0809 <32197959+whisky0809@users.noreply.github.com> * style(android): order TLS test imports * style(android): group TLS test imports --------- Co-authored-by: whisky0809 <32197959+whisky0809@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
9b057036e3
commit
e4d8a2a55b
@@ -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<X509Certificate>,
|
||||
authType: String,
|
||||
@@ -79,6 +94,30 @@ fun buildGatewayTlsConfig(
|
||||
defaultTrust.checkClientTrusted(chain, authType)
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
socket: Socket,
|
||||
) {
|
||||
if (defaultTrust is X509ExtendedTrustManager) {
|
||||
defaultTrust.checkClientTrusted(chain, authType, socket)
|
||||
} else {
|
||||
checkClientTrusted(chain, authType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) {
|
||||
if (defaultTrust is X509ExtendedTrustManager) {
|
||||
defaultTrust.checkClientTrusted(chain, authType, engine)
|
||||
} else {
|
||||
checkClientTrusted(chain, authType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
@@ -101,6 +140,31 @@ fun buildGatewayTlsConfig(
|
||||
defaultTrust.checkServerTrusted(chain, authType)
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
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<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) {
|
||||
if (usesPlatformTrust && defaultTrust is X509ExtendedTrustManager) {
|
||||
defaultTrust.checkServerTrusted(chain, authType, engine)
|
||||
} else {
|
||||
checkServerTrusted(chain, authType)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = defaultTrust.acceptedIssuers
|
||||
}
|
||||
|
||||
@@ -147,12 +211,24 @@ internal suspend fun probeGatewayTlsFingerprint(
|
||||
val fingerprintRef = AtomicReference<String?>(null)
|
||||
val probeTrustManager =
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
object : X509TrustManager {
|
||||
object : X509ExtendedTrustManager() {
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
): Unit = throw CertificateException("gateway TLS probe does not accept client certificates")
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
socket: Socket,
|
||||
) = checkClientTrusted(chain, authType)
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) = checkClientTrusted(chain, authType)
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
@@ -163,6 +239,18 @@ internal suspend fun probeGatewayTlsFingerprint(
|
||||
throw CertificateException("gateway TLS probe captured fingerprint")
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
socket: Socket,
|
||||
) = checkServerTrusted(chain, authType)
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) = checkServerTrusted(chain, authType)
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- This app is primarily used on a trusted tailnet; allow cleartext for IP-based endpoints too. -->
|
||||
<!-- This app is primarily used on a trusted tailnet; allow cleartext for every endpoint. -->
|
||||
<base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" />
|
||||
<!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">openclaw.local</domain>
|
||||
</domain-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">ts.net</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
@@ -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<X509Certificate>,
|
||||
authType: String,
|
||||
) = Unit
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
socket: Socket,
|
||||
) = Unit
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) = Unit
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
) {
|
||||
serverTwoArgumentCalls += 1
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
socket: Socket,
|
||||
) {
|
||||
serverSocketCalls += 1
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String,
|
||||
engine: SSLEngine,
|
||||
) {
|
||||
serverEngineCalls += 1
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LOOPBACK_HOST = "127.0.0.1"
|
||||
val LOOPBACK_ADDRESS: InetAddress = InetAddress.getByName(LOOPBACK_HOST)
|
||||
|
||||
Reference in New Issue
Block a user