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 58 private OkHostnameVerifier() { 59 } 60 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) { 84 for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) { 85 if (ipAddress.equalsIgnoreCase(altName)) { 86 return true; 87 } 88 } 89 return false; 90 } 91 92 /** 93 * Returns true if {@code certificate} matches {@code hostName}. 94 */ 95 private boolean verifyHostName(String hostName, X509Certificate certificate) { 96 hostName = hostName.toLowerCase(Locale.US); 97 boolean hasDns = false; 98 for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) { 99 hasDns = true; 100 if (verifyHostName(hostName, altName)) { 101 return true; 102 } 103 } 104 105 if (!hasDns) { 106 X500Principal principal = certificate.getSubjectX500Principal(); 107 // RFC 2818 advises using the most specific name for matching. 108 String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); 109 if (cn != null) { 110 return verifyHostName(hostName, cn); 111 } 112 } 113 114 return false; 115 } 116 117 private List<String> getSubjectAltNames(X509Certificate certificate, int type) { 118 List<String> result = new ArrayList<String>(); 119 try { 120 Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames(); 121 if (subjectAltNames == null) { 122 return Collections.emptyList(); 123 } 124 for (Object subjectAltName : subjectAltNames) { 125 List<?> entry = (List<?>) subjectAltName; 126 if (entry == null || entry.size() < 2) { 127 continue; 128 } 129 Integer altNameType = (Integer) entry.get(0); 130 if (altNameType == null) { 131 continue; 132 } 133 if (altNameType == type) { 134 String altName = (String) entry.get(1); 135 if (altName != null) { 136 result.add(altName); 137 } 138 } 139 } 140 return result; 141 } catch (CertificateParsingException e) { 142 return Collections.emptyList(); 143 } 144 } 145 146 /** 147 * Returns true if {@code hostName} matches the name or pattern {@code cn}. 148 * 149 * @param hostName lowercase host name. 150 * @param cn certificate host name. May include wildcards like 151 * {@code *.android.com}. 152 */ 153 public boolean verifyHostName(String hostName, String cn) { 154 // Check length == 0 instead of .isEmpty() to support Java 5. 155 if (hostName == null || hostName.length() == 0 || cn == null || cn.length() == 0) { 156 return false; 157 } 158 159 cn = cn.toLowerCase(Locale.US); 160 161 if (!cn.contains("*")) { 162 return hostName.equals(cn); 163 } 164 165 if (cn.startsWith("*.") && hostName.regionMatches(0, cn, 2, cn.length() - 2)) { 166 return true; // "*.foo.com" matches "foo.com" 167 } 168 169 int asterisk = cn.indexOf('*'); 170 int dot = cn.indexOf('.'); 171 if (asterisk > dot) { 172 return false; // malformed; wildcard must be in the first part of the cn 173 } 174 175 if (!hostName.regionMatches(0, cn, 0, asterisk)) { 176 return false; // prefix before '*' doesn't match 177 } 178 179 int suffixLength = cn.length() - (asterisk + 1); 180 int suffixStart = hostName.length() - suffixLength; 181 if (hostName.indexOf('.', asterisk) < suffixStart) { 182 // TODO: remove workaround for *.clients.google.com http://b/5426333 183 if (!hostName.endsWith(".clients.google.com")) { 184 return false; // wildcard '*' can't match a '.' 185 } 186 } 187 188 if (!hostName.regionMatches(suffixStart, cn, asterisk + 1, suffixLength)) { 189 return false; // suffix after '*' doesn't match 190 } 191 192 return true; 193 } 194 } 195