1/* 2 * Copyright (c) 2002, 2006, 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.util.*; 30 31import java.security.Principal; 32import java.security.cert.*; 33 34import javax.security.auth.x500.X500Principal; 35 36import sun.security.ssl.Krb5Helper; 37import sun.security.x509.X500Name; 38 39import sun.net.util.IPAddressUtil; 40 41/** 42 * Class to check hostnames against the names specified in a certificate as 43 * required for TLS and LDAP. 44 * 45 */ 46public class HostnameChecker { 47 48 // Constant for a HostnameChecker for TLS 49 public final static byte TYPE_TLS = 1; 50 private final static HostnameChecker INSTANCE_TLS = 51 new HostnameChecker(TYPE_TLS); 52 53 // Constant for a HostnameChecker for LDAP 54 public final static byte TYPE_LDAP = 2; 55 private final static HostnameChecker INSTANCE_LDAP = 56 new HostnameChecker(TYPE_LDAP); 57 58 // constants for subject alt names of type DNS and IP 59 private final static int ALTNAME_DNS = 2; 60 private final static 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 * @exception CertificateException if the name does not match any of 86 * the names specified in the certificate 87 */ 88 public void match(String expectedName, X509Certificate cert) 89 throws CertificateException { 90 if (isIpAddress(expectedName)) { 91 matchIP(expectedName, cert); 92 } else { 93 matchDNS(expectedName, cert); 94 } 95 } 96 97 /** 98 * Perform the check for Kerberos. 99 */ 100 public static boolean match(String expectedName, Principal principal) { 101 String hostName = getServerName(principal); 102 return (expectedName.equalsIgnoreCase(hostName)); 103 } 104 105 /** 106 * Return the Server name from Kerberos principal. 107 */ 108 public static String getServerName(Principal principal) { 109 return Krb5Helper.getPrincipalHostName(principal); 110 } 111 112 /** 113 * Test whether the given hostname looks like a literal IPv4 or IPv6 114 * address. The hostname does not need to be a fully qualified name. 115 * 116 * This is not a strict check that performs full input validation. 117 * That means if the method returns true, name need not be a correct 118 * IP address, rather that it does not represent a valid DNS hostname. 119 * Likewise for IP addresses when it returns false. 120 */ 121 private static boolean isIpAddress(String name) { 122 if (IPAddressUtil.isIPv4LiteralAddress(name) || 123 IPAddressUtil.isIPv6LiteralAddress(name)) { 124 return true; 125 } else { 126 return false; 127 } 128 } 129 130 /** 131 * Check if the certificate allows use of the given IP address. 132 * 133 * From RFC2818: 134 * In some cases, the URI is specified as an IP address rather than a 135 * hostname. In this case, the iPAddress subjectAltName must be present 136 * in the certificate and must exactly match the IP in the URI. 137 */ 138 private static void matchIP(String expectedIP, X509Certificate cert) 139 throws CertificateException { 140 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 141 if (subjAltNames == null) { 142 throw new CertificateException 143 ("No subject alternative names present"); 144 } 145 for (List<?> next : subjAltNames) { 146 // For IP address, it needs to be exact match 147 if (((Integer)next.get(0)).intValue() == ALTNAME_IP) { 148 String ipAddress = (String)next.get(1); 149 if (expectedIP.equalsIgnoreCase(ipAddress)) { 150 return; 151 } 152 } 153 } 154 throw new CertificateException("No subject alternative " + 155 "names matching " + "IP address " + 156 expectedIP + " found"); 157 } 158 159 /** 160 * Check if the certificate allows use of the given DNS name. 161 * 162 * From RFC2818: 163 * If a subjectAltName extension of type dNSName is present, that MUST 164 * be used as the identity. Otherwise, the (most specific) Common Name 165 * field in the Subject field of the certificate MUST be used. Although 166 * the use of the Common Name is existing practice, it is deprecated and 167 * Certification Authorities are encouraged to use the dNSName instead. 168 * 169 * Matching is performed using the matching rules specified by 170 * [RFC2459]. If more than one identity of a given type is present in 171 * the certificate (e.g., more than one dNSName name, a match in any one 172 * of the set is considered acceptable.) 173 */ 174 private void matchDNS(String expectedName, X509Certificate cert) 175 throws CertificateException { 176 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); 177 if (subjAltNames != null) { 178 boolean foundDNS = false; 179 for ( List<?> next : subjAltNames) { 180 if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) { 181 foundDNS = true; 182 String dnsName = (String)next.get(1); 183 if (isMatched(expectedName, dnsName)) { 184 return; 185 } 186 } 187 } 188 if (foundDNS) { 189 // if certificate contains any subject alt names of type DNS 190 // but none match, reject 191 throw new CertificateException("No subject alternative DNS " 192 + "name matching " + expectedName + " found."); 193 } 194 } 195 X500Name subjectName = getSubjectX500Name(cert); 196 DerValue derValue = subjectName.findMostSpecificAttribute 197 (X500Name.commonName_oid); 198 if (derValue != null) { 199 try { 200 if (isMatched(expectedName, derValue.getAsString())) { 201 return; 202 } 203 } catch (IOException e) { 204 // ignore 205 } 206 } 207 String msg = "No name matching " + expectedName + " found"; 208 throw new CertificateException(msg); 209 } 210 211 212 /** 213 * Return the subject of a certificate as X500Name, by reparsing if 214 * necessary. X500Name should only be used if access to name components 215 * is required, in other cases X500Principal is to be prefered. 216 * 217 * This method is currently used from within JSSE, do not remove. 218 */ 219 public static X500Name getSubjectX500Name(X509Certificate cert) 220 throws CertificateParsingException { 221 try { 222 Principal subjectDN = cert.getSubjectDN(); 223 if (subjectDN instanceof X500Name) { 224 return (X500Name)subjectDN; 225 } else { 226 X500Principal subjectX500 = cert.getSubjectX500Principal(); 227 return new X500Name(subjectX500.getEncoded()); 228 } 229 } catch (IOException e) { 230 throw(CertificateParsingException) 231 new CertificateParsingException().initCause(e); 232 } 233 } 234 235 236 /** 237 * Returns true if name matches against template.<p> 238 * 239 * The matching is performed as per RFC 2818 rules for TLS and 240 * RFC 2830 rules for LDAP.<p> 241 * 242 * The <code>name</code> parameter should represent a DNS name. 243 * The <code>template</code> parameter 244 * may contain the wildcard character * 245 */ 246 private boolean isMatched(String name, String template) { 247 if (checkType == TYPE_TLS) { 248 return matchAllWildcards(name, template); 249 } else if (checkType == TYPE_LDAP) { 250 return matchLeftmostWildcard(name, template); 251 } else { 252 return false; 253 } 254 } 255 256 257 /** 258 * Returns true if name matches against template.<p> 259 * 260 * According to RFC 2818, section 3.1 - 261 * Names may contain the wildcard character * which is 262 * considered to match any single domain name component 263 * or component fragment. 264 * E.g., *.a.com matches foo.a.com but not 265 * bar.foo.a.com. f*.com matches foo.com but not bar.com. 266 */ 267 private static boolean matchAllWildcards(String name, 268 String template) { 269 name = name.toLowerCase(); 270 template = template.toLowerCase(); 271 StringTokenizer nameSt = new StringTokenizer(name, "."); 272 StringTokenizer templateSt = new StringTokenizer(template, "."); 273 274 if (nameSt.countTokens() != templateSt.countTokens()) { 275 return false; 276 } 277 278 while (nameSt.hasMoreTokens()) { 279 if (!matchWildCards(nameSt.nextToken(), 280 templateSt.nextToken())) { 281 return false; 282 } 283 } 284 return true; 285 } 286 287 288 /** 289 * Returns true if name matches against template.<p> 290 * 291 * As per RFC 2830, section 3.6 - 292 * The "*" wildcard character is allowed. If present, it applies only 293 * to the left-most name component. 294 * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not 295 * bar.com. 296 */ 297 private static boolean matchLeftmostWildcard(String name, 298 String template) { 299 name = name.toLowerCase(); 300 template = template.toLowerCase(); 301 302 // Retreive leftmost component 303 int templateIdx = template.indexOf("."); 304 int nameIdx = name.indexOf("."); 305 306 if (templateIdx == -1) 307 templateIdx = template.length(); 308 if (nameIdx == -1) 309 nameIdx = name.length(); 310 311 if (matchWildCards(name.substring(0, nameIdx), 312 template.substring(0, templateIdx))) { 313 314 // match rest of the name 315 return template.substring(templateIdx).equals( 316 name.substring(nameIdx)); 317 } else { 318 return false; 319 } 320 } 321 322 323 /** 324 * Returns true if the name matches against the template that may 325 * contain wildcard char * <p> 326 */ 327 private static boolean matchWildCards(String name, String template) { 328 329 int wildcardIdx = template.indexOf("*"); 330 if (wildcardIdx == -1) 331 return name.equals(template); 332 333 boolean isBeginning = true; 334 String beforeWildcard = ""; 335 String afterWildcard = template; 336 337 while (wildcardIdx != -1) { 338 339 // match in sequence the non-wildcard chars in the template. 340 beforeWildcard = afterWildcard.substring(0, wildcardIdx); 341 afterWildcard = afterWildcard.substring(wildcardIdx + 1); 342 343 int beforeStartIdx = name.indexOf(beforeWildcard); 344 if ((beforeStartIdx == -1) || 345 (isBeginning && beforeStartIdx != 0)) { 346 return false; 347 } 348 isBeginning = false; 349 350 // update the match scope 351 name = name.substring(beforeStartIdx + beforeWildcard.length()); 352 wildcardIdx = afterWildcard.indexOf("*"); 353 } 354 return name.endsWith(afterWildcard); 355 } 356} 357