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; 32 import javax.security.auth.x500.X500Principal; 33 34 /** 35 * A HostnameVerifier consistent with <a 36 * href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>. 37 */ 38 public final class OkHostnameVerifier implements HostnameVerifier { 39 public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier(); 40 41 /** 42 * Quick and dirty pattern to differentiate IP addresses from hostnames. This 43 * is an approximation of Android's private InetAddress#isNumeric API. 44 * 45 * <p>This matches IPv6 addresses as a hex string containing at least one 46 * colon, and possibly including dots after the first colon. It matches IPv4 47 * addresses as strings containing only decimal digits and dots. This pattern 48 * matches strings like "a:.23" and "54" that are neither IP addresses nor 49 * hostnames; they will be verified as IP addresses (which is a more strict 50 * verification). 51 */ 52 private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile( 53 "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)"); 54 55 private static final int ALT_DNS_NAME = 2; 56 private static final int ALT_IPA_NAME = 7; 57 OkHostnameVerifier()58 private OkHostnameVerifier() { 59 } 60 61 @Override verify(String host, SSLSession session)62 public boolean verify(String host, SSLSession session) { 63 try { 64 Certificate[] certificates = session.getPeerCertificates(); 65 return verify(host, (X509Certificate) certificates[0]); 66 } catch (SSLException e) { 67 return false; 68 } 69 } 70 verify(String host, X509Certificate certificate)71 public boolean verify(String host, X509Certificate certificate) { 72 return verifyAsIpAddress(host) 73 ? verifyIpAddress(host, certificate) 74 : verifyHostName(host, certificate); 75 } 76 verifyAsIpAddress(String host)77 static boolean verifyAsIpAddress(String host) { 78 return VERIFY_AS_IP_ADDRESS.matcher(host).matches(); 79 } 80 81 /** 82 * Returns true if {@code certificate} matches {@code ipAddress}. 83 */ verifyIpAddress(String ipAddress, X509Certificate certificate)84 private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { 85 List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME); 86 for (int i = 0, size = altNames.size(); i < size; i++) { 87 if (ipAddress.equalsIgnoreCase(altNames.get(i))) { 88 return true; 89 } 90 } 91 return false; 92 } 93 94 /** 95 * Returns true if {@code certificate} matches {@code hostName}. 96 */ verifyHostName(String hostName, X509Certificate certificate)97 private boolean verifyHostName(String hostName, X509Certificate certificate) { 98 hostName = hostName.toLowerCase(Locale.US); 99 boolean hasDns = false; 100 List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); 101 for (int i = 0, size = altNames.size(); i < size; i++) { 102 hasDns = true; 103 if (verifyHostName(hostName, altNames.get(i))) { 104 return true; 105 } 106 } 107 108 if (!hasDns) { 109 X500Principal principal = certificate.getSubjectX500Principal(); 110 // RFC 2818 advises using the most specific name for matching. 111 String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); 112 if (cn != null) { 113 return verifyHostName(hostName, cn); 114 } 115 } 116 117 return false; 118 } 119 allSubjectAltNames(X509Certificate certificate)120 public static List<String> allSubjectAltNames(X509Certificate certificate) { 121 List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME); 122 List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME); 123 List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size()); 124 result.addAll(altIpaNames); 125 result.addAll(altDnsNames); 126 return result; 127 } 128 getSubjectAltNames(X509Certificate certificate, int type)129 private static List<String> getSubjectAltNames(X509Certificate certificate, int type) { 130 List<String> result = new ArrayList<>(); 131 try { 132 Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames(); 133 if (subjectAltNames == null) { 134 return Collections.emptyList(); 135 } 136 for (Object subjectAltName : subjectAltNames) { 137 List<?> entry = (List<?>) subjectAltName; 138 if (entry == null || entry.size() < 2) { 139 continue; 140 } 141 Integer altNameType = (Integer) entry.get(0); 142 if (altNameType == null) { 143 continue; 144 } 145 if (altNameType == type) { 146 String altName = (String) entry.get(1); 147 if (altName != null) { 148 result.add(altName); 149 } 150 } 151 } 152 return result; 153 } catch (CertificateParsingException e) { 154 return Collections.emptyList(); 155 } 156 } 157 158 /** 159 * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern}. 160 * 161 * @param hostName lower-case host name. 162 * @param pattern domain name pattern from certificate. May be a wildcard pattern such as 163 * {@code *.android.com}. 164 */ verifyHostName(String hostName, String pattern)165 private boolean verifyHostName(String hostName, String pattern) { 166 // Basic sanity checks 167 // Check length == 0 instead of .isEmpty() to support Java 5. 168 if ((hostName == null) || (hostName.length() == 0) || (hostName.startsWith(".")) 169 || (hostName.endsWith(".."))) { 170 // Invalid domain name 171 return false; 172 } 173 if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith(".")) 174 || (pattern.endsWith(".."))) { 175 // Invalid pattern/domain name 176 return false; 177 } 178 179 // Normalize hostName and pattern by turning them into absolute domain names if they are not 180 // yet absolute. This is needed because server certificates do not normally contain absolute 181 // names or patterns, but they should be treated as absolute. At the same time, any hostName 182 // presented to this method should also be treated as absolute for the purposes of matching 183 // to the server certificate. 184 // www.android.com matches www.android.com 185 // www.android.com matches www.android.com. 186 // www.android.com. matches www.android.com. 187 // www.android.com. matches www.android.com 188 if (!hostName.endsWith(".")) { 189 hostName += '.'; 190 } 191 if (!pattern.endsWith(".")) { 192 pattern += '.'; 193 } 194 // hostName and pattern are now absolute domain names. 195 196 pattern = pattern.toLowerCase(Locale.US); 197 // hostName and pattern are now in lower case -- domain names are case-insensitive. 198 199 if (!pattern.contains("*")) { 200 // Not a wildcard pattern -- hostName and pattern must match exactly. 201 return hostName.equals(pattern); 202 } 203 // Wildcard pattern 204 205 // WILDCARD PATTERN RULES: 206 // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the 207 // only character in that label (i.e., must match the whole left-most label). 208 // For example, *.example.com is permitted, while *a.example.com, a*.example.com, 209 // a*b.example.com, a.*.example.com are not permitted. 210 // 2. Asterisk (*) cannot match across domain name labels. 211 // For example, *.example.com matches test.example.com but does not match 212 // sub.test.example.com. 213 // 3. Wildcard patterns for single-label domain names are not permitted. 214 215 if ((!pattern.startsWith("*.")) || (pattern.indexOf('*', 1) != -1)) { 216 // Asterisk (*) is only permitted in the left-most domain name label and must be the only 217 // character in that label 218 return false; 219 } 220 221 // Optimization: check whether hostName is too short to match the pattern. hostName must be at 222 // least as long as the pattern because asterisk must match the whole left-most label and 223 // hostName starts with a non-empty label. Thus, asterisk has to match one or more characters. 224 if (hostName.length() < pattern.length()) { 225 // hostName too short to match the pattern. 226 return false; 227 } 228 229 if ("*.".equals(pattern)) { 230 // Wildcard pattern for single-label domain name -- not permitted. 231 return false; 232 } 233 234 // hostName must end with the region of pattern following the asterisk. 235 String suffix = pattern.substring(1); 236 if (!hostName.endsWith(suffix)) { 237 // hostName does not end with the suffix 238 return false; 239 } 240 241 // Check that asterisk did not match across domain name labels. 242 int suffixStartIndexInHostName = hostName.length() - suffix.length(); 243 if ((suffixStartIndexInHostName > 0) 244 && (hostName.lastIndexOf('.', suffixStartIndexInHostName - 1) != -1)) { 245 // Asterisk is matching across domain name labels -- not permitted. 246 return false; 247 } 248 249 // hostName matches pattern 250 return true; 251 } 252 } 253