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 
  26 package sun.security.util;
  27 
  28 import java.io.IOException;
  29 import java.util.*;
  30 
  31 import java.security.Principal;
  32 import java.security.cert.*;
  33 
  34 import javax.security.auth.x500.X500Principal;
  35 
  36 import sun.security.ssl.Krb5Helper;
  37 import sun.security.x509.X500Name;
  38 
  39 import 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  */
  46 public 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 }