HostnameChecker.java revision 12745:f068a4ffddd2
1/* 2 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package sun.security.util; 27 28import java.io.IOException; 29import java.net.InetAddress; 30import java.net.UnknownHostException; 31import java.util.*; 32 33import java.security.Principal; 34import java.security.cert.*; 35 36import javax.security.auth.x500.X500Principal; 37 38import sun.security.ssl.ClientKeyExchangeService; 39import sun.security.x509.X500Name; 40 41import sun.net.util.IPAddressUtil; 42 43/** 44 * Class to check hostnames against the names specified in a certificate as 45 * required for TLS and LDAP. 46 * 47 */ 48public class HostnameChecker { 49 50 // Constant for a HostnameChecker for TLS 51 public static final byte TYPE_TLS = 1; 52 private static final HostnameChecker INSTANCE_TLS = 53 new HostnameChecker(TYPE_TLS); 54 55 // Constant for a HostnameChecker for LDAP 56 public static final byte TYPE_LDAP = 2; 57 private static final HostnameChecker INSTANCE_LDAP = 58 new HostnameChecker(TYPE_LDAP); 59 60 // constants for subject alt names of type DNS and IP 61 private static final int ALTNAME_DNS = 2; 62 private static final int ALTNAME_IP = 7; 63 64 // the algorithm to follow to perform the check. Currently unused. 65 private final byte checkType; 66 67 private HostnameChecker(byte checkType) { 68 this.checkType = checkType; 69 } 70 71 /** 72 * Get a HostnameChecker instance. checkType should be one of the 73 * TYPE_* constants defined in this class. 74 */ 75 public static HostnameChecker getInstance(byte checkType) { 76 if (checkType == TYPE_TLS) { 77 return INSTANCE_TLS; 78 } else if (checkType == TYPE_LDAP) { 79 return INSTANCE_LDAP; 80 } 81 throw new IllegalArgumentException("Unknown check type: " + checkType); 82 } 83 84 /** 85 * Perform the check. 86 * 87 * @exception CertificateException if the name does not match any of 88 * the names specified in the certificate 89 */ 90 public void match(String expectedName, X509Certificate cert) 91 throws CertificateException { 92 if (isIpAddress(expectedName)) { 93 matchIP(expectedName, cert); 94 } else { 95 matchDNS(expectedName, cert); 96 } 97 } 98 99 /** 100 * Perform the check for Kerberos. 101 */ 102 public static boolean match(String expectedName, Principal principal) { 103 String hostName = getServerName(principal); 104 return (expectedName.equalsIgnoreCase(hostName)); 105 } 106 107 /** 108 * Return the Server name from Kerberos principal. 109 */ 110 public static String getServerName(Principal principal) { 111 ClientKeyExchangeService p = 112 ClientKeyExchangeService.find("KRB5"); 113 if (p == null) { 114 throw new AssertionError("Kerberos should have been available"); 115 } 116 return p.getServiceHostName(principal); 117 } 118 119 /** 120 * Test whether the given hostname looks like a literal IPv4 or IPv6 121 * address. The hostname does not need to be a fully qualified name. 122 * 123 * This is not a strict check that performs full input validation. 124 * That means if the method returns true, name need not be a correct 125 * IP address, rather that it does not represent a valid DNS hostname. 126 * Likewise for IP addresses when it returns false. 127 */ 128 private static boolean isIpAddress(String name) { 129 if (IPAddressUtil.isIPv4LiteralAddress(name) || 130 IPAddressUtil.isIPv6LiteralAddress(name)) { 131 return true; 132 } else { 133 return false; 134 } 135 } 136 137 /** 138 * Check if the certificate allows use of the given IP address. 139 * 140 * From RFC2818: 141 * In some cases, the URI is specified as an IP address rather than a 142 * hostname. In this case, the iPAddress subjectAltName must be present 143 * in the certificate and must exactly match the IP in the URI. 144 */ 145 private static void matchIP(String expectedIP, X509Certificate cert) 146 throws CertificateException { 147 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 148 if (subjAltNames == null) { 149 throw new CertificateException 150 ("No subject alternative names present"); 151 } 152 for (List<?> next : subjAltNames) { 153 // For IP address, it needs to be exact match 154 if (((Integer)next.get(0)).intValue() == ALTNAME_IP) { 155 String ipAddress = (String)next.get(1); 156 if (expectedIP.equalsIgnoreCase(ipAddress)) { 157 return; 158 } else { 159 // compare InetAddress objects in order to ensure 160 // equality between a long IPv6 address and its 161 // abbreviated form. 162 try { 163 if (InetAddress.getByName(expectedIP).equals( 164 InetAddress.getByName(ipAddress))) { 165 return; 166 } 167 } catch (UnknownHostException e) { 168 } catch (SecurityException e) {} 169 } 170 } 171 } 172 throw new CertificateException("No subject alternative " + 173 "names matching " + "IP address " + 174 expectedIP + " found"); 175 } 176 177 /** 178 * Check if the certificate allows use of the given DNS name. 179 * 180 * From RFC2818: 181 * If a subjectAltName extension of type dNSName is present, that MUST 182 * be used as the identity. Otherwise, the (most specific) Common Name 183 * field in the Subject field of the certificate MUST be used. Although 184 * the use of the Common Name is existing practice, it is deprecated and 185 * Certification Authorities are encouraged to use the dNSName instead. 186 * 187 * Matching is performed using the matching rules specified by 188 * [RFC2459]. If more than one identity of a given type is present in 189 * the certificate (e.g., more than one dNSName name, a match in any one 190 * of the set is considered acceptable.) 191 */ 192 private void matchDNS(String expectedName, X509Certificate cert) 193 throws CertificateException { 194 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 195 if (subjAltNames != null) { 196 boolean foundDNS = false; 197 for ( List<?> next : subjAltNames) { 198 if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) { 199 foundDNS = true; 200 String dnsName = (String)next.get(1); 201 if (isMatched(expectedName, dnsName)) { 202 return; 203 } 204 } 205 } 206 if (foundDNS) { 207 // if certificate contains any subject alt names of type DNS 208 // but none match, reject 209 throw new CertificateException("No subject alternative DNS " 210 + "name matching " + expectedName + " found."); 211 } 212 } 213 X500Name subjectName = getSubjectX500Name(cert); 214 DerValue derValue = subjectName.findMostSpecificAttribute 215 (X500Name.commonName_oid); 216 if (derValue != null) { 217 try { 218 if (isMatched(expectedName, derValue.getAsString())) { 219 return; 220 } 221 } catch (IOException e) { 222 // ignore 223 } 224 } 225 String msg = "No name matching " + expectedName + " found"; 226 throw new CertificateException(msg); 227 } 228 229 230 /** 231 * Return the subject of a certificate as X500Name, by reparsing if 232 * necessary. X500Name should only be used if access to name components 233 * is required, in other cases X500Principal is to be preferred. 234 * 235 * This method is currently used from within JSSE, do not remove. 236 */ 237 public static X500Name getSubjectX500Name(X509Certificate cert) 238 throws CertificateParsingException { 239 try { 240 Principal subjectDN = cert.getSubjectDN(); 241 if (subjectDN instanceof X500Name) { 242 return (X500Name)subjectDN; 243 } else { 244 X500Principal subjectX500 = cert.getSubjectX500Principal(); 245 return new X500Name(subjectX500.getEncoded()); 246 } 247 } catch (IOException e) { 248 throw(CertificateParsingException) 249 new CertificateParsingException().initCause(e); 250 } 251 } 252 253 254 /** 255 * Returns true if name matches against template.<p> 256 * 257 * The matching is performed as per RFC 2818 rules for TLS and 258 * RFC 2830 rules for LDAP.<p> 259 * 260 * The <code>name</code> parameter should represent a DNS name. 261 * The <code>template</code> parameter 262 * may contain the wildcard character * 263 */ 264 private boolean isMatched(String name, String template) { 265 if (checkType == TYPE_TLS) { 266 return matchAllWildcards(name, template); 267 } else if (checkType == TYPE_LDAP) { 268 return matchLeftmostWildcard(name, template); 269 } else { 270 return false; 271 } 272 } 273 274 275 /** 276 * Returns true if name matches against template.<p> 277 * 278 * According to RFC 2818, section 3.1 - 279 * Names may contain the wildcard character * which is 280 * considered to match any single domain name component 281 * or component fragment. 282 * E.g., *.a.com matches foo.a.com but not 283 * bar.foo.a.com. f*.com matches foo.com but not bar.com. 284 */ 285 private static boolean matchAllWildcards(String name, 286 String template) { 287 name = name.toLowerCase(Locale.ENGLISH); 288 template = template.toLowerCase(Locale.ENGLISH); 289 StringTokenizer nameSt = new StringTokenizer(name, "."); 290 StringTokenizer templateSt = new StringTokenizer(template, "."); 291 292 if (nameSt.countTokens() != templateSt.countTokens()) { 293 return false; 294 } 295 296 while (nameSt.hasMoreTokens()) { 297 if (!matchWildCards(nameSt.nextToken(), 298 templateSt.nextToken())) { 299 return false; 300 } 301 } 302 return true; 303 } 304 305 306 /** 307 * Returns true if name matches against template.<p> 308 * 309 * As per RFC 2830, section 3.6 - 310 * The "*" wildcard character is allowed. If present, it applies only 311 * to the left-most name component. 312 * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not 313 * bar.com. 314 */ 315 private static boolean matchLeftmostWildcard(String name, 316 String template) { 317 name = name.toLowerCase(Locale.ENGLISH); 318 template = template.toLowerCase(Locale.ENGLISH); 319 320 // Retreive leftmost component 321 int templateIdx = template.indexOf('.'); 322 int nameIdx = name.indexOf('.'); 323 324 if (templateIdx == -1) 325 templateIdx = template.length(); 326 if (nameIdx == -1) 327 nameIdx = name.length(); 328 329 if (matchWildCards(name.substring(0, nameIdx), 330 template.substring(0, templateIdx))) { 331 332 // match rest of the name 333 return template.substring(templateIdx).equals( 334 name.substring(nameIdx)); 335 } else { 336 return false; 337 } 338 } 339 340 341 /** 342 * Returns true if the name matches against the template that may 343 * contain wildcard char * <p> 344 */ 345 private static boolean matchWildCards(String name, String template) { 346 347 int wildcardIdx = template.indexOf('*'); 348 if (wildcardIdx == -1) 349 return name.equals(template); 350 351 boolean isBeginning = true; 352 String beforeWildcard = ""; 353 String afterWildcard = template; 354 355 while (wildcardIdx != -1) { 356 357 // match in sequence the non-wildcard chars in the template. 358 beforeWildcard = afterWildcard.substring(0, wildcardIdx); 359 afterWildcard = afterWildcard.substring(wildcardIdx + 1); 360 361 int beforeStartIdx = name.indexOf(beforeWildcard); 362 if ((beforeStartIdx == -1) || 363 (isBeginning && beforeStartIdx != 0)) { 364 return false; 365 } 366 isBeginning = false; 367 368 // update the match scope 369 name = name.substring(beforeStartIdx + beforeWildcard.length()); 370 wildcardIdx = afterWildcard.indexOf('*'); 371 } 372 return name.endsWith(afterWildcard); 373 } 374} 375