Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | * contributor license agreements. See the NOTICE file distributed with |
| 4 | * this work for additional information regarding copyright ownership. |
| 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | * (the "License"); you may not use this file except in compliance with |
| 7 | * the License. You may obtain a copy of the License at |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | */ |
| 17 | |
| 18 | package com.squareup.okhttp.internal.tls; |
| 19 | |
| 20 | import java.security.cert.Certificate; |
| 21 | import java.security.cert.CertificateParsingException; |
| 22 | import java.security.cert.X509Certificate; |
| 23 | import java.util.ArrayList; |
| 24 | import java.util.Collection; |
| 25 | import java.util.Collections; |
| 26 | import java.util.List; |
| 27 | import java.util.Locale; |
| 28 | import java.util.regex.Pattern; |
| 29 | import javax.net.ssl.HostnameVerifier; |
| 30 | import javax.net.ssl.SSLException; |
| 31 | import javax.net.ssl.SSLSession; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 32 | |
| 33 | /** |
| 34 | * A HostnameVerifier consistent with <a |
| 35 | * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>. |
| 36 | */ |
| 37 | public final class OkHostnameVerifier implements HostnameVerifier { |
jwilson | 7462358 | 2013-06-23 21:47:59 -0400 | [diff] [blame] | 38 | public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier(); |
| 39 | |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 40 | /** |
| 41 | * Quick and dirty pattern to differentiate IP addresses from hostnames. This |
| 42 | * is an approximation of Android's private InetAddress#isNumeric API. |
| 43 | * |
| 44 | * <p>This matches IPv6 addresses as a hex string containing at least one |
| 45 | * colon, and possibly including dots after the first colon. It matches IPv4 |
| 46 | * addresses as strings containing only decimal digits and dots. This pattern |
| 47 | * matches strings like "a:.23" and "54" that are neither IP addresses nor |
| 48 | * hostnames; they will be verified as IP addresses (which is a more strict |
| 49 | * verification). |
| 50 | */ |
| 51 | private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile( |
| 52 | "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)"); |
| 53 | |
| 54 | private static final int ALT_DNS_NAME = 2; |
| 55 | private static final int ALT_IPA_NAME = 7; |
| 56 | |
jwilson | 7462358 | 2013-06-23 21:47:59 -0400 | [diff] [blame] | 57 | private OkHostnameVerifier() { |
| 58 | } |
| 59 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 60 | @Override |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 61 | public boolean verify(String host, SSLSession session) { |
| 62 | try { |
| 63 | Certificate[] certificates = session.getPeerCertificates(); |
| 64 | return verify(host, (X509Certificate) certificates[0]); |
| 65 | } catch (SSLException e) { |
| 66 | return false; |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | public boolean verify(String host, X509Certificate certificate) { |
| 71 | return verifyAsIpAddress(host) |
| 72 | ? verifyIpAddress(host, certificate) |
| 73 | : verifyHostName(host, certificate); |
| 74 | } |
| 75 | |
| 76 | static boolean verifyAsIpAddress(String host) { |
| 77 | return VERIFY_AS_IP_ADDRESS.matcher(host).matches(); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Returns true if {@code certificate} matches {@code ipAddress}. |
| 82 | */ |
| 83 | private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { |
Neil Fuller | e78f117 | 2015-01-20 09:39:41 +0000 | [diff] [blame] | 84 | List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME); |
| 85 | for (int i = 0, size = altNames.size(); i < size; i++) { |
| 86 | if (ipAddress.equalsIgnoreCase(altNames.get(i))) { |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 87 | return true; |
| 88 | } |
| 89 | } |
| 90 | return false; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Returns true if {@code certificate} matches {@code hostName}. |
| 95 | */ |
| 96 | private boolean verifyHostName(String hostName, X509Certificate certificate) { |
| 97 | hostName = hostName.toLowerCase(Locale.US); |
| 98 | boolean hasDns = false; |
Neil Fuller | e78f117 | 2015-01-20 09:39:41 +0000 | [diff] [blame] | 99 | List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); |
| 100 | for (int i = 0, size = altNames.size(); i < size; i++) { |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 101 | hasDns = true; |
Neil Fuller | e78f117 | 2015-01-20 09:39:41 +0000 | [diff] [blame] | 102 | if (verifyHostName(hostName, altNames.get(i))) { |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 103 | return true; |
| 104 | } |
| 105 | } |
| 106 | |
Tobias Thierer | cdbc32f | 2018-01-11 14:56:44 +0000 | [diff] [blame] | 107 | // BEGIN Android-removed: Ignore common name in hostname verification. http://b/70278814 |
| 108 | /* |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 109 | if (!hasDns) { |
| 110 | X500Principal principal = certificate.getSubjectX500Principal(); |
| 111 | // RFC 2818 advises using the most specific name for matching. |
| 112 | String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); |
| 113 | if (cn != null) { |
| 114 | return verifyHostName(hostName, cn); |
| 115 | } |
| 116 | } |
Tobias Thierer | cdbc32f | 2018-01-11 14:56:44 +0000 | [diff] [blame] | 117 | */ |
| 118 | // END Android-removed: Ignore common name in hostname verification. http://b/70278814 |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 119 | |
| 120 | return false; |
| 121 | } |
| 122 | |
Neil Fuller | e78f117 | 2015-01-20 09:39:41 +0000 | [diff] [blame] | 123 | public static List<String> allSubjectAltNames(X509Certificate certificate) { |
| 124 | List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME); |
| 125 | List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME); |
| 126 | List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size()); |
| 127 | result.addAll(altIpaNames); |
| 128 | result.addAll(altDnsNames); |
| 129 | return result; |
| 130 | } |
| 131 | |
| 132 | private static List<String> getSubjectAltNames(X509Certificate certificate, int type) { |
| 133 | List<String> result = new ArrayList<>(); |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 134 | try { |
| 135 | Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames(); |
| 136 | if (subjectAltNames == null) { |
| 137 | return Collections.emptyList(); |
| 138 | } |
| 139 | for (Object subjectAltName : subjectAltNames) { |
| 140 | List<?> entry = (List<?>) subjectAltName; |
| 141 | if (entry == null || entry.size() < 2) { |
| 142 | continue; |
| 143 | } |
| 144 | Integer altNameType = (Integer) entry.get(0); |
| 145 | if (altNameType == null) { |
| 146 | continue; |
| 147 | } |
| 148 | if (altNameType == type) { |
| 149 | String altName = (String) entry.get(1); |
| 150 | if (altName != null) { |
| 151 | result.add(altName); |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | return result; |
| 156 | } catch (CertificateParsingException e) { |
| 157 | return Collections.emptyList(); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /** |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 162 | * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}. |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 163 | * |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 164 | * @param hostName lower-case host name. |
| 165 | * @param pattern domain name pattern from certificate. May be a wildcard pattern such as |
| 166 | * {@code *.android.com}. |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 167 | */ |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 168 | private boolean verifyHostName(String hostName, String pattern) { |
| 169 | // Basic sanity checks |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 170 | // Check length == 0 instead of .isEmpty() to support Java 5. |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 171 | if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith(".")) |
| 172 | || (hostName.endsWith(".."))) { |
| 173 | // Invalid domain name |
| 174 | return false; |
| 175 | } |
| 176 | if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith(".")) |
| 177 | || (pattern.endsWith(".."))) { |
| 178 | // Invalid pattern/domain name |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 179 | return false; |
| 180 | } |
| 181 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 182 | // Normalize hostName and pattern by turning them into absolute domain names if they are not |
| 183 | // yet absolute. This is needed because server certificates do not normally contain absolute |
| 184 | // names or patterns, but they should be treated as absolute. At the same time, any hostName |
| 185 | // presented to this method should also be treated as absolute for the purposes of matching |
| 186 | // to the server certificate. |
| 187 | // www.android.com matches www.android.com |
| 188 | // www.android.com matches www.android.com. |
| 189 | // www.android.com. matches www.android.com. |
| 190 | // www.android.com. matches www.android.com |
| 191 | if (!hostName.endsWith(".")) { |
| 192 | hostName += '.'; |
| 193 | } |
| 194 | if (!pattern.endsWith(".")) { |
| 195 | pattern += '.'; |
| 196 | } |
| 197 | // hostName and pattern are now absolute domain names. |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 198 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 199 | pattern = pattern.toLowerCase(Locale.US); |
| 200 | // hostName and pattern are now in lower case -- domain names are case-insensitive. |
| 201 | |
| 202 | if (!pattern.contains("*")) { |
| 203 | // Not a wildcard pattern -- hostName and pattern must match exactly. |
| 204 | return hostName.equals(pattern); |
| 205 | } |
| 206 | // Wildcard pattern |
| 207 | |
| 208 | // WILDCARD PATTERN RULES: |
| 209 | // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the |
| 210 | // only character in that label (i.e., must match the whole left-most label). |
| 211 | // For example, *.example.com is permitted, while *a.example.com, a*.example.com, |
| 212 | // a*b.example.com, a.*.example.com are not permitted. |
| 213 | // 2. Asterisk (*) cannot match across domain name labels. |
| 214 | // For example, *.example.com matches test.example.com but does not match |
| 215 | // sub.test.example.com. |
| 216 | // 3. Wildcard patterns for single-label domain names are not permitted. |
| 217 | |
| 218 | if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) { |
| 219 | // Asterisk (*) is only permitted in the left-most domain name label and must be the only |
| 220 | // character in that label |
| 221 | return false; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 222 | } |
| 223 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 224 | // Optimization: check whether hostName is too short to match the pattern. hostName must be at |
| 225 | // least as long as the pattern because asterisk must match the whole left-most label and |
| 226 | // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters. |
| 227 | if (hostName.length() < pattern.length()) { |
| 228 | // hostName too short to match the pattern. |
| 229 | return false; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 230 | } |
| 231 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 232 | if ("*.".equals(pattern)) { |
| 233 | // Wildcard pattern for single-label domain name -- not permitted. |
| 234 | return false; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 235 | } |
| 236 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 237 | // hostName must end with the region of pattern following the asterisk. |
| 238 | String suffix = pattern.substring(1); |
| 239 | if (!hostName.endsWith(suffix)) { |
| 240 | // hostName does not end with the suffix |
| 241 | return false; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 242 | } |
| 243 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 244 | // Check that asterisk did not match across domain name labels. |
| 245 | int suffixStartIndexInHostName = hostName.length() - suffix.length(); |
| 246 | if ((suffixStartIndexInHostName > 0) |
| 247 | && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) { |
| 248 | // Asterisk is matching across domain name labels -- not permitted. |
| 249 | return false; |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 250 | } |
| 251 | |
Alex Klyubin | 5759201 | 2014-12-02 12:51:25 -0800 | [diff] [blame] | 252 | // hostName matches pattern |
Narayan Kamath | faf4972 | 2013-06-06 10:17:55 +0100 | [diff] [blame] | 253 | return true; |
| 254 | } |
| 255 | } |