jdk/src/java.base/share/classes/sun/security/util/HostnameChecker.java
changeset 44760 61b03b960583
parent 32649 2ee9017c7597
child 45981 2ef418883ade
--- a/jdk/src/java.base/share/classes/sun/security/util/HostnameChecker.java	Thu Feb 23 12:08:44 2017 +0000
+++ b/jdk/src/java.base/share/classes/sun/security/util/HostnameChecker.java	Mon Mar 20 07:38:52 2017 -0400
@@ -28,18 +28,16 @@
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.*;
-
 import java.security.Principal;
 import java.security.cert.*;
-
+import java.util.*;
 import javax.security.auth.x500.X500Principal;
 
+import sun.net.util.IPAddressUtil;
 import sun.security.ssl.ClientKeyExchangeService;
+import sun.security.ssl.Debug;
 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.
@@ -61,6 +59,8 @@
     private static final int ALTNAME_DNS = 2;
     private static final int ALTNAME_IP  = 7;
 
+    private static final Debug debug = Debug.getInstance("ssl");
+
     // the algorithm to follow to perform the check. Currently unused.
     private final byte checkType;
 
@@ -84,18 +84,27 @@
     /**
      * Perform the check.
      *
-     * @exception CertificateException if the name does not match any of
-     * the names specified in the certificate
+     * @param expectedName the expected host name or ip address
+     * @param cert the certificate to check against
+     * @param chainsToPublicCA true if the certificate chains to a public
+     *     root CA (as pre-installed in the cacerts file)
+     * @throws CertificateException if the name does not match any of
+     *     the names specified in the certificate
      */
-    public void match(String expectedName, X509Certificate cert)
-            throws CertificateException {
+    public void match(String expectedName, X509Certificate cert,
+                      boolean chainsToPublicCA) throws CertificateException {
         if (isIpAddress(expectedName)) {
            matchIP(expectedName, cert);
         } else {
-           matchDNS(expectedName, cert);
+           matchDNS(expectedName, cert, chainsToPublicCA);
         }
     }
 
+    public void match(String expectedName, X509Certificate cert)
+            throws CertificateException {
+        match(expectedName, cert, false);
+    }
+
     /**
      * Perform the check for Kerberos.
      */
@@ -185,20 +194,21 @@
      * 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
+     * [RFC5280].  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)
+    private void matchDNS(String expectedName, X509Certificate cert,
+                          boolean chainsToPublicCA)
             throws CertificateException {
         Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
         if (subjAltNames != null) {
             boolean foundDNS = false;
-            for ( List<?> next : subjAltNames) {
+            for (List<?> next : subjAltNames) {
                 if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) {
                     foundDNS = true;
                     String dnsName = (String)next.get(1);
-                    if (isMatched(expectedName, dnsName)) {
+                    if (isMatched(expectedName, dnsName, chainsToPublicCA)) {
                         return;
                     }
                 }
@@ -215,7 +225,8 @@
                                                     (X500Name.commonName_oid);
         if (derValue != null) {
             try {
-                if (isMatched(expectedName, derValue.getAsString())) {
+                if (isMatched(expectedName, derValue.getAsString(),
+                              chainsToPublicCA)) {
                     return;
                 }
             } catch (IOException e) {
@@ -261,7 +272,11 @@
      * The <code>template</code> parameter
      * may contain the wildcard character *
      */
-    private boolean isMatched(String name, String template) {
+    private boolean isMatched(String name, String template,
+                              boolean chainsToPublicCA) {
+        if (hasIllegalWildcard(name, template, chainsToPublicCA)) {
+            return false;
+        }
         if (checkType == TYPE_TLS) {
             return matchAllWildcards(name, template);
         } else if (checkType == TYPE_LDAP) {
@@ -271,6 +286,61 @@
         }
     }
 
+    /**
+     * Returns true if the template contains an illegal wildcard character.
+     */
+    private static boolean hasIllegalWildcard(String domain, String template,
+                                              boolean chainsToPublicCA) {
+        // not ok if it is a single wildcard character or "*."
+        if (template.equals("*") || template.equals("*.")) {
+            if (debug != null) {
+                debug.println("Certificate domain name has illegal single " +
+                              "wildcard character: " + template);
+            }
+            return true;
+        }
+
+        int lastWildcardIndex = template.lastIndexOf("*");
+
+        // ok if it has no wildcard character
+        if (lastWildcardIndex == -1) {
+            return false;
+        }
+
+        String afterWildcard = template.substring(lastWildcardIndex);
+        int firstDotIndex = afterWildcard.indexOf(".");
+
+        // not ok if there is no dot after wildcard (ex: "*com")
+        if (firstDotIndex == -1) {
+            if (debug != null) {
+                debug.println("Certificate domain name has illegal wildcard, " +
+                              "no dot after wildcard character: " + template);
+            }
+            return true;
+        }
+
+        // If the wildcarded domain is a top-level domain under which names
+        // can be registered, then a wildcard is not allowed.
+
+        if (!chainsToPublicCA) {
+            return false; // skip check for non-public certificates
+        }
+        Optional<RegisteredDomain> rd = RegisteredDomain.from(domain)
+                .filter(d -> d.type() == RegisteredDomain.Type.ICANN);
+
+        if (rd.isPresent()) {
+            String wDomain = afterWildcard.substring(firstDotIndex + 1);
+            if (rd.get().publicSuffix().equalsIgnoreCase(wDomain)) {
+                if (debug != null) {
+                    debug.println("Certificate domain name has illegal " +
+                                  "wildcard for public suffix: " + template);
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
 
     /**
      * Returns true if name matches against template.<p>
@@ -317,9 +387,9 @@
         name = name.toLowerCase(Locale.ENGLISH);
         template = template.toLowerCase(Locale.ENGLISH);
 
-        // Retreive leftmost component
-        int templateIdx = template.indexOf('.');
-        int nameIdx = name.indexOf('.');
+        // Retrieve leftmost component
+        int templateIdx = template.indexOf(".");
+        int nameIdx = name.indexOf(".");
 
         if (templateIdx == -1)
             templateIdx = template.length();
@@ -344,7 +414,7 @@
      */
     private static boolean matchWildCards(String name, String template) {
 
-        int wildcardIdx = template.indexOf('*');
+        int wildcardIdx = template.indexOf("*");
         if (wildcardIdx == -1)
             return name.equals(template);
 
@@ -367,7 +437,7 @@
 
             // update the match scope
             name = name.substring(beforeStartIdx + beforeWildcard.length());
-            wildcardIdx = afterWildcard.indexOf('*');
+            wildcardIdx = afterWildcard.indexOf("*");
         }
         return name.endsWith(afterWildcard);
     }