diff --git a/okhttp-tls/build.gradle b/okhttp-tls/build.gradle index 533d1f1c8..651e6fcfa 100644 --- a/okhttp-tls/build.gradle +++ b/okhttp-tls/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation deps.bouncycastlepkix implementation deps.bouncycastletls compileOnly deps.jsr305 + compileOnly deps.animalSniffer testImplementation project(':okhttp-testing-support') testImplementation deps.junit diff --git a/okhttp-tls/src/main/kotlin/okhttp3/tls/HandshakeCertificates.kt b/okhttp-tls/src/main/kotlin/okhttp3/tls/HandshakeCertificates.kt index 9ca08dff2..dc305c424 100644 --- a/okhttp-tls/src/main/kotlin/okhttp3/tls/HandshakeCertificates.kt +++ b/okhttp-tls/src/main/kotlin/okhttp3/tls/HandshakeCertificates.kt @@ -18,6 +18,7 @@ package okhttp3.tls import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.Collections +import javax.net.ssl.HostnameVerifier import javax.net.ssl.KeyManager import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory @@ -26,6 +27,7 @@ import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager import okhttp3.CertificatePinner import okhttp3.internal.platform.Platform +import okhttp3.internal.toImmutableList import okhttp3.tls.internal.TlsUtil.newKeyManager import okhttp3.tls.internal.TlsUtil.newTrustManager @@ -97,6 +99,7 @@ class HandshakeCertificates private constructor( private var heldCertificate: HeldCertificate? = null private var intermediates: Array? = null private val trustedCertificates = mutableListOf() + private val insecureHosts = mutableListOf() /** * Configure the certificate chain to use when being authenticated. The first certificate is @@ -140,9 +143,37 @@ class HandshakeCertificates private constructor( Collections.addAll(trustedCertificates, *platformTrustManager.acceptedIssuers) } + /** + * Configures this to not authenticate the HTTPS server on to [hostname]. This makes the user + * vulnerable to man-in-the-middle attacks and should only be used only in private development + * environments and only to carry test data. + * + * The server’s TLS certificate **does not need to be signed** by a trusted certificate + * authority. Instead, it will trust any well-formed certificate, even if it is self-signed. + * This is necessary for testing against localhost or in development environments where a + * certificate authority is not possible. + * + * The server’s TLS certificate still must match the requested hostname. For example, if the + * certificate is issued to `example.com` and the request is to `localhost`, the connection will + * fail. Use a custom [HostnameVerifier] to ignore such problems. + * + * Other TLS features are still used but provide no security benefits in absence of the above + * gaps. For example, an insecure TLS connection is capable of negotiating HTTP/2 with ALPN and + * it also has a regular-looking handshake. + * + * **This feature is not supported on Android API levels less than 24.** Prior releases lacked + * a mechanism to trust some hosts and not others. + * + * @param hostname the exact hostname from the URL for insecure connections. + */ + fun addInsecureHost(hostname: String) = apply { + insecureHosts += hostname + } + fun build(): HandshakeCertificates { + val immutableInsecureHosts = insecureHosts.toImmutableList() val keyManager = newKeyManager(null, heldCertificate, *(intermediates ?: emptyArray())) - val trustManager = newTrustManager(null, trustedCertificates) + val trustManager = newTrustManager(null, trustedCertificates, immutableInsecureHosts) return HandshakeCertificates(keyManager, trustManager) } } diff --git a/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureAndroidTrustManager.kt b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureAndroidTrustManager.kt new file mode 100644 index 000000000..c8876f93b --- /dev/null +++ b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureAndroidTrustManager.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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.tls.internal + +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.X509TrustManager + +/** This extends [X509TrustManager] for Android to disable verification for a set of hosts. */ +internal class InsecureAndroidTrustManager( + private val delegate: X509TrustManager, + private val insecureHosts: List +) : X509TrustManager { + private val checkServerTrustedMethod: Method? = try { + delegate::class.java.getMethod("checkServerTrusted", + Array::class.java, String::class.java, String::class.java) + } catch (_: NoSuchMethodException) { + null + } + + /** Android method to clean and sort certificates, called via reflection. */ + @Suppress("unused", "UNCHECKED_CAST") + fun checkServerTrusted( + chain: Array, + authType: String, + host: String + ): List { + if (host in insecureHosts) return listOf() + try { + val method = checkServerTrustedMethod + ?: throw CertificateException("Failed to call checkServerTrusted") + return method.invoke(this, chain, authType, host) as List + } catch (e: InvocationTargetException) { + throw e.targetException + } + } + + override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers + + override fun checkClientTrusted(chain: Array, authType: String?) = + throw CertificateException("Unsupported operation") + + override fun checkServerTrusted(chain: Array, authType: String) = + throw CertificateException("Unsupported operation") +} diff --git a/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureExtendedTrustManager.kt b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureExtendedTrustManager.kt new file mode 100644 index 000000000..19ae8f3d8 --- /dev/null +++ b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureExtendedTrustManager.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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.tls.internal + +import java.net.Socket +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedTrustManager +import okhttp3.internal.peerName +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +/** + * This extends [X509ExtendedTrustManager] to disable verification for a set of hosts. + * + * Note that the superclass [X509ExtendedTrustManager] isn't available on Android until version 7 + * (API level 24). + */ +@IgnoreJRERequirement +internal class InsecureExtendedTrustManager( + private val delegate: X509ExtendedTrustManager, + private val insecureHosts: List +) : X509ExtendedTrustManager() { + override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers + + override fun checkServerTrusted( + chain: Array, + authType: String, + socket: Socket + ) { + if (socket.peerName() !in insecureHosts) { + delegate.checkServerTrusted(chain, authType, socket) + } + } + + override fun checkServerTrusted( + chain: Array, + authType: String, + engine: SSLEngine + ) { + if (engine.peerHost !in insecureHosts) { + delegate.checkServerTrusted(chain, authType, engine) + } + } + + override fun checkServerTrusted(chain: Array, authType: String) = + throw CertificateException("Unsupported operation") + + override fun checkClientTrusted(chain: Array, authType: String?) = + throw CertificateException("Unsupported operation") + + override fun checkClientTrusted( + chain: Array, + authType: String, + engine: SSLEngine? + ) = throw CertificateException("Unsupported operation") + + override fun checkClientTrusted( + chain: Array, + authType: String, + socket: Socket? + ) = throw CertificateException("Unsupported operation") +} diff --git a/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/TlsUtil.kt b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/TlsUtil.kt index 21ff4646e..8746f2551 100644 --- a/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/TlsUtil.kt +++ b/okhttp-tls/src/main/kotlin/okhttp3/tls/internal/TlsUtil.kt @@ -22,10 +22,13 @@ import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.KeyManagerFactory import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedTrustManager import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager +import okhttp3.internal.platform.AndroidPlatform import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement object TlsUtil { val password = "password".toCharArray() @@ -47,10 +50,11 @@ object TlsUtil { fun localhost(): HandshakeCertificates = localhost /** Returns a trust manager that trusts `trustedCertificates`. */ - @JvmStatic + @JvmStatic @IgnoreJRERequirement fun newTrustManager( keyStoreType: String?, - trustedCertificates: List + trustedCertificates: List, + insecureHosts: List ): X509TrustManager { val trustStore = newEmptyKeyStore(keyStoreType) for (i in trustedCertificates.indices) { @@ -64,7 +68,13 @@ object TlsUtil { "Unexpected trust managers: ${result.contentToString()}" } - return result[0] as X509TrustManager + val trustManager = result[0] as X509TrustManager + + return when { + insecureHosts.isEmpty() -> trustManager + AndroidPlatform.isAndroid -> InsecureAndroidTrustManager(trustManager, insecureHosts) + else -> InsecureExtendedTrustManager(trustManager as X509ExtendedTrustManager, insecureHosts) + } } /** diff --git a/okhttp/src/test/java/okhttp3/InsecureForHostTest.kt b/okhttp/src/test/java/okhttp3/InsecureForHostTest.kt new file mode 100644 index 000000000..3cd2af0fb --- /dev/null +++ b/okhttp/src/test/java/okhttp3/InsecureForHostTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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 + +import javax.net.ssl.SSLException +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.testing.PlatformRule +import okhttp3.tls.HandshakeCertificates +import okhttp3.tls.HeldCertificate +import okhttp3.tls.internal.TlsUtil.localhost +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test + +class InsecureForHostTest { + @JvmField @Rule val platform = PlatformRule() + @JvmField @Rule val server = MockWebServer() + @JvmField @Rule val clientTestRule = OkHttpClientTestRule() + + @Test fun `untrusted host in insecureHosts connects successfully`() { + val serverCertificates = localhost() + server.useHttps(serverCertificates.sslSocketFactory(), false) + server.enqueue(MockResponse()) + + val clientCertificates = HandshakeCertificates.Builder() + .addPlatformTrustedCertificates() + .addInsecureHost(server.hostName) + .build() + + val client = clientTestRule.newClientBuilder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) + .build() + + val call = client.newCall(Request.Builder() + .url(server.url("/")) + .build()) + val response = call.execute() + assertThat(response.code).isEqualTo(200) + assertThat(response.handshake!!.cipherSuite).isNotNull() + assertThat(response.handshake!!.tlsVersion).isNotNull() + assertThat(response.handshake!!.localCertificates).isEmpty() + assertThat(response.handshake!!.localPrincipal).isNull() + assertThat(response.handshake!!.peerCertificates).isEmpty() + assertThat(response.handshake!!.peerPrincipal).isNull() + } + + @Test fun `bad certificates host in insecureHosts fails with SSLException`() { + val heldCertificate = HeldCertificate.Builder() + .addSubjectAlternativeName("example.com") + .build() + val serverCertificates = HandshakeCertificates.Builder() + .heldCertificate(heldCertificate) + .build() + server.useHttps(serverCertificates.sslSocketFactory(), false) + server.enqueue(MockResponse()) + + val clientCertificates = HandshakeCertificates.Builder() + .addPlatformTrustedCertificates() + .addInsecureHost(server.hostName) + .build() + + val client = clientTestRule.newClientBuilder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) + .build() + + val call = client.newCall(Request.Builder() + .url(server.url("/")) + .build()) + try { + call.execute() + fail() + } catch (expected: SSLException) { + } + } + + @Test fun `untrusted host not in insecureHosts fails with SSLException`() { + val serverCertificates = localhost() + server.useHttps(serverCertificates.sslSocketFactory(), false) + server.enqueue(MockResponse()) + + val clientCertificates = HandshakeCertificates.Builder() + .addPlatformTrustedCertificates() + .addInsecureHost("${server.hostName}2") + .build() + + val client = clientTestRule.newClientBuilder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) + .build() + + val call = client.newCall(Request.Builder() + .url(server.url("/")) + .build()) + try { + call.execute() + fail() + } catch (expected: SSLException) { + } + } +} diff --git a/okhttp/src/test/java/okhttp3/internal/tls/CertificatePinnerChainValidationTest.java b/okhttp/src/test/java/okhttp3/internal/tls/CertificatePinnerChainValidationTest.java index e434ba6ed..ffb98ad63 100644 --- a/okhttp/src/test/java/okhttp3/internal/tls/CertificatePinnerChainValidationTest.java +++ b/okhttp/src/test/java/okhttp3/internal/tls/CertificatePinnerChainValidationTest.java @@ -355,7 +355,8 @@ public final class CertificatePinnerChainValidationTest { // http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/2c1c21d11e58/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java#l596 String keystoreType = platform.isJdk9() ? "JKS" : null; X509KeyManager x509KeyManager = newKeyManager(keystoreType, heldCertificate, intermediates); - X509TrustManager trustManager = newTrustManager(keystoreType, Collections.emptyList()); + X509TrustManager trustManager = newTrustManager( + keystoreType, Collections.emptyList(), Collections.emptyList()); SSLContext sslContext = Platform.get().newSSLContext(); sslContext.init(new KeyManager[] {x509KeyManager}, new TrustManager[] {trustManager}, new SecureRandom()); diff --git a/okhttp/src/test/java/okhttp3/internal/tls/ClientAuthTest.java b/okhttp/src/test/java/okhttp3/internal/tls/ClientAuthTest.java index 95ad5033a..8b06ffbfc 100644 --- a/okhttp/src/test/java/okhttp3/internal/tls/ClientAuthTest.java +++ b/okhttp/src/test/java/okhttp3/internal/tls/ClientAuthTest.java @@ -19,6 +19,7 @@ import java.net.SocketException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.Collections; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; @@ -286,8 +287,8 @@ public final class ClientAuthTest { try { X509KeyManager keyManager = newKeyManager( null, serverCert, serverIntermediateCa.certificate()); - X509TrustManager trustManager = newTrustManager( - null, asList(serverRootCa.certificate(), clientRootCa.certificate())); + X509TrustManager trustManager = newTrustManager(null, + asList(serverRootCa.certificate(), clientRootCa.certificate()), Collections.emptyList()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(new KeyManager[] {keyManager}, new TrustManager[] {trustManager}, new SecureRandom()); diff --git a/samples/guide/src/main/java/okhttp3/recipes/kt/DevServer.kt b/samples/guide/src/main/java/okhttp3/recipes/kt/DevServer.kt new file mode 100644 index 000000000..50288f12d --- /dev/null +++ b/samples/guide/src/main/java/okhttp3/recipes/kt/DevServer.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * 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.recipes.kt + +import java.io.IOException +import java.net.HttpURLConnection.HTTP_MOVED_TEMP +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.tls.HandshakeCertificates +import okhttp3.tls.internal.TlsUtil + +class DevServer { + val handshakeCertificates = TlsUtil.localhost() + + val server = MockWebServer().apply { + useHttps(handshakeCertificates.sslSocketFactory(), false) + + enqueue(MockResponse() + .setResponseCode(HTTP_MOVED_TEMP) + .setHeader("Location", "https://www.google.com/robots.txt")) + } + + val clientCertificates = HandshakeCertificates.Builder() + .addPlatformTrustedCertificates() + .addInsecureHost(server.hostName) + .build() + + val client = OkHttpClient.Builder() + .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) + .build() + + fun run() { + try { + val request = Request.Builder() + .url(server.url("/")) + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + println(response.request.url) + } + } finally { + server.shutdown() + } + } +} + +fun main() { + DevServer().run() +} diff --git a/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerAndroid.kt b/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerAndroid.kt deleted file mode 100644 index ef483824b..000000000 --- a/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerAndroid.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (C) 2020 Square, Inc. - * - * 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.recipes.kt - -import java.io.IOException -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method -import java.net.HttpURLConnection.HTTP_MOVED_TEMP -import java.security.KeyStore -import java.security.cert.Certificate -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.internal.platform.Platform -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.tls.internal.TlsUtil - -class AndroidAllowlistedTrustManager( - private val delegate: X509TrustManager, - private vararg val hosts: String -) : X509TrustManager { - val delegateMethod = lookupDelegateMethod() - - override fun checkClientTrusted(chain: Array, authType: String?) { - delegate.checkClientTrusted(chain, authType) - } - - override fun checkServerTrusted(chain: Array, authType: String) { - throw CertificateException("Unsupported operation") - } - - /** - * Android method to clean and sort certificates, called via reflection. - */ - fun checkServerTrusted( - chain: Array, - authType: String, - host: String - ): List { - - if (isAllowed(host)) { - println("Skipping security checks for $host") - println(chain.map { it.subjectDN.name }) - - return listOf() - } - - println("Running security checks for $host") - println(chain.map { it.subjectDN.name }.take(1)) - - if (delegateMethod != null) { - return invokeDelegateMethod(delegateMethod, chain, authType, host) - } - - throw CertificateException("Failed to call checkServerTrusted") - } - - fun isAllowed(host: String): Boolean = hosts.contains(host) - - override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers - - private fun lookupDelegateMethod(): Method? { - return try { - delegate.javaClass.getMethod("checkServerTrusted", - Array::class.java, String::class.java, String::class.java) - } catch (nsme: NoSuchMethodException) { - null - } - } - - @Suppress("UNCHECKED_CAST") - private fun invokeDelegateMethod( - delegateMethod: Method, - chain: Array, - authType: String, - host: String - ): List { - try { - return delegateMethod.invoke(delegate, chain, authType, host) as List - } catch (ite: InvocationTargetException) { - throw ite.targetException - } - } -} - -class DevServerAndroid { - val handshakeCertificates = TlsUtil.localhost() - - val server = MockWebServer().apply { - useHttps(handshakeCertificates.sslSocketFactory(), false) - - enqueue(MockResponse() - .setResponseCode(HTTP_MOVED_TEMP) - .setHeader("Location", "https://www.google.com/robots.txt")) - } - - val hosts = arrayOf(server.hostName) - - val platformTrustManager = platformTrustManager() - val trustManager = AndroidAllowlistedTrustManager(platformTrustManager, *hosts) - val sslSocketFactory = Platform.get().newSSLContext().apply { - init(null, arrayOf(trustManager), null) - }.socketFactory - - val client = OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustManager) - .build() - - fun platformTrustManager(): X509TrustManager { - val factory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()) - factory.init(null as KeyStore?) - return factory.trustManagers!![0] as X509TrustManager - } - - fun run() { - try { - val request = Request.Builder() - .url(server.url("/")) - .build() - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) throw IOException("Unexpected code $response") - - println(response.request.url) - } - } finally { - server.shutdown() - } - } -} - -fun main() { - DevServerAndroid().run() -} diff --git a/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerJvm.kt b/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerJvm.kt deleted file mode 100644 index 957733d97..000000000 --- a/samples/guide/src/main/java/okhttp3/recipes/kt/DevServerJvm.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2020 Square, Inc. - * - * 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.recipes.kt - -import java.io.IOException -import java.net.HttpURLConnection.HTTP_MOVED_TEMP -import java.net.Socket -import java.security.KeyStore -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import javax.net.ssl.SSLEngine -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509ExtendedTrustManager -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.internal.platform.Platform -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.tls.internal.TlsUtil -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement - -@IgnoreJRERequirement -class JvmAllowlistedTrustManager( - private val delegate: X509ExtendedTrustManager, - private vararg val hosts: String -) : X509ExtendedTrustManager() { - override fun checkClientTrusted(chain: Array, authType: String?) { - throw CertificateException("Unsupported client operation") - } - - override fun checkClientTrusted( - chain: Array?, - authType: String?, - engine: SSLEngine? - ) { - throw CertificateException("Unsupported client operation") - } - - override fun checkClientTrusted( - chain: Array?, - authType: String?, - socket: Socket? - ) { - throw CertificateException("Unsupported client operation") - } - - override fun checkServerTrusted(chain: Array, authType: String) { - throw CertificateException("Unsupported operation") - } - - override fun checkServerTrusted( - chain: Array, - authType: String, - socket: Socket - ) { - val host = socket.inetAddress.hostName - - if (isAllowed(host)) { - println("Skipping security checks for $host") - println(chain.map { it.subjectDN.name }) - } else { - println("Running security checks for $host") - println(chain.map { it.subjectDN.name }.take(1)) - delegate.checkServerTrusted(chain, authType, socket) - } - } - - override fun checkServerTrusted( - chain: Array, - authType: String, - engine: SSLEngine - ) { - val host = engine.peerHost - - if (isAllowed(host)) { - println("Skipping security checks for $host") - println(chain.map { it.subjectDN.name }) - } else { - println("Running security checks for $host") - println(chain.map { it.subjectDN.name }.take(1)) - - delegate.checkServerTrusted(chain, authType, engine) - } - } - - fun isAllowed(host: String): Boolean = hosts.contains(host) - - override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers -} - -@IgnoreJRERequirement -class DevServerJvm { - val handshakeCertificates = TlsUtil.localhost() - - val server = MockWebServer().apply { - useHttps(handshakeCertificates.sslSocketFactory(), false) - - enqueue(MockResponse() - .setResponseCode(HTTP_MOVED_TEMP) - .setHeader("Location", "https://www.google.com/robots.txt")) - } - - val hosts = arrayOf(server.hostName) - - val platformTrustManager = platformTrustManager() - val trustManager = JvmAllowlistedTrustManager(platformTrustManager, *hosts) - val sslSocketFactory = Platform.get().newSSLContext().apply { - init(null, arrayOf(trustManager), null) - }.socketFactory - - val client = OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustManager) - .build() - - fun platformTrustManager(): X509ExtendedTrustManager { - val factory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()) - factory.init(null as KeyStore?) - return factory.trustManagers!![0] as X509ExtendedTrustManager - } - - fun run() { - try { - val request = Request.Builder() - .url(server.url("/")) - .build() - - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) throw IOException("Unexpected code $response") - - println(response.request.url) - } - } finally { - server.shutdown() - } - } -} - -fun main() { - DevServerJvm().run() -}