From a9a9f7aef5393f14dbe05d53ac9dab2734a7f260 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 22:46:08 -0400 Subject: [PATCH 1/2] Create a place for ECH in our configureTlsExtensions API --- .../okhttp/android/test/AndroidSocketAdapterTest.kt | 4 ++-- .../src/main/kotlin/mockwebserver3/MockWebServer.kt | 7 ++++++- .../java/mockwebserver3/internal/http2/Http2Server.kt | 7 ++++++- .../kotlin/okhttp3/internal/platform/Android10Platform.kt | 4 +++- .../kotlin/okhttp3/internal/platform/AndroidPlatform.kt | 4 +++- .../internal/platform/android/Android10SocketAdapter.kt | 2 ++ .../internal/platform/android/AndroidSocketAdapter.kt | 2 ++ .../platform/android/BouncyCastleSocketAdapter.kt | 2 ++ .../internal/platform/android/ConscryptSocketAdapter.kt | 2 ++ .../internal/platform/android/DeferredSocketAdapter.kt | 4 +++- .../okhttp3/internal/platform/android/SocketAdapter.kt | 2 ++ .../kotlin/okhttp3/internal/connection/ConnectPlan.kt | 8 +++++++- .../kotlin/okhttp3/internal/platform/Jdk9Platform.kt | 2 ++ .../kotlin/okhttp3/internal/platform/Platform.kt | 2 ++ .../okhttp3/internal/platform/BouncyCastlePlatform.kt | 4 +++- .../kotlin/okhttp3/internal/platform/ConscryptPlatform.kt | 4 +++- .../internal/platform/Jdk8WithJettyBootPlatform.kt | 2 ++ .../kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt | 4 +++- 18 files changed, 55 insertions(+), 11 deletions(-) diff --git a/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt b/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt index 59fc723ab598..e62c5885eb79 100644 --- a/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt +++ b/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt @@ -58,7 +58,7 @@ class AndroidSocketAdapterTest( val sslSocket = socketFactory.createSocket() as SSLSocket assertTrue(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1), null) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } @@ -89,7 +89,7 @@ class AndroidSocketAdapterTest( object : DelegatingSSLSocket(context.socketFactory.createSocket() as SSLSocket) {} assertFalse(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1), null) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } diff --git a/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt b/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt index 60b0d28034e1..a4ef8311953c 100644 --- a/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt +++ b/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt @@ -473,7 +473,12 @@ public class MockWebServer : Closeable { openClientSockets.add(sslSocket) if (protocolNegotiationEnabled) { - Platform.get().configureTlsExtensions(sslSocket, null, protocols) + Platform.get().configureTlsExtensions( + sslSocket = sslSocket, + hostname = null, + protocols = protocols, + echConfigList = null, + ) } sslSocket.startHandshake() diff --git a/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt b/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt index bc9a3a12b4cd..a8c53fe6e0c8 100644 --- a/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt +++ b/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt @@ -82,7 +82,12 @@ class Http2Server( true, ) as SSLSocket sslSocket.useClientMode = false - Platform.get().configureTlsExtensions(sslSocket, null, listOf(Protocol.HTTP_2)) + Platform.get().configureTlsExtensions( + sslSocket = sslSocket, + hostname = null, + protocols = listOf(Protocol.HTTP_2), + echConfigList = null, + ) sslSocket.startHandshake() return sslSocket } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt index 671427de4e67..00cbe21a7079 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt @@ -37,6 +37,7 @@ import okhttp3.internal.platform.android.ConscryptSocketAdapter import okhttp3.internal.platform.android.DeferredSocketAdapter import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex +import okio.ByteString /** Android 10+ (API 29+). */ @SuppressSignatureCheck @@ -75,11 +76,12 @@ class Android10Platform : sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } - ?.configureTlsExtensions(sslSocket, hostname, protocols) + ?.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt index 4f94d192b034..0acf655366f9 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt @@ -42,6 +42,7 @@ import okhttp3.internal.platform.android.StandardAndroidSocketAdapter import okhttp3.internal.tls.BasicTrustRootIndex import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex +import okio.ByteString /** Android 5 to 9 (API 21 to 28). */ @SuppressSignatureCheck @@ -93,11 +94,12 @@ class AndroidPlatform : sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } - ?.configureTlsExtensions(sslSocket, hostname, protocols) + ?.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt index 83a3f4f41cbb..d8e83eabcd10 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt @@ -25,6 +25,7 @@ import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.isAndroid +import okio.ByteString /** * Simple non-reflection SocketAdapter for Android Q+. @@ -57,6 +58,7 @@ class Android10SocketAdapter : SocketAdapter { sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { try { SSLSockets.setUseSessionTickets(sslSocket, true) diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt index 5913600873c5..1c57942272d3 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt @@ -22,6 +22,7 @@ import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.AndroidPlatform import okhttp3.internal.platform.Platform +import okio.ByteString /** * Modern reflection based SocketAdapter for Conscrypt class SSLSockets. @@ -48,6 +49,7 @@ open class AndroidSocketAdapter( sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt index 6d2b8beb0e62..1673a0b312bc 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt @@ -18,6 +18,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.Platform +import okio.ByteString import org.bouncycastle.jsse.BCSSLSocket /** @@ -41,6 +42,7 @@ class BouncyCastleSocketAdapter : SocketAdapter { sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt index 006e593b7bcf..48dec2683938 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt @@ -18,6 +18,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.Platform +import okio.ByteString import org.conscrypt.Conscrypt /** @@ -39,6 +40,7 @@ class ConscryptSocketAdapter : SocketAdapter { sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt index 10b619dec831..f968e4747546 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt @@ -17,6 +17,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol +import okio.ByteString /** * Deferred implementation of SocketAdapter that works by observing the socket @@ -39,8 +40,9 @@ class DeferredSocketAdapter( sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { - getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, hostname, protocols) + getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = getDelegate(sslSocket)?.getSelectedProtocol(sslSocket) diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt index 40776f6bfb17..fa6a62614140 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt @@ -19,6 +19,7 @@ import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol +import okio.ByteString interface SocketAdapter { fun isSupported(): Boolean @@ -33,6 +34,7 @@ interface SocketAdapter { sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) fun getSelectedProtocol(sslSocket: SSLSocket): String? diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt index 9fe3bbbd2008..407fc7fc2624 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt @@ -18,6 +18,7 @@ package okhttp3.internal.connection import java.io.IOException import java.net.ConnectException import java.net.HttpURLConnection +import java.net.NoRouteToHostException import java.net.ProtocolException import java.net.Proxy import java.net.Socket as JavaNetSocket @@ -345,7 +346,12 @@ class ConnectPlan internal constructor( var success = false try { if (connectionSpec.supportsTlsExtensions) { - Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols) + Platform.get().configureTlsExtensions( + sslSocket = sslSocket, + hostname = address.url.host, + protocols = address.protocols, + echConfigList = route.echConfigList + ) } // Force handshake. This can throw! diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt index 6247d485331f..c014e1b82456 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt @@ -22,6 +22,7 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck +import okio.ByteString /** * OpenJDK 9+ and JDK8 build 252+. @@ -35,6 +36,7 @@ open class Jdk9Platform : Platform() { sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { val sslParameters = sslSocket.sslParameters diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt index f33d97972b94..d65df2df3df3 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt @@ -40,6 +40,7 @@ import okhttp3.internal.tls.BasicTrustRootIndex import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex import okio.Buffer +import okio.ByteString import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** @@ -116,6 +117,7 @@ open class Platform { sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt index 83e2e57ee61b..c0239d37bf53 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt @@ -23,6 +23,7 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol +import okio.ByteString import org.bouncycastle.jsse.BCSSLSocket import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider @@ -59,6 +60,7 @@ class BouncyCastlePlatform private constructor() : Platform() { sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { if (sslSocket is BCSSLSocket) { val sslParameters = sslSocket.parameters @@ -69,7 +71,7 @@ class BouncyCastlePlatform private constructor() : Platform() { sslSocket.parameters = sslParameters } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt index 51ef4ca4b3c2..c309dc598000 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt @@ -26,6 +26,7 @@ import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol +import okio.ByteString import org.conscrypt.Conscrypt import org.conscrypt.ConscryptHostnameVerifier @@ -78,6 +79,7 @@ class ConscryptPlatform private constructor() : Platform() { sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { if (Conscrypt.isConscrypt(sslSocket)) { // Enable session tickets. @@ -87,7 +89,7 @@ class ConscryptPlatform private constructor() : Platform() { val names = alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt index 5ae0567cd212..4efb364d5e90 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt @@ -21,6 +21,7 @@ import java.lang.reflect.Method import java.lang.reflect.Proxy import javax.net.ssl.SSLSocket import okhttp3.Protocol +import okio.ByteString /** OpenJDK 8 with `org.mortbay.jetty.alpn:alpn-boot` in the boot class path. */ class Jdk8WithJettyBootPlatform( @@ -34,6 +35,7 @@ class Jdk8WithJettyBootPlatform( sslSocket: SSLSocket, hostname: String?, protocols: List, + echConfigList: ByteString?, ) { val names = alpnProtocolNames(protocols) diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt index e4d50a391144..cbdabc767406 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt @@ -23,6 +23,7 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol +import okio.ByteString /** * Platform using OpenJSSE (https://github.com/openjsse/openjsse) if installed as the first @@ -63,6 +64,7 @@ class OpenJSSEPlatform private constructor() : Platform() { sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, + echConfigList: ByteString?, ) { if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) { val sslParameters = sslSocket.sslParameters @@ -75,7 +77,7 @@ class OpenJSSEPlatform private constructor() : Platform() { sslSocket.sslParameters = sslParameters } } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } } From 7eca4489f49ca01ec7e483087e6731ed6ad28689 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Fri, 17 Jul 2026 22:43:37 -0400 Subject: [PATCH 2/2] Successfully exercise ECH with Conscrypt This worked locally using a local build of @mnbogner's pull request https://github.com/google/conscrypt/pull/1406 --- gradle/libs.versions.toml | 2 +- .../okhttp3/dnsoverhttps/EchRemoteTest.kt | 73 +++++++++++++++++++ .../internal/platform/ConscryptPlatform.kt | 4 + settings.gradle.kts | 1 + 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/EchRemoteTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c31716ac6fee..a9a762b577c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ mockserver-client = "5.15.0" mrjar = "0.1.1" openjsse = "1.1.14" org-bouncycastle = "1.84" -org-conscrypt = "2.5.2" +org-conscrypt = "2.6-SNAPSHOT" org-junit-jupiter = "5.13.4" playservices-safetynet = "18.1.0" robolectric = "4.16.1" diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/EchRemoteTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/EchRemoteTest.kt new file mode 100644 index 000000000000..428deb78b9f5 --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/EchRemoteTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.dnsoverhttps + +import assertk.assertThat +import assertk.assertions.matchesPredicate +import java.net.InetAddress +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.OkHttpClientTestRule +import okhttp3.Request +import okhttp3.Response +import okhttp3.testing.PlatformRule +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Remote") +class EchRemoteTest { + @RegisterExtension + val platform = PlatformRule(requiredPlatformName = PlatformRule.CONSCRYPT_PROPERTY) + + @RegisterExtension + val clientTestRule = OkHttpClientTestRule() + + private var client = clientTestRule.newClientBuilder() + .dns( + DnsOverHttps + .Builder() + .client(clientTestRule.newClient()) + .url("https://1.1.1.1/dns-query".toHttpUrl()) + .bootstrapDnsHosts(InetAddress.getByName("1.1.1.1")) + .includeHttps(true) + .build() + ) + .build() + + @Test + fun testHttpsRequest() { + val cloudflareBody = client.sendRequest( + Request.Builder().url("https://crypto.cloudflare.com/cdn-cgi/trace").build() + ) { + it.body.string() + } + assertThat(cloudflareBody).matchesPredicate { it.contains("sni=encrypted") } + + val tlsEchBody = client.sendRequest(Request.Builder().url("https://tls-ech.dev/").build()) { + it.body.string() + } + assertThat(tlsEchBody).matchesPredicate { it.contains("You are using ECH. :)") } + } + + private fun OkHttpClient.sendRequest(request: Request, fn: (Response) -> T): T { + val response = newCall(request).execute() + + return response.use { + fn(it) + } + } +} diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt index c309dc598000..543543ad9130 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt @@ -29,6 +29,7 @@ import okhttp3.Protocol import okio.ByteString import org.conscrypt.Conscrypt import org.conscrypt.ConscryptHostnameVerifier +import org.conscrypt.EchParameters /** * Platform using Conscrypt (conscrypt.org) if installed as the first Security Provider. @@ -88,6 +89,9 @@ class ConscryptPlatform private constructor() : Platform() { // Enable ALPN. val names = alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) + + // Enable ECH parameters. + Conscrypt.setEchParameters(sslSocket, EchParameters(true, echConfigList?.toByteArray())) } else { super.configureTlsExtensions(sslSocket, hostname, protocols, echConfigList) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 53d4dfdba76e..203eeaaf9af2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ dependencyResolutionManagement { repositories { mavenCentral() google() + mavenLocal() } }