diff --git a/okhttp/src/main/java/okhttp3/OkHttpClient.kt b/okhttp/src/main/java/okhttp3/OkHttpClient.kt index c927a47fd..03baad19c 100644 --- a/okhttp/src/main/java/okhttp3/OkHttpClient.kt +++ b/okhttp/src/main/java/okhttp3/OkHttpClient.kt @@ -290,7 +290,7 @@ open class OkHttpClient internal constructor( internal var socketFactory: SocketFactory = SocketFactory.getDefault() internal var sslSocketFactory: SSLSocketFactory? = null internal var certificateChainCleaner: CertificateChainCleaner? = null - internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier.INSTANCE + internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT internal var proxyAuthenticator: Authenticator = Authenticator.NONE internal var authenticator: Authenticator = Authenticator.NONE diff --git a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java index f963b1fc7..f52541fdd 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java +++ b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.java @@ -347,7 +347,8 @@ public final class RealConnection extends Http2Connection.Listener implements Co "Hostname " + address.url().host() + " not verified:" + "\n certificate: " + CertificatePinner.pin(cert) + "\n DN: " + cert.getSubjectDN().getName() - + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); + + "\n subjectAltNames: " + + OkHostnameVerifier.INSTANCE.allSubjectAltNames(cert)); } else { throw new SSLPeerUnverifiedException( "Hostname " + address.url().host() + " not verified (no certificates)"); diff --git a/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.java b/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.java deleted file mode 100644 index cfd892366..000000000 --- a/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2016 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.internal.tls; - -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import javax.security.auth.x500.X500Principal; - -/** A simple index that of trusted root certificates that have been loaded into memory. */ -public final class BasicTrustRootIndex implements TrustRootIndex { - private final Map> subjectToCaCerts; - - public BasicTrustRootIndex(X509Certificate... caCerts) { - subjectToCaCerts = new LinkedHashMap<>(); - for (X509Certificate caCert : caCerts) { - X500Principal subject = caCert.getSubjectX500Principal(); - Set subjectCaCerts = subjectToCaCerts.get(subject); - if (subjectCaCerts == null) { - subjectCaCerts = new LinkedHashSet<>(1); - subjectToCaCerts.put(subject, subjectCaCerts); - } - subjectCaCerts.add(caCert); - } - } - - @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) { - X500Principal issuer = cert.getIssuerX500Principal(); - Set subjectCaCerts = subjectToCaCerts.get(issuer); - if (subjectCaCerts == null) return null; - - for (X509Certificate caCert : subjectCaCerts) { - PublicKey publicKey = caCert.getPublicKey(); - try { - cert.verify(publicKey); - return caCert; - } catch (Exception ignored) { - } - } - - return null; - } - - @Override public boolean equals(Object other) { - if (other == this) return true; - return other instanceof okhttp3.internal.tls.BasicTrustRootIndex - && ((okhttp3.internal.tls.BasicTrustRootIndex) other).subjectToCaCerts.equals( - subjectToCaCerts); - } - - @Override public int hashCode() { - return subjectToCaCerts.hashCode(); - } -} diff --git a/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.kt b/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.kt new file mode 100644 index 000000000..2b93613b8 --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/tls/BasicTrustRootIndex.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 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.internal.tls + +import java.security.cert.X509Certificate +import javax.security.auth.x500.X500Principal + +/** A simple index that of trusted root certificates that have been loaded into memory. */ +class BasicTrustRootIndex(vararg caCerts: X509Certificate) : TrustRootIndex { + private val subjectToCaCerts: Map> + + init { + val map = mutableMapOf>() + for (caCert in caCerts) { + map.getOrPut(caCert.subjectX500Principal) { mutableSetOf() }.add(caCert) + } + this.subjectToCaCerts = map + } + + override fun findByIssuerAndSignature(cert: X509Certificate): X509Certificate? { + val issuer = cert.issuerX500Principal + val subjectCaCerts = subjectToCaCerts[issuer] ?: return null + + return subjectCaCerts.firstOrNull { + try { + cert.verify(it.publicKey) + return@firstOrNull true + } catch (_: Exception) { + return@firstOrNull false + } + } + } + + override fun equals(other: Any?): Boolean { + return other === this || + (other is BasicTrustRootIndex && other.subjectToCaCerts == subjectToCaCerts) + } + + override fun hashCode(): Int { + return subjectToCaCerts.hashCode() + } +} diff --git a/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java b/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java deleted file mode 100644 index 36ac5b381..000000000 --- a/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.internal.tls; - -import java.security.cert.Certificate; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; - -import static okhttp3.internal.Util.verifyAsIpAddress; - -/** - * A HostnameVerifier consistent with RFC 2818. - */ -public final class OkHostnameVerifier implements HostnameVerifier { - public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier(); - - private static final int ALT_DNS_NAME = 2; - private static final int ALT_IPA_NAME = 7; - - private OkHostnameVerifier() { - } - - @Override - public boolean verify(String host, SSLSession session) { - try { - Certificate[] certificates = session.getPeerCertificates(); - return verify(host, (X509Certificate) certificates[0]); - } catch (SSLException e) { - return false; - } - } - - public boolean verify(String host, X509Certificate certificate) { - return verifyAsIpAddress(host) - ? verifyIpAddress(host, certificate) - : verifyHostname(host, certificate); - } - - /** Returns true if {@code certificate} matches {@code ipAddress}. */ - private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { - List altNames = getSubjectAltNames(certificate, ALT_IPA_NAME); - for (int i = 0, size = altNames.size(); i < size; i++) { - if (ipAddress.equalsIgnoreCase(altNames.get(i))) { - return true; - } - } - return false; - } - - /** Returns true if {@code certificate} matches {@code hostname}. */ - private boolean verifyHostname(String hostname, X509Certificate certificate) { - hostname = hostname.toLowerCase(Locale.US); - List altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); - for (String altName : altNames) { - if (verifyHostname(hostname, altName)) { - return true; - } - } - return false; - } - - public static List allSubjectAltNames(X509Certificate certificate) { - List altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME); - List altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME); - List result = new ArrayList<>(altIpaNames.size() + altDnsNames.size()); - result.addAll(altIpaNames); - result.addAll(altDnsNames); - return result; - } - - private static List getSubjectAltNames(X509Certificate certificate, int type) { - List result = new ArrayList<>(); - try { - Collection subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames == null) { - return Collections.emptyList(); - } - for (Object subjectAltName : subjectAltNames) { - List entry = (List) subjectAltName; - if (entry == null || entry.size() < 2) { - continue; - } - Integer altNameType = (Integer) entry.get(0); - if (altNameType == null) { - continue; - } - if (altNameType == type) { - String altName = (String) entry.get(1); - if (altName != null) { - result.add(altName); - } - } - } - return result; - } catch (CertificateParsingException e) { - return Collections.emptyList(); - } - } - - /** - * Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}. - * - * @param hostname lower-case host name. - * @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code - * *.android.com}. - */ - public boolean verifyHostname(String hostname, String pattern) { - // Basic sanity checks - // Check length == 0 instead of .isEmpty() to support Java 5. - if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith(".")) - || (hostname.endsWith(".."))) { - // Invalid domain name - return false; - } - if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith(".")) - || (pattern.endsWith(".."))) { - // Invalid pattern/domain name - return false; - } - - // Normalize hostname and pattern by turning them into absolute domain names if they are not - // yet absolute. This is needed because server certificates do not normally contain absolute - // names or patterns, but they should be treated as absolute. At the same time, any hostname - // presented to this method should also be treated as absolute for the purposes of matching - // to the server certificate. - // www.android.com matches www.android.com - // www.android.com matches www.android.com. - // www.android.com. matches www.android.com. - // www.android.com. matches www.android.com - if (!hostname.endsWith(".")) { - hostname += '.'; - } - if (!pattern.endsWith(".")) { - pattern += '.'; - } - // hostname and pattern are now absolute domain names. - - pattern = pattern.toLowerCase(Locale.US); - // hostname and pattern are now in lower case -- domain names are case-insensitive. - - if (!pattern.contains("*")) { - // Not a wildcard pattern -- hostname and pattern must match exactly. - return hostname.equals(pattern); - } - // Wildcard pattern - - // WILDCARD PATTERN RULES: - // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the - // only character in that label (i.e., must match the whole left-most label). - // For example, *.example.com is permitted, while *a.example.com, a*.example.com, - // a*b.example.com, a.*.example.com are not permitted. - // 2. Asterisk (*) cannot match across domain name labels. - // For example, *.example.com matches test.example.com but does not match - // sub.test.example.com. - // 3. Wildcard patterns for single-label domain names are not permitted. - - if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) { - // Asterisk (*) is only permitted in the left-most domain name label and must be the only - // character in that label - return false; - } - - // Optimization: check whether hostname is too short to match the pattern. hostName must be at - // least as long as the pattern because asterisk must match the whole left-most label and - // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters. - if (hostname.length() < pattern.length()) { - // hostname too short to match the pattern. - return false; - } - - if ("*.".equals(pattern)) { - // Wildcard pattern for single-label domain name -- not permitted. - return false; - } - - // hostname must end with the region of pattern following the asterisk. - String suffix = pattern.substring(1); - if (!hostname.endsWith(suffix)) { - // hostname does not end with the suffix - return false; - } - - // Check that asterisk did not match across domain name labels. - int suffixStartIndexInHostname = hostname.length() - suffix.length(); - if ((suffixStartIndexInHostname > 0) - && (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) { - // Asterisk is matching across domain name labels -- not permitted. - return false; - } - - // hostname matches pattern - return true; - } -} diff --git a/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.kt b/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.kt new file mode 100644 index 000000000..0ca517283 --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/tls/OkHostnameVerifier.kt @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.internal.tls + +import okhttp3.internal.Util.verifyAsIpAddress +import java.security.cert.CertificateParsingException +import java.security.cert.X509Certificate +import java.util.Locale +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLException +import javax.net.ssl.SSLSession + +/** + * A HostnameVerifier consistent with [RFC 2818][rfc_2818]. + * + * [rfc_2818]: http://www.ietf.org/rfc/rfc2818.txt + */ +object OkHostnameVerifier : HostnameVerifier { + private const val ALT_DNS_NAME = 2 + private const val ALT_IPA_NAME = 7 + + override fun verify(host: String, session: SSLSession): Boolean { + return try { + verify(host, session.peerCertificates[0] as X509Certificate) + } catch (_: SSLException) { + false + } + } + + fun verify(host: String, certificate: X509Certificate): Boolean { + return when { + verifyAsIpAddress(host) -> verifyIpAddress(host, certificate) + else -> verifyHostname(host, certificate) + } + } + + /** Returns true if `certificate` matches `ipAddress`. */ + private fun verifyIpAddress(ipAddress: String, certificate: X509Certificate): Boolean { + return getSubjectAltNames(certificate, ALT_IPA_NAME).any { + ipAddress.equals(it, ignoreCase = true) + } + } + + /** Returns true if `certificate` matches `hostname`. */ + private fun verifyHostname(hostname: String, certificate: X509Certificate): Boolean { + val hostname = hostname.toLowerCase(Locale.US) + return getSubjectAltNames(certificate, ALT_DNS_NAME).any { + verifyHostname(hostname, it) + } + } + + /** + * Returns true if [hostname] matches the domain name [pattern]. + * + * @param hostname lower-case host name. + * @param pattern domain name pattern from certificate. May be a wildcard pattern such as + * `*.android.com`. + */ + fun verifyHostname(hostname: String?, pattern: String?): Boolean { + var hostname = hostname + var pattern = pattern + // Basic sanity checks + if (hostname.isNullOrEmpty() || + hostname.startsWith(".") || + hostname.endsWith("..")) { + // Invalid domain name + return false + } + if (pattern.isNullOrEmpty() || + pattern.startsWith(".") || + pattern.endsWith("..")) { + // Invalid pattern/domain name + return false + } + + // Normalize hostname and pattern by turning them into absolute domain names if they are not + // yet absolute. This is needed because server certificates do not normally contain absolute + // names or patterns, but they should be treated as absolute. At the same time, any hostname + // presented to this method should also be treated as absolute for the purposes of matching + // to the server certificate. + // www.android.com matches www.android.com + // www.android.com matches www.android.com. + // www.android.com. matches www.android.com. + // www.android.com. matches www.android.com + if (!hostname.endsWith(".")) { + hostname += "." + } + if (!pattern.endsWith(".")) { + pattern += "." + } + // Hostname and pattern are now absolute domain names. + + pattern = pattern.toLowerCase(Locale.US) + // Hostname and pattern are now in lower case -- domain names are case-insensitive. + + if ("*" !in pattern) { + // Not a wildcard pattern -- hostname and pattern must match exactly. + return hostname == pattern + } + + // Wildcard pattern + + // WILDCARD PATTERN RULES: + // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the + // only character in that label (i.e., must match the whole left-most label). + // For example, *.example.com is permitted, while *a.example.com, a*.example.com, + // a*b.example.com, a.*.example.com are not permitted. + // 2. Asterisk (*) cannot match across domain name labels. + // For example, *.example.com matches test.example.com but does not match + // sub.test.example.com. + // 3. Wildcard patterns for single-label domain names are not permitted. + + if (!pattern.startsWith("*.") || pattern.indexOf('*', 1) != -1) { + // Asterisk (*) is only permitted in the left-most domain name label and must be the only + // character in that label + return false + } + + // Optimization: check whether hostname is too short to match the pattern. hostName must be at + // least as long as the pattern because asterisk must match the whole left-most label and + // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters. + if (hostname.length < pattern.length) { + return false // Hostname too short to match the pattern. + } + + if ("*." == pattern) { + return false // Wildcard pattern for single-label domain name -- not permitted. + } + + // Hostname must end with the region of pattern following the asterisk. + val suffix = pattern.substring(1) + if (!hostname.endsWith(suffix)) { + return false // Hostname does not end with the suffix. + } + + // Check that asterisk did not match across domain name labels. + val suffixStartIndexInHostname = hostname.length - suffix.length + if (suffixStartIndexInHostname > 0 && + hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1) { + return false // Asterisk is matching across domain name labels -- not permitted. + } + + // Hostname matches pattern. + return true + } + + fun allSubjectAltNames(certificate: X509Certificate): List { + val altIpaNames = getSubjectAltNames(certificate, OkHostnameVerifier.ALT_IPA_NAME) + val altDnsNames = getSubjectAltNames(certificate, OkHostnameVerifier.ALT_DNS_NAME) + return altIpaNames + altDnsNames + } + + private fun getSubjectAltNames(certificate: X509Certificate, type: Int): List { + try { + val subjectAltNames = certificate.subjectAlternativeNames ?: return emptyList() + val result = mutableListOf() + for (subjectAltName in subjectAltNames) { + if (subjectAltName == null || subjectAltName.size < 2) continue + if (subjectAltName[0] != type) continue + val altName = subjectAltName[1] ?: continue + result.add(altName as String) + } + return result + } catch (_: CertificateParsingException) { + return emptyList() + } + } +} diff --git a/okhttp/src/test/java/okhttp3/KotlinSourceCompatibilityTest.kt b/okhttp/src/test/java/okhttp3/KotlinSourceCompatibilityTest.kt index 156dac1d0..9afea026a 100644 --- a/okhttp/src/test/java/okhttp3/KotlinSourceCompatibilityTest.kt +++ b/okhttp/src/test/java/okhttp3/KotlinSourceCompatibilityTest.kt @@ -866,7 +866,7 @@ class KotlinSourceCompatibilityTest { builder = builder.dns(Dns.SYSTEM) builder = builder.socketFactory(SocketFactory.getDefault()) builder = builder.sslSocketFactory(localhost().sslSocketFactory(), localhost().trustManager()) - builder = builder.hostnameVerifier(OkHostnameVerifier.INSTANCE) + builder = builder.hostnameVerifier(OkHostnameVerifier) builder = builder.certificatePinner(CertificatePinner.DEFAULT) builder = builder.authenticator(Authenticator.NONE) builder = builder.proxyAuthenticator(Authenticator.NONE) @@ -1161,7 +1161,7 @@ class KotlinSourceCompatibilityTest { Dns.SYSTEM, SocketFactory.getDefault(), localhost().sslSocketFactory(), - OkHostnameVerifier.INSTANCE, + OkHostnameVerifier, CertificatePinner.DEFAULT, Authenticator.NONE, Proxy.NO_PROXY,