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.x509.X500Name; 39 import sun.security.ssl.SSLLogger; 40 41 /** 42 * Class to check hostnames against the names specified in a certificate as 43 * required for TLS and LDAP. 44 * 45 */ 46 public class HostnameChecker { 47 48 // Constant for a HostnameChecker for TLS 49 public static final byte TYPE_TLS = 1; 50 private static final HostnameChecker INSTANCE_TLS = 51 new HostnameChecker(TYPE_TLS); 52 53 // Constant for a HostnameChecker for LDAP 54 public static final byte TYPE_LDAP = 2; 55 private static final HostnameChecker INSTANCE_LDAP = 56 new HostnameChecker(TYPE_LDAP); 57 58 // constants for subject alt names of type DNS and IP 59 private static final int ALTNAME_DNS = 2; 60 private static final int ALTNAME_IP = 7; 61 62 // the algorithm to follow to perform the check. Currently unused. 63 private final byte checkType; 64 65 private HostnameChecker(byte checkType) { 66 this.checkType = checkType; 67 } 68 69 /** 70 * Get a HostnameChecker instance. checkType should be one of the 71 * TYPE_* constants defined in this class. 72 */ 73 public static HostnameChecker getInstance(byte checkType) { 74 if (checkType == TYPE_TLS) { 75 return INSTANCE_TLS; 76 } else if (checkType == TYPE_LDAP) { 77 return INSTANCE_LDAP; 78 } 79 throw new IllegalArgumentException("Unknown check type: " + checkType); 80 } 81 82 /** 83 * Perform the check. 84 * 85 * @param expectedName the expected host name or ip address 86 * @param cert the certificate to check against 87 * @param chainsToPublicCA true if the certificate chains to a public 88 * root CA (as pre-installed in the cacerts file) 89 * @throws CertificateException if the name does not match any of 90 * the names specified in the certificate 91 */ 92 public void match(String expectedName, X509Certificate cert, 93 boolean chainsToPublicCA) throws CertificateException { 94 if (isIpAddress(expectedName)) { 95 matchIP(expectedName, cert); 96 } else { 97 matchDNS(expectedName, cert, chainsToPublicCA); 98 } 99 } 100 101 public void match(String expectedName, X509Certificate cert) 102 throws CertificateException { 103 match(expectedName, cert, false); 104 } 105 106 /** 107 * Perform the check for Kerberos. 108 */ 109 public static boolean match(String expectedName, Principal principal) { 110 String hostName = getServerName(principal); 111 return (expectedName.equalsIgnoreCase(hostName)); 112 } 113 114 /** 115 * Return the Server name from Kerberos principal. 116 */ 117 public static String getServerName(Principal principal) { 118 /* 119 ClientKeyExchangeService p = 120 ClientKeyExchangeService.find("KRB5"); 121 if (p == null) { 122 throw new AssertionError("Kerberos should have been available"); 123 } 124 return p.getServiceHostName(principal); 125 */ 126 return null; 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 (SSLLogger.isOn) { 320 SSLLogger.fine( 321 "Certificate domain name has illegal single " + 322 "wildcard character: " + template); 323 } 324 return true; 325 } 326 327 int lastWildcardIndex = template.lastIndexOf("*"); 328 329 // ok if it has no wildcard character 330 if (lastWildcardIndex == -1) { 331 return false; 332 } 333 334 String afterWildcard = template.substring(lastWildcardIndex); 335 int firstDotIndex = afterWildcard.indexOf("."); 336 337 // not ok if there is no dot after wildcard (ex: "*com") 338 if (firstDotIndex == -1) { 339 if (SSLLogger.isOn) { 340 SSLLogger.fine( 341 "Certificate domain name has illegal wildcard, " + 342 "no dot after wildcard character: " + template); 343 } 344 return true; 345 } 346 347 // If the wildcarded domain is a top-level domain under which names 348 // can be registered, then a wildcard is not allowed. 349 350 if (!chainsToPublicCA) { 351 return false; // skip check for non-public certificates 352 } 353 Optional<RegisteredDomain> rd = RegisteredDomain.from(domain) 354 .filter(d -> d.type() == RegisteredDomain.Type.ICANN); 355 356 if (rd.isPresent()) { 357 String wDomain = afterWildcard.substring(firstDotIndex + 1); 358 if (rd.get().publicSuffix().equalsIgnoreCase(wDomain)) { 359 if (SSLLogger.isOn) { 360 SSLLogger.fine( 361 "Certificate domain name has illegal " + 362 "wildcard for public suffix: " + template); 363 } 364 return true; 365 } 366 } 367 368 return false; 369 } 370 371 /** 372 * Returns true if name matches against template.<p> 373 * 374 * According to RFC 2818, section 3.1 - 375 * Names may contain the wildcard character * which is 376 * considered to match any single domain name component 377 * or component fragment. 378 * E.g., *.a.com matches foo.a.com but not 379 * bar.foo.a.com. f*.com matches foo.com but not bar.com. 380 */ 381 private static boolean matchAllWildcards(String name, 382 String template) { 383 name = name.toLowerCase(Locale.ENGLISH); 384 template = template.toLowerCase(Locale.ENGLISH); 385 StringTokenizer nameSt = new StringTokenizer(name, "."); 386 StringTokenizer templateSt = new StringTokenizer(template, "."); 387 388 if (nameSt.countTokens() != templateSt.countTokens()) { 389 return false; 390 } 391 392 while (nameSt.hasMoreTokens()) { 393 if (!matchWildCards(nameSt.nextToken(), 394 templateSt.nextToken())) { 395 return false; 396 } 397 } 398 return true; 399 } 400 401 402 /** 403 * Returns true if name matches against template.<p> 404 * 405 * As per RFC 2830, section 3.6 - 406 * The "*" wildcard character is allowed. If present, it applies only 407 * to the left-most name component. 408 * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not 409 * bar.com. 410 */ 411 private static boolean matchLeftmostWildcard(String name, 412 String template) { 413 name = name.toLowerCase(Locale.ENGLISH); 414 template = template.toLowerCase(Locale.ENGLISH); 415 416 // Retrieve leftmost component 417 int templateIdx = template.indexOf("."); 418 int nameIdx = name.indexOf("."); 419 420 if (templateIdx == -1) 421 templateIdx = template.length(); 422 if (nameIdx == -1) 423 nameIdx = name.length(); 424 425 if (matchWildCards(name.substring(0, nameIdx), 426 template.substring(0, templateIdx))) { 427 428 // match rest of the name 429 return template.substring(templateIdx).equals( 430 name.substring(nameIdx)); 431 } else { 432 return false; 433 } 434 } 435 436 437 /** 438 * Returns true if the name matches against the template that may 439 * contain wildcard char * <p> 440 */ 441 private static boolean matchWildCards(String name, String template) { 442 443 int wildcardIdx = template.indexOf("*"); 444 if (wildcardIdx == -1) 445 return name.equals(template); 446 447 boolean isBeginning = true; 448 String beforeWildcard = ""; 449 String afterWildcard = template; 450 451 while (wildcardIdx != -1) { 452 453 // match in sequence the non-wildcard chars in the template. 454 beforeWildcard = afterWildcard.substring(0, wildcardIdx); 455 afterWildcard = afterWildcard.substring(wildcardIdx + 1); 456 457 int beforeStartIdx = name.indexOf(beforeWildcard); 458 if ((beforeStartIdx == -1) || 459 (isBeginning && beforeStartIdx != 0)) { 460 return false; 461 } 462 isBeginning = false; 463 464 // update the match scope 465 name = name.substring(beforeStartIdx + beforeWildcard.length()); 466 wildcardIdx = afterWildcard.indexOf("*"); 467 } 468 return name.endsWith(afterWildcard); 469 } 470 }