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 javax.net.ssl; 19 20 import java.net.InetAddress; 21 import java.security.cert.Certificate; 22 import java.security.cert.CertificateParsingException; 23 import java.security.cert.X509Certificate; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collection; 27 import java.util.Collections; 28 import java.util.List; 29 import java.util.Locale; 30 31 /** 32 * A HostnameVerifier that works the same way as Curl and Firefox. 33 * 34 * <p>The hostname must match either the first CN, or any of the subject-alts. 35 * A wildcard can occur in the CN, and in any of the subject-alts. 36 * 37 * @author Julius Davies 38 */ 39 class DefaultHostnameVerifier implements HostnameVerifier { 40 41 /** 42 * This contains a list of 2nd-level domains that aren't allowed to 43 * have wildcards when combined with country-codes. 44 * For example: [*.co.uk]. 45 * 46 * <p>The [*.co.uk] problem is an interesting one. Should we just hope 47 * that CA's would never foolishly allow such a certificate to happen? 48 * Looks like we're the only implementation guarding against this. 49 * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. 50 */ 51 private static final String[] BAD_COUNTRY_2LDS = 52 { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", 53 "lg", "ne", "net", "or", "org" }; 54 55 static { 56 // Just in case developer forgot to manually sort the array. :-) 57 Arrays.sort(BAD_COUNTRY_2LDS); 58 } 59 60 public final boolean verify(String host, SSLSession session) { 61 Certificate[] certs; 62 try { 63 certs = session.getPeerCertificates(); 64 } catch (SSLException e) { 65 return false; 66 } 67 68 X509Certificate x509 = (X509Certificate) certs[0]; 69 70 // We can be case-insensitive when comparing the host we used to 71 // establish the socket to the hostname in the certificate. 72 String hostName = host.trim().toLowerCase(Locale.ENGLISH); 73 74 // Verify the first CN provided. Other CNs are ignored. Firefox, wget, 75 // curl, and Sun Java work this way. 76 String firstCn = getFirstCn(x509); 77 if (matches(hostName, firstCn)) { 78 return true; 79 } 80 81 for (String cn : getDNSSubjectAlts(x509)) { 82 if (matches(hostName, cn)) { 83 return true; 84 } 85 } 86 87 return false; 88 } 89 90 /** 91 * Returns true if {@code hostname} matches {@code cn}. 92 * 93 * @param hostName a trimmed, lowercase hostname to verify 94 * @param cn a certificate CN or DNS subject alt. Either a literal name or 95 * a wildcard of the form "*.google.com". 96 */ 97 private boolean matches(String hostName, String cn) { 98 if (cn == null) { 99 return false; 100 } 101 102 // Don't trim the CN, though! 103 cn = cn.toLowerCase(Locale.ENGLISH); 104 105 if (cn.startsWith("*.")) { 106 // When a wildcard matches, also check that the wildcard is legit 107 // - Wildcards must contain at least two dots: "*.google.com" 108 // - Wildcards must be for private domains. No "*.co.uk" etc. 109 // - Wildcards must not match IP addresses: "*.8.8" 110 int matchLength = cn.length() - 1; 111 return hostName.regionMatches(hostName.length() - matchLength, cn, 1, matchLength) 112 && cn.indexOf('.', 2) != -1 113 && acceptableCountryWildcard(cn) 114 && !InetAddress.isNumeric(hostName); 115 } else { 116 return hostName.equals(cn); 117 } 118 } 119 120 private boolean acceptableCountryWildcard(String cn) { 121 int cnLen = cn.length(); 122 if (cnLen >= 7 && cnLen <= 9) { 123 // Look for the '.' in the 3rd-last position: 124 if (cn.charAt(cnLen - 3) == '.') { 125 // Trim off the [*.] and the [.XX]. 126 String s = cn.substring(2, cnLen - 3); 127 // And test against the sorted array of bad 2lds: 128 int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); 129 return x < 0; 130 } 131 } 132 return true; 133 } 134 135 private String getFirstCn(X509Certificate cert) { 136 /* 137 * Sebastian Hauer's original StrictSSLProtocolSocketFactory used 138 * getName() and had the following comment: 139 * 140 * Parses a X.500 distinguished name for the value of the 141 * "Common Name" field. This is done a bit sloppy right 142 * now and should probably be done a bit more according to 143 * <code>RFC 2253</code>. 144 * 145 * I've noticed that toString() seems to do a better job than 146 * getName() on these X500Principal objects, so I'm hoping that 147 * addresses Sebastian's concern. 148 * 149 * For example, getName() gives me this: 150 * 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d 151 * 152 * whereas toString() gives me this: 153 * EMAILADDRESS=juliusdavies (at) cucbc.com 154 * 155 * Looks like toString() even works with non-ascii domain names! 156 * I tested it with "花子.co.jp" and it worked fine. 157 */ 158 String subjectPrincipal = cert.getSubjectX500Principal().toString(); 159 for (String token : subjectPrincipal.split(",")) { 160 int x = token.indexOf("CN="); 161 if (x >= 0) { 162 return token.substring(x + 3); 163 } 164 } 165 return null; 166 } 167 168 /** 169 * Returns all SubjectAlt DNS names from an X509Certificate. 170 * 171 * <p>Note: Java doesn't appear able to extract international characters 172 * from the SubjectAlts. It can only extract international characters 173 * from the CN field. 174 * 175 * <p>(Or maybe the version of OpenSSL I'm using to test isn't storing the 176 * international characters correctly in the SubjectAlts?). 177 */ 178 private List<String> getDNSSubjectAlts(X509Certificate cert) { 179 Collection<List<?>> subjectAlternativeNames; 180 try { 181 subjectAlternativeNames = cert.getSubjectAlternativeNames(); 182 } catch (CertificateParsingException cpe) { 183 System.logI("Error parsing certificate", cpe); 184 return Collections.emptyList(); 185 } 186 187 if (subjectAlternativeNames == null) { 188 return Collections.emptyList(); 189 } 190 191 List<String> subjectAltList = new ArrayList<String>(); 192 for (List<?> pair : subjectAlternativeNames) { 193 int type = (Integer) pair.get(0); 194 // If type is 2, then we've got a dNSName 195 if (type == 2) { 196 subjectAltList.add((String) pair.get(1)); 197 } 198 } 199 return subjectAltList; 200 } 201 } 202