1 /* 2 * Copyright (c) 2002, 2017, 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 26 package sun.security.util; 27 28 import java.io.IOException; 29 import java.net.InetAddress; 30 import java.net.UnknownHostException; 31 import java.security.Principal; 32 import java.security.cert.*; 33 import java.util.*; 34 import javax.security.auth.x500.X500Principal; 35 import javax.net.ssl.SNIHostName; 36 37 import sun.net.util.IPAddressUtil; 38 import sun.security.ssl.ClientKeyExchangeService; 39 import sun.security.ssl.Debug; 40 import sun.security.x509.X500Name; 41 42 /** 43 * Class to check hostnames against the names specified in a certificate as 44 * required for TLS and LDAP. 45 * 46 */ 47 public class HostnameChecker { 48 49 // Constant for a HostnameChecker for TLS 50 public static final byte TYPE_TLS = 1; 51 private static final HostnameChecker INSTANCE_TLS = 52 new HostnameChecker(TYPE_TLS); 53 54 // Constant for a HostnameChecker for LDAP 55 public static final byte TYPE_LDAP = 2; 56 private static final HostnameChecker INSTANCE_LDAP = 57 new HostnameChecker(TYPE_LDAP); 58 59 // constants for subject alt names of type DNS and IP 60 private static final int ALTNAME_DNS = 2; 61 private static final int ALTNAME_IP = 7; 62 63 private static final Debug debug = Debug.getInstance("ssl"); 64 65 // the algorithm to follow to perform the check. Currently unused. 66 private final byte checkType; 67 68 private HostnameChecker(byte checkType) { 69 this.checkType = checkType; 70 } 71 72 /** 73 * Get a HostnameChecker instance. checkType should be one of the 74 * TYPE_* constants defined in this class. 75 */ 76 public static HostnameChecker getInstance(byte checkType) { 77 if (checkType == TYPE_TLS) { 78 return INSTANCE_TLS; 79 } else if (checkType == TYPE_LDAP) { 80 return INSTANCE_LDAP; 81 } 82 throw new IllegalArgumentException("Unknown check type: " + checkType); 83 } 84 85 /** 86 * Perform the check. 87 * 88 * @param expectedName the expected host name or ip address 89 * @param cert the certificate to check against 90 * @param chainsToPublicCA true if the certificate chains to a public 91 * root CA (as pre-installed in the cacerts file) 92 * @throws CertificateException if the name does not match any of 93 * the names specified in the certificate 94 */ 95 public void match(String expectedName, X509Certificate cert, 96 boolean chainsToPublicCA) throws CertificateException { 97 if (isIpAddress(expectedName)) { 98 matchIP(expectedName, cert); 99 } else { 100 matchDNS(expectedName, cert, chainsToPublicCA); 101 } 102 } 103 104 public void match(String expectedName, X509Certificate cert) 105 throws CertificateException { 106 match(expectedName, cert, false); 107 } 108 109 /** 110 * Perform the check for Kerberos. 111 */ 112 public static boolean match(String expectedName, Principal principal) { 113 String hostName = getServerName(principal); 114 return (expectedName.equalsIgnoreCase(hostName)); 115 } 116 117 /** 118 * Return the Server name from Kerberos principal. 119 */ 120 public static String getServerName(Principal principal) { 121 ClientKeyExchangeService p = 122 ClientKeyExchangeService.find("KRB5"); 123 if (p == null) { 124 throw new AssertionError("Kerberos should have been available"); 125 } 126 return p.getServiceHostName(principal); 127 } 128 129 /** 130 * Test whether the given hostname looks like a literal IPv4 or IPv6 131 * address. The hostname does not need to be a fully qualified name. 132 * 133 * This is not a strict check that performs full input validation. 134 * That means if the method returns true, name need not be a correct 135 * IP address, rather that it does not represent a valid DNS hostname. 136 * Likewise for IP addresses when it returns false. 137 */ 138 private static boolean isIpAddress(String name) { 139 if (IPAddressUtil.isIPv4LiteralAddress(name) || 140 IPAddressUtil.isIPv6LiteralAddress(name)) { 141 return true; 142 } else { 143 return false; 144 } 145 } 146 147 /** 148 * Check if the certificate allows use of the given IP address. 149 * 150 * From RFC2818: 151 * In some cases, the URI is specified as an IP address rather than a 152 * hostname. In this case, the iPAddress subjectAltName must be present 153 * in the certificate and must exactly match the IP in the URI. 154 */ 155 private static void matchIP(String expectedIP, X509Certificate cert) 156 throws CertificateException { 157 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 158 if (subjAltNames == null) { 159 throw new CertificateException 160 ("No subject alternative names present"); 161 } 162 for (List<?> next : subjAltNames) { 163 // For IP address, it needs to be exact match 164 if (((Integer)next.get(0)).intValue() == ALTNAME_IP) { 165 String ipAddress = (String)next.get(1); 166 if (expectedIP.equalsIgnoreCase(ipAddress)) { 167 return; 168 } else { 169 // compare InetAddress objects in order to ensure 170 // equality between a long IPv6 address and its 171 // abbreviated form. 172 try { 173 if (InetAddress.getByName(expectedIP).equals( 174 InetAddress.getByName(ipAddress))) { 175 return; 176 } 177 } catch (UnknownHostException e) { 178 } catch (SecurityException e) {} 179 } 180 } 181 } 182 throw new CertificateException("No subject alternative " + 183 "names matching " + "IP address " + 184 expectedIP + " found"); 185 } 186 187 /** 188 * Check if the certificate allows use of the given DNS name. 189 * 190 * From RFC2818: 191 * If a subjectAltName extension of type dNSName is present, that MUST 192 * be used as the identity. Otherwise, the (most specific) Common Name 193 * field in the Subject field of the certificate MUST be used. Although 194 * the use of the Common Name is existing practice, it is deprecated and 195 * Certification Authorities are encouraged to use the dNSName instead. 196 * 197 * Matching is performed using the matching rules specified by 198 * [RFC5280]. If more than one identity of a given type is present in 199 * the certificate (e.g., more than one dNSName name, a match in any one 200 * of the set is considered acceptable.) 201 */ 202 private void matchDNS(String expectedName, X509Certificate cert, 203 boolean chainsToPublicCA) 204 throws CertificateException { 205 // Check that the expected name is a valid domain name. 206 try { 207 // Using the checking implemented in SNIHostName 208 SNIHostName sni = new SNIHostName(expectedName); 209 } catch (IllegalArgumentException iae) { 210 throw new CertificateException( 211 "Illegal given domain name: " + expectedName, iae); 212 } 213 214 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 215 if (subjAltNames != null) { 216 boolean foundDNS = false; 217 for (List<?> next : subjAltNames) { 218 if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) { 219 foundDNS = true; 220 String dnsName = (String)next.get(1); 221 if (isMatched(expectedName, dnsName, chainsToPublicCA)) { 222 return; 223 } 224 } 225 } 226 if (foundDNS) { 227 // if certificate contains any subject alt names of type DNS 228 // but none match, reject 229 throw new CertificateException("No subject alternative DNS " 230 + "name matching " + expectedName + " found."); 231 } 232 } 233 X500Name subjectName = getSubjectX500Name(cert); 234 DerValue derValue = subjectName.findMostSpecificAttribute 235 (X500Name.commonName_oid); 236 if (derValue != null) { 237 try { 238 if (isMatched(expectedName, derValue.getAsString(), 239 chainsToPublicCA)) { 240 return; 241 } 242 } catch (IOException e) { 243 // ignore 244 } 245 } 246 String msg = "No name matching " + expectedName + " found"; 247 throw new CertificateException(msg); 248 } 249 250 251 /** 252 * Return the subject of a certificate as X500Name, by reparsing if 253 * necessary. X500Name should only be used if access to name components 254 * is required, in other cases X500Principal is to be preferred. 255 * 256 * This method is currently used from within JSSE, do not remove. 257 */ 258 public static X500Name getSubjectX500Name(X509Certificate cert) 259 throws CertificateParsingException { 260 try { 261 Principal subjectDN = cert.getSubjectDN(); 262 if (subjectDN instanceof X500Name) { 263 return (X500Name)subjectDN; 264 } else { 265 X500Principal subjectX500 = cert.getSubjectX500Principal(); 266 return new X500Name(subjectX500.getEncoded()); 267 } 268 } catch (IOException e) { 269 throw(CertificateParsingException) 270 new CertificateParsingException().initCause(e); 271 } 272 } 273 274 275 /** 276 * Returns true if name matches against template.<p> 277 * 278 * The matching is performed as per RFC 2818 rules for TLS and 279 * RFC 2830 rules for LDAP.<p> 280 * 281 * The <code>name</code> parameter should represent a DNS name. 282 * The <code>template</code> parameter 283 * may contain the wildcard character * 284 */ 285 private boolean isMatched(String name, String template, 286 boolean chainsToPublicCA) { 287 if (hasIllegalWildcard(name, template, chainsToPublicCA)) { 288 return false; 289 } 290 291 // check the validity of the domain name template. 292 try { 293 // Replacing wildcard character '*' with 'x' so as to check 294 // the domain name template validity. 295 // 296 // Using the checking implemented in SNIHostName 297 SNIHostName sni = new SNIHostName(template.replace('*', 'x')); 298 } catch (IllegalArgumentException iae) { 299 // It would be nice to add debug log if not matching. 300 return false; 301 } 302 303 if (checkType == TYPE_TLS) { 304 return matchAllWildcards(name, template); 305 } else if (checkType == TYPE_LDAP) { 306 return matchLeftmostWildcard(name, template); 307 } else { 308 return false; 309 } 310 } 311 312 /** 313 * Returns true if the template contains an illegal wildcard character. 314 */ 315 private static boolean hasIllegalWildcard(String domain, String template, 316 boolean chainsToPublicCA) { 317 // not ok if it is a single wildcard character or "*." 318 if (template.equals("*") || template.equals("*.")) { 319 if (debug != null) { 320 debug.println("Certificate domain name has illegal single " + 321 "wildcard character: " + template); 322 } 323 return true; 324 } 325 326 int lastWildcardIndex = template.lastIndexOf("*"); 327 328 // ok if it has no wildcard character 329 if (lastWildcardIndex == -1) { 330 return false; 331 } 332 333 String afterWildcard = template.substring(lastWildcardIndex); 334 int firstDotIndex = afterWildcard.indexOf("."); 335 336 // not ok if there is no dot after wildcard (ex: "*com") 337 if (firstDotIndex == -1) { 338 if (debug != null) { 339 debug.println("Certificate domain name has illegal wildcard, " + 340 "no dot after wildcard character: " + template); 341 } 342 return true; 343 } 344 345 // If the wildcarded domain is a top-level domain under which names 346 // can be registered, then a wildcard is not allowed. 347 348 if (!chainsToPublicCA) { 349 return false; // skip check for non-public certificates 350 } 351 Optional<RegisteredDomain> rd = RegisteredDomain.from(domain) 352 .filter(d -> d.type() == RegisteredDomain.Type.ICANN); 353 354 if (rd.isPresent()) { 355 String wDomain = afterWildcard.substring(firstDotIndex + 1); 356 if (rd.get().publicSuffix().equalsIgnoreCase(wDomain)) { 357 if (debug != null) { 358 debug.println("Certificate domain name has illegal " + 359 "wildcard for public suffix: " + template); 360 } 361 return true; 362 } 363 } 364 365 return false; 366 } 367 368 /** 369 * Returns true if name matches against template.<p> 370 * 371 * According to RFC 2818, section 3.1 - 372 * Names may contain the wildcard character * which is 373 * considered to match any single domain name component 374 * or component fragment. 375 * E.g., *.a.com matches foo.a.com but not 376 * bar.foo.a.com. f*.com matches foo.com but not bar.com. 377 */ 378 private static boolean matchAllWildcards(String name, 379 String template) { 380 name = name.toLowerCase(Locale.ENGLISH); 381 template = template.toLowerCase(Locale.ENGLISH); 382 StringTokenizer nameSt = new StringTokenizer(name, "."); 383 StringTokenizer templateSt = new StringTokenizer(template, "."); 384 385 if (nameSt.countTokens() != templateSt.countTokens()) { 386 return false; 387 } 388 389 while (nameSt.hasMoreTokens()) { 390 if (!matchWildCards(nameSt.nextToken(), 391 templateSt.nextToken())) { 392 return false; 393 } 394 } 395 return true; 396 } 397 398 399 /** 400 * Returns true if name matches against template.<p> 401 * 402 * As per RFC 2830, section 3.6 - 403 * The "*" wildcard character is allowed. If present, it applies only 404 * to the left-most name component. 405 * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not 406 * bar.com. 407 */ 408 private static boolean matchLeftmostWildcard(String name, 409 String template) { 410 name = name.toLowerCase(Locale.ENGLISH); 411 template = template.toLowerCase(Locale.ENGLISH); 412 413 // Retrieve leftmost component 414 int templateIdx = template.indexOf("."); 415 int nameIdx = name.indexOf("."); 416 417 if (templateIdx == -1) 418 templateIdx = template.length(); 419 if (nameIdx == -1) 420 nameIdx = name.length(); 421 422 if (matchWildCards(name.substring(0, nameIdx), 423 template.substring(0, templateIdx))) { 424 425 // match rest of the name 426 return template.substring(templateIdx).equals( 427 name.substring(nameIdx)); 428 } else { 429 return false; 430 } 431 } 432 433 434 /** 435 * Returns true if the name matches against the template that may 436 * contain wildcard char * <p> 437 */ 438 private static boolean matchWildCards(String name, String template) { 439 440 int wildcardIdx = template.indexOf("*"); 441 if (wildcardIdx == -1) 442 return name.equals(template); 443 444 boolean isBeginning = true; 445 String beforeWildcard = ""; 446 String afterWildcard = template; 447 448 while (wildcardIdx != -1) { 449 450 // match in sequence the non-wildcard chars in the template. 451 beforeWildcard = afterWildcard.substring(0, wildcardIdx); 452 afterWildcard = afterWildcard.substring(wildcardIdx + 1); 453 454 int beforeStartIdx = name.indexOf(beforeWildcard); 455 if ((beforeStartIdx == -1) || 456 (isBeginning && beforeStartIdx != 0)) { 457 return false; 458 } 459 isBeginning = false; 460 461 // update the match scope 462 name = name.substring(beforeStartIdx + beforeWildcard.length()); 463 wildcardIdx = afterWildcard.indexOf("*"); 464 } 465 return name.endsWith(afterWildcard); 466 } 467 }