jdk/src/java.base/share/classes/sun/security/util/HostnameChecker.java
changeset 25859 3317bb8137f4
parent 24685 215fa91e1b4c
child 28865 4729ff15079b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/java.base/share/classes/sun/security/util/HostnameChecker.java	Sun Aug 17 15:54:13 2014 +0100
@@ -0,0 +1,356 @@
+/*
+ * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package sun.security.util;
+
+import java.io.IOException;
+import java.util.*;
+
+import java.security.Principal;
+import java.security.cert.*;
+
+import javax.security.auth.x500.X500Principal;
+
+import sun.security.ssl.Krb5Helper;
+import sun.security.x509.X500Name;
+
+import sun.net.util.IPAddressUtil;
+
+/**
+ * Class to check hostnames against the names specified in a certificate as
+ * required for TLS and LDAP.
+ *
+ */
+public class HostnameChecker {
+
+    // Constant for a HostnameChecker for TLS
+    public final static byte TYPE_TLS = 1;
+    private final static HostnameChecker INSTANCE_TLS =
+                                        new HostnameChecker(TYPE_TLS);
+
+    // Constant for a HostnameChecker for LDAP
+    public final static byte TYPE_LDAP = 2;
+    private final static HostnameChecker INSTANCE_LDAP =
+                                        new HostnameChecker(TYPE_LDAP);
+
+    // constants for subject alt names of type DNS and IP
+    private final static int ALTNAME_DNS = 2;
+    private final static int ALTNAME_IP  = 7;
+
+    // the algorithm to follow to perform the check. Currently unused.
+    private final byte checkType;
+
+    private HostnameChecker(byte checkType) {
+        this.checkType = checkType;
+    }
+
+    /**
+     * Get a HostnameChecker instance. checkType should be one of the
+     * TYPE_* constants defined in this class.
+     */
+    public static HostnameChecker getInstance(byte checkType) {
+        if (checkType == TYPE_TLS) {
+            return INSTANCE_TLS;
+        } else if (checkType == TYPE_LDAP) {
+            return INSTANCE_LDAP;
+        }
+        throw new IllegalArgumentException("Unknown check type: " + checkType);
+    }
+
+    /**
+     * Perform the check.
+     *
+     * @exception CertificateException if the name does not match any of
+     * the names specified in the certificate
+     */
+    public void match(String expectedName, X509Certificate cert)
+            throws CertificateException {
+        if (isIpAddress(expectedName)) {
+           matchIP(expectedName, cert);
+        } else {
+           matchDNS(expectedName, cert);
+        }
+    }
+
+    /**
+     * Perform the check for Kerberos.
+     */
+    public static boolean match(String expectedName, Principal principal) {
+        String hostName = getServerName(principal);
+        return (expectedName.equalsIgnoreCase(hostName));
+    }
+
+    /**
+     * Return the Server name from Kerberos principal.
+     */
+    public static String getServerName(Principal principal) {
+        return Krb5Helper.getPrincipalHostName(principal);
+    }
+
+    /**
+     * Test whether the given hostname looks like a literal IPv4 or IPv6
+     * address. The hostname does not need to be a fully qualified name.
+     *
+     * This is not a strict check that performs full input validation.
+     * That means if the method returns true, name need not be a correct
+     * IP address, rather that it does not represent a valid DNS hostname.
+     * Likewise for IP addresses when it returns false.
+     */
+    private static boolean isIpAddress(String name) {
+        if (IPAddressUtil.isIPv4LiteralAddress(name) ||
+            IPAddressUtil.isIPv6LiteralAddress(name)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Check if the certificate allows use of the given IP address.
+     *
+     * From RFC2818:
+     * In some cases, the URI is specified as an IP address rather than a
+     * hostname. In this case, the iPAddress subjectAltName must be present
+     * in the certificate and must exactly match the IP in the URI.
+     */
+    private static void matchIP(String expectedIP, X509Certificate cert)
+            throws CertificateException {
+        Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
+        if (subjAltNames == null) {
+            throw new CertificateException
+                                ("No subject alternative names present");
+        }
+        for (List<?> next : subjAltNames) {
+            // For IP address, it needs to be exact match
+            if (((Integer)next.get(0)).intValue() == ALTNAME_IP) {
+                String ipAddress = (String)next.get(1);
+                if (expectedIP.equalsIgnoreCase(ipAddress)) {
+                    return;
+                }
+            }
+        }
+        throw new CertificateException("No subject alternative " +
+                        "names matching " + "IP address " +
+                        expectedIP + " found");
+    }
+
+    /**
+     * Check if the certificate allows use of the given DNS name.
+     *
+     * From RFC2818:
+     * If a subjectAltName extension of type dNSName is present, that MUST
+     * be used as the identity. Otherwise, the (most specific) Common Name
+     * field in the Subject field of the certificate MUST be used. Although
+     * the use of the Common Name is existing practice, it is deprecated and
+     * Certification Authorities are encouraged to use the dNSName instead.
+     *
+     * Matching is performed using the matching rules specified by
+     * [RFC2459].  If more than one identity of a given type is present in
+     * the certificate (e.g., more than one dNSName name, a match in any one
+     * of the set is considered acceptable.)
+     */
+    private void matchDNS(String expectedName, X509Certificate cert)
+            throws CertificateException {
+        Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
+        if (subjAltNames != null) {
+            boolean foundDNS = false;
+            for ( List<?> next : subjAltNames) {
+                if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) {
+                    foundDNS = true;
+                    String dnsName = (String)next.get(1);
+                    if (isMatched(expectedName, dnsName)) {
+                        return;
+                    }
+                }
+            }
+            if (foundDNS) {
+                // if certificate contains any subject alt names of type DNS
+                // but none match, reject
+                throw new CertificateException("No subject alternative DNS "
+                        + "name matching " + expectedName + " found.");
+            }
+        }
+        X500Name subjectName = getSubjectX500Name(cert);
+        DerValue derValue = subjectName.findMostSpecificAttribute
+                                                    (X500Name.commonName_oid);
+        if (derValue != null) {
+            try {
+                if (isMatched(expectedName, derValue.getAsString())) {
+                    return;
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+        String msg = "No name matching " + expectedName + " found";
+        throw new CertificateException(msg);
+    }
+
+
+    /**
+     * Return the subject of a certificate as X500Name, by reparsing if
+     * necessary. X500Name should only be used if access to name components
+     * is required, in other cases X500Principal is to be preferred.
+     *
+     * This method is currently used from within JSSE, do not remove.
+     */
+    public static X500Name getSubjectX500Name(X509Certificate cert)
+            throws CertificateParsingException {
+        try {
+            Principal subjectDN = cert.getSubjectDN();
+            if (subjectDN instanceof X500Name) {
+                return (X500Name)subjectDN;
+            } else {
+                X500Principal subjectX500 = cert.getSubjectX500Principal();
+                return new X500Name(subjectX500.getEncoded());
+            }
+        } catch (IOException e) {
+            throw(CertificateParsingException)
+                new CertificateParsingException().initCause(e);
+        }
+    }
+
+
+    /**
+     * Returns true if name matches against template.<p>
+     *
+     * The matching is performed as per RFC 2818 rules for TLS and
+     * RFC 2830 rules for LDAP.<p>
+     *
+     * The <code>name</code> parameter should represent a DNS name.
+     * The <code>template</code> parameter
+     * may contain the wildcard character *
+     */
+    private boolean isMatched(String name, String template) {
+        if (checkType == TYPE_TLS) {
+            return matchAllWildcards(name, template);
+        } else if (checkType == TYPE_LDAP) {
+            return matchLeftmostWildcard(name, template);
+        } else {
+            return false;
+        }
+    }
+
+
+    /**
+     * Returns true if name matches against template.<p>
+     *
+     * According to RFC 2818, section 3.1 -
+     * Names may contain the wildcard character * which is
+     * considered to match any single domain name component
+     * or component fragment.
+     * E.g., *.a.com matches foo.a.com but not
+     * bar.foo.a.com. f*.com matches foo.com but not bar.com.
+     */
+    private static boolean matchAllWildcards(String name,
+         String template) {
+        name = name.toLowerCase(Locale.ENGLISH);
+        template = template.toLowerCase(Locale.ENGLISH);
+        StringTokenizer nameSt = new StringTokenizer(name, ".");
+        StringTokenizer templateSt = new StringTokenizer(template, ".");
+
+        if (nameSt.countTokens() != templateSt.countTokens()) {
+            return false;
+        }
+
+        while (nameSt.hasMoreTokens()) {
+            if (!matchWildCards(nameSt.nextToken(),
+                        templateSt.nextToken())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    /**
+     * Returns true if name matches against template.<p>
+     *
+     * As per RFC 2830, section 3.6 -
+     * The "*" wildcard character is allowed.  If present, it applies only
+     * to the left-most name component.
+     * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not
+     * bar.com.
+     */
+    private static boolean matchLeftmostWildcard(String name,
+                         String template) {
+        name = name.toLowerCase(Locale.ENGLISH);
+        template = template.toLowerCase(Locale.ENGLISH);
+
+        // Retreive leftmost component
+        int templateIdx = template.indexOf('.');
+        int nameIdx = name.indexOf('.');
+
+        if (templateIdx == -1)
+            templateIdx = template.length();
+        if (nameIdx == -1)
+            nameIdx = name.length();
+
+        if (matchWildCards(name.substring(0, nameIdx),
+            template.substring(0, templateIdx))) {
+
+            // match rest of the name
+            return template.substring(templateIdx).equals(
+                        name.substring(nameIdx));
+        } else {
+            return false;
+        }
+    }
+
+
+    /**
+     * Returns true if the name matches against the template that may
+     * contain wildcard char * <p>
+     */
+    private static boolean matchWildCards(String name, String template) {
+
+        int wildcardIdx = template.indexOf('*');
+        if (wildcardIdx == -1)
+            return name.equals(template);
+
+        boolean isBeginning = true;
+        String beforeWildcard = "";
+        String afterWildcard = template;
+
+        while (wildcardIdx != -1) {
+
+            // match in sequence the non-wildcard chars in the template.
+            beforeWildcard = afterWildcard.substring(0, wildcardIdx);
+            afterWildcard = afterWildcard.substring(wildcardIdx + 1);
+
+            int beforeStartIdx = name.indexOf(beforeWildcard);
+            if ((beforeStartIdx == -1) ||
+                        (isBeginning && beforeStartIdx != 0)) {
+                return false;
+            }
+            isBeginning = false;
+
+            // update the match scope
+            name = name.substring(beforeStartIdx + beforeWildcard.length());
+            wildcardIdx = afterWildcard.indexOf('*');
+        }
+        return name.endsWith(afterWildcard);
+    }
+}