changeset 9561:8461944f5e30 jdk7u181-b00

8020842: IDN do not throw IAE when hostname ends with a trailing dot 8024068: sun/security/ssl/javax/net/ssl/ServerName/IllegalSNIName.java fails Reviewed-by: weijun, michaelm
author xuelei
date Thu, 12 Apr 2018 04:24:20 +0100
parents a6227be4f236
children deaf411f47b1
files src/share/classes/sun/security/util/HostnameChecker.java test/sun/security/ssl/javax/net/ssl/ServerName/IllegalSNIName.java
diffstat 2 files changed, 367 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/sun/security/util/HostnameChecker.java	Sun Mar 04 16:33:27 2018 +0000
+++ b/src/share/classes/sun/security/util/HostnameChecker.java	Thu Apr 12 04:24:20 2018 +0100
@@ -25,10 +25,14 @@
 
 package sun.security.util;
 
+import java.io.InputStream;
 import java.io.IOException;
 import java.net.IDN;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.*;
 
 import java.security.Principal;
@@ -36,10 +40,12 @@
 
 import javax.security.auth.x500.X500Principal;
 
+import sun.net.idn.Punycode;
+import sun.net.idn.StringPrep;
+import sun.net.util.IPAddressUtil;
 import sun.security.ssl.Krb5Helper;
 import sun.security.x509.X500Name;
