--- /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);
+ }
+}