-
-import sun.net.util.IPAddressUtil;
+import sun.text.normalizer.UCharacterIterator;
 
 /**
  * Class to check hostnames against the names specified in a certificate as
@@ -390,10 +396,12 @@
     }
 
     // check the validity of the string hostname
-    private void checkHostName(String hostname) {
-        hostname = IDN.toASCII(Objects.requireNonNull(hostname,
-                                   "Server name value of host_name cannot be null"),
-                               IDN.USE_STD3_ASCII_RULES);
+    private static void checkHostName(String hostname) {
+        hostname = toASCII(Objects.requireNonNull(hostname,
+                                                  "Server name value of host_name cannot be null"),
+                           IDN.USE_STD3_ASCII_RULES);
+        // Check it can be encoded to ASCII
+        hostname.getBytes(StandardCharsets.US_ASCII);
 
         if (hostname.isEmpty()) {
             throw new IllegalArgumentException(
@@ -406,4 +414,272 @@
         }
     }
 
+    /*
+     * Local versions of toASCII(String,int), toASCIIInternal(String, int)
+     * and their helper methods with 8020842 fix added. Can't change the
+     * public version due to compatibility.
+     */
+
+    // ACE Prefix is "xn--"
+    private static final String ACE_PREFIX = "xn--";
+    private static final int ACE_PREFIX_LENGTH = ACE_PREFIX.length();
+
+    private static final int MAX_LABEL_LENGTH   = 63;
+
+    // single instance of nameprep
+    private static StringPrep namePrep = null;
+
+    static {
+        InputStream stream = null;
+
+        try {
+            final String IDN_PROFILE = "uidna.spp";
+            if (System.getSecurityManager() != null) {
+                stream = AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
+                    public InputStream run() {
+                        return StringPrep.class.getResourceAsStream(IDN_PROFILE);
+                    }
+                });
+            } else {
+                stream = StringPrep.class.getResourceAsStream(IDN_PROFILE);
+            }
+
+            namePrep = new StringPrep(stream);
+            stream.close();
+        } catch (IOException e) {
+            // should never reach here
+            assert false;
+        }
+    }
+
+    /**
+     * Translates a string from Unicode to ASCII Compatible Encoding (ACE),
+     * as defined by the ToASCII operation of <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>.
+     *
+     * <p>ToASCII operation can fail. ToASCII fails if any step of it fails.
+     * If ToASCII operation fails, an IllegalArgumentException will be thrown.
+     * In this case, the input string should not be used in an internationalized domain name.
+     *
+     * <p> A label is an individual part of a domain name. The original ToASCII operation,
+     * as defined in RFC 3490, only operates on a single label. This method can handle
+     * both label and entire domain name, by assuming that labels in a domain name are
+     * always separated by dots. The following characters are recognized as dots:
+     * &#0092;u002E (full stop), &#0092;u3002 (ideographic full stop), &#0092;uFF0E (fullwidth full stop),
+     * and &#0092;uFF61 (halfwidth ideographic full stop). if dots are
+     * used as label separators, this method also changes all of them to &#0092;u002E (full stop)
+     * in output translated string.
+     *
+     * @param input     the string to be processed
+     * @param flag      process flag; can be 0 or any logical OR of possible flags
+     *
+     * @return          the translated {@code String}
+     *
+     * @throws IllegalArgumentException   if the input string doesn't conform to RFC 3490 specification
+     */
+    private static String toASCII(String input, int flag)
+    {
+        int p = 0, q = 0;
+        StringBuffer out = new StringBuffer();
+
+        if (isRootLabel(input)) {
+            return ".";
+        }
+
+        while (p < input.length()) {
+            q = searchDots(input, p);
+            out.append(toASCIIInternal(input.substring(p, q),  flag));
+            if (q != (input.length())) {
+               // has more labels, or keep the trailing dot as at present
+               out.append('.');
+            }
+            p = q + 1;
+        }
+
+        return out.toString();
+    }
+
+    //
+    // toASCII operation; should only apply to a single label
+    //
+    private static String toASCIIInternal(String label, int flag)
+    {
+        // step 1
+        // Check if the string contains code points outside the ASCII range 0..0x7c.
+        boolean isASCII  = isAllASCII(label);
+        StringBuffer dest;
+
+        // step 2
+        // perform the nameprep operation; flag ALLOW_UNASSIGNED is used here
+        if (!isASCII) {
+            UCharacterIterator iter = UCharacterIterator.getInstance(label);
+            try {
+                dest = namePrep.prepare(iter, flag);
+            } catch (java.text.ParseException e) {
+                throw new IllegalArgumentException(e);
+            }
+        } else {
+            dest = new StringBuffer(label);
+        }
+
+        // step 8, move forward to check the smallest number of the code points
+        // the length must be inside 1..63
+        if (dest.length() == 0) {
+            throw new IllegalArgumentException(
+                        "Empty label is not a legal name");
+        }
+
+        // step 3
+        // Verify the absence of non-LDH ASCII code points
+        //   0..0x2c, 0x2e..0x2f, 0x3a..0x40, 0x5b..0x60, 0x7b..0x7f
+        // Verify the absence of leading and trailing hyphen
+        boolean useSTD3ASCIIRules = ((flag & IDN.USE_STD3_ASCII_RULES) != 0);
+        if (useSTD3ASCIIRules) {
+            for (int i = 0; i < dest.length(); i++) {
+                int c = dest.charAt(i);
+                if (isNonLDHAsciiCodePoint(c)) {
+                    throw new IllegalArgumentException(
+                        "Contains non-LDH ASCII characters");
+                }
+            }
+
+            if (dest.charAt(0) == '-' ||
+                dest.charAt(dest.length() - 1) == '-') {
+
+                throw new IllegalArgumentException(
+                        "Has leading or trailing hyphen");
+            }
+        }
+
+        if (!isASCII) {
+            // step 4
+            // If all code points are inside 0..0x7f, skip to step 8
+            if (!isAllASCII(dest.toString())) {
+                // step 5
+                // verify the sequence does not begin with ACE prefix
+                if(!startsWithACEPrefix(dest)){
+
+                    // step 6
+                    // encode the sequence with punycode
+                    try {
+                        dest = Punycode.encode(dest, null);
+                    } catch (java.text.ParseException e) {
+                        throw new IllegalArgumentException(e);
+                    }
+
+                    dest = toASCIILower(dest);
+
+                    // step 7
+                    // prepend the ACE prefix
+                    dest.insert(0, ACE_PREFIX);
+                } else {
+                    throw new IllegalArgumentException("The input starts with the ACE Prefix");
+                }
+
+            }
+        }
+
+        // step 8
+        // the length must be inside 1..63
+        if (dest.length() > MAX_LABEL_LENGTH) {
+            throw new IllegalArgumentException("The label in the input is too long");
+        }
+
+        return dest.toString();
+    }
+
+    //
+    // LDH stands for "letter/digit/hyphen", with characters restricted to the
+    // 26-letter Latin alphabet <A-Z a-z>, the digits <0-9>, and the hyphen
+    // <->.
+    // Non LDH refers to characters in the ASCII range, but which are not
+    // letters, digits or the hypen.
+    //
+    // non-LDH = 0..0x2C, 0x2E..0x2F, 0x3A..0x40, 0x5B..0x60, 0x7B..0x7F
+    //
+    private static boolean isNonLDHAsciiCodePoint(int ch){
+        return (0x0000 <= ch && ch <= 0x002C) ||
+               (0x002E <= ch && ch <= 0x002F) ||
+               (0x003A <= ch && ch <= 0x0040) ||
+               (0x005B <= ch && ch <= 0x0060) ||
+               (0x007B <= ch && ch <= 0x007F);
+    }
+
+    //
+    // search dots in a string and return the index of that character;
+    // or if there is no dots, return the length of input string
+    // dots might be: \u002E (full stop), \u3002 (ideographic full stop), \uFF0E (fullwidth full stop),
+    // and \uFF61 (halfwidth ideographic full stop).
+    //
+    private static int searchDots(String s, int start) {
+        int i;
+        for (i = start; i < s.length(); i++) {
+            if (isLabelSeparator(s.charAt(i))) {
+                break;
+            }
+        }
+
+        return i;
+    }
+
+    //
+    // to check if a string is a root label, ".".
+    //
+    private static boolean isRootLabel(String s) {
+        return (s.length() == 1 && isLabelSeparator(s.charAt(0)));
+    }
+
+    //
+    // to check if a character is a label separator, i.e. a dot character.
+    //
+    private static boolean isLabelSeparator(char c) {
+        return (c == '.' || c == '\u3002' || c == '\uFF0E' || c == '\uFF61');
+    }
+
+    //
+    // to check if a string starts with ACE-prefix
+    //
+    private static boolean startsWithACEPrefix(StringBuffer input){
+        boolean startsWithPrefix = true;
+
+        if(input.length() < ACE_PREFIX_LENGTH){
+            return false;
+        }
+        for(int i = 0; i < ACE_PREFIX_LENGTH; i++){
+            if(toASCIILower(input.charAt(i)) != ACE_PREFIX.charAt(i)){
+                startsWithPrefix = false;
+            }
+        }
+        return startsWithPrefix;
+    }
+
+    private static char toASCIILower(char ch){
+        if('A' <= ch && ch <= 'Z'){
+            return (char)(ch + 'a' - 'A');
+        }
+        return ch;
+    }
+
+    private static StringBuffer toASCIILower(StringBuffer input){
+        StringBuffer dest = new StringBuffer();
+        for(int i = 0; i < input.length();i++){
+            dest.append(toASCIILower(input.charAt(i)));
+        }
+        return dest;
+    }
+
+    //
+    // to check if a string only contains US-ASCII code point
+    //
+    private static boolean isAllASCII(String input) {
+        boolean isASCII = true;
+        for (int i = 0; i < input.length(); i++) {
+            int c = input.charAt(i);
+            if (c > 0x7F) {
+                isASCII = false;
+                break;
+            }
+        }
+        return isASCII;
+    }
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/sun/security/ssl/javax/net/ssl/ServerName/IllegalSNIName.java	Thu Apr 12 04:24:20 2018 +0100
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 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.
+ *
+ * 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.
+ */
+
+/*
+ * @test
+ * @bug 8020842
+ * @summary SNIHostName does not throw IAE when hostname ends
+ *          with a trailing dot
+ */
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import sun.security.util.HostnameChecker;
+
+public class IllegalSNIName {
+
+    private static final boolean DEBUG = true;
+
+    public static void main(String[] args) throws Throwable {
+        String[] illegalNames = {
+                "example\u3002\u3002com",
+                "example..com",
+                "com\u3002",
+                "com.",
+                "."
+            };
+
+        Method m = HostnameChecker.class.getDeclaredMethod("checkHostName",
+                                                           String.class);
+        m.setAccessible(true);
+        // Old versions of HostnameChecker.checkHostName are not static
+        // so we need an instance of the class
+        Object instance = null;
+        if (!Modifier.isStatic(m.getModifiers())) {
+            instance = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS);
+        }
+        for (String name : illegalNames) {
+            Throwable t = null;
+            try {
+                if (DEBUG) {
+                    System.err.println("Checking " + name);
+                }
+                m.invoke(instance, name);
+                throw new Exception(
+                    "Expected to get IllegalArgumentException for " + name);
+            } catch (IllegalArgumentException iae) {
+                // That's the right behavior.
+                t = iae;
+            } catch (InvocationTargetException ite) {
+                Throwable target = ite.getTargetException();
+                if (!(target instanceof IllegalArgumentException)) {
+                    throw target;
+                } else {
+                    t = target;
+                }
+            }
+            if (DEBUG && t != null) {
+                System.err.println(name + " correctly threw: ");
+                t.printStackTrace();
+            }
+        }
+    }
+}