6695402: Jarsigner with multi-byte characters in class names
authorweijun
Wed, 27 Sep 2017 14:02:15 +0800
changeset 47273 f60a42d4b8cd
parent 47272 e0d686cdf608
child 47274 0e67ab18b511
6695402: Jarsigner with multi-byte characters in class names Reviewed-by: weijun Contributed-by: Philipp Kunz <philipp.kunz@paratix.ch>
src/java.base/share/classes/sun/security/util/ManifestDigester.java
test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java
--- a/src/java.base/share/classes/sun/security/util/ManifestDigester.java	Tue Sep 26 20:03:48 2017 -0700
+++ b/src/java.base/share/classes/sun/security/util/ManifestDigester.java	Wed Sep 27 14:02:15 2017 +0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2017, 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
@@ -28,6 +28,7 @@
 import java.security.*;
 import java.util.HashMap;
 import java.io.ByteArrayOutputStream;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 /**
  * This class is used to compute digests on sections of the Manifest.
@@ -112,8 +113,6 @@
         rawBytes = bytes;
         entries = new HashMap<>();
 
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
         Position pos = new Position();
 
         if (!findSection(0, pos))
@@ -131,50 +130,41 @@
 
             if (len > 6) {
                 if (isNameAttr(bytes, start)) {
-                    StringBuilder nameBuf = new StringBuilder(sectionLen);
-
-                    try {
-                        nameBuf.append(
-                            new String(bytes, start+6, len-6, "UTF8"));
+                    ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
+                    nameBuf.write(bytes, start+6, len-6);
 
-                        int i = start + len;
-                        if ((i-start) < sectionLen) {
-                            if (bytes[i] == '\r') {
-                                i += 2;
-                            } else {
-                                i += 1;
-                            }
+                    int i = start + len;
+                    if ((i-start) < sectionLen) {
+                        if (bytes[i] == '\r') {
+                            i += 2;
+                        } else {
+                            i += 1;
                         }
+                    }
 
-                        while ((i-start) < sectionLen) {
-                            if (bytes[i++] == ' ') {
-                                // name is wrapped
-                                int wrapStart = i;
-                                while (((i-start) < sectionLen)
-                                        && (bytes[i++] != '\n'));
-                                    if (bytes[i-1] != '\n')
-                                        return; // XXX: exception?
-                                    int wrapLen;
-                                    if (bytes[i-2] == '\r')
-                                        wrapLen = i-wrapStart-2;
-                                    else
-                                        wrapLen = i-wrapStart-1;
+                    while ((i-start) < sectionLen) {
+                        if (bytes[i++] == ' ') {
+                            // name is wrapped
+                            int wrapStart = i;
+                            while (((i-start) < sectionLen)
+                                    && (bytes[i++] != '\n'));
+                            if (bytes[i-1] != '\n')
+                                return; // XXX: exception?
+                            int wrapLen;
+                            if (bytes[i-2] == '\r')
+                                wrapLen = i-wrapStart-2;
+                            else
+                                wrapLen = i-wrapStart-1;
 
-                            nameBuf.append(new String(bytes, wrapStart,
-                                                      wrapLen, "UTF8"));
-                            } else {
-                                break;
-                            }
+                            nameBuf.write(bytes, wrapStart, wrapLen);
+                        } else {
+                            break;
                         }
+                    }
 
-                        entries.put(nameBuf.toString(),
-                            new Entry(start, sectionLen, sectionLenWithBlank,
+                    entries.put(new String(nameBuf.toByteArray(), UTF_8),
+                        new Entry(start, sectionLen, sectionLenWithBlank,
                                 rawBytes));
-
-                    } catch (java.io.UnsupportedEncodingException uee) {
-                        throw new IllegalStateException(
-                            "UTF8 not available on platform");
-                    }
                 }
             }
             start = pos.startOfNext;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java	Wed Sep 27 14:02:15 2017 +0800
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2017, 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 6695402
+ * @summary verify signatures of jars containing classes with names
+ *          with multi-byte unicode characters broken across lines
+ * @library /test/lib
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.jar.JarFile;
+import java.util.jar.Attributes.Name;
+import java.util.jar.JarEntry;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import jdk.test.lib.SecurityTools;
+import jdk.test.lib.util.JarUtils;
+
+public class LineBrokenMultiByteCharacter {
+
+    /**
+     * this name will break across lines in MANIFEST.MF at the
+     * middle of a two-byte utf-8 encoded character due to its e acute letter
+     * at its exact position.
+     *
+     * because no file with such a name exists {@link JarUtils} will add the
+     * name itself as contents to the jar entry which would have contained a
+     * compiled class in the original bug. For this test, the contents of the
+     * files contained in the jar file is not important as long as they get
+     * signed.
+     *
+     * @see #verifyClassNameLineBroken(JarFile, String)
+     */
+    static final String testClassName =
+            "LineBrokenMultiByteCharacterA1234567890B1234567890C123456789D12\u00E9xyz.class";
+
+    static final String anotherName =
+            "LineBrokenMultiByteCharacterA1234567890B1234567890C123456789D1234567890.class";
+
+    static final String alias = "a";
+    static final String keystoreFileName = "test.jks";
+    static final String manifestFileName = "MANIFEST.MF";
+
+    public static void main(String[] args) throws Exception {
+        prepare();
+
+        testSignJar("test.jar");
+        testSignJarNoManifest("test-no-manifest.jar");
+        testSignJarUpdate("test-update.jar", "test-updated.jar");
+    }
+
+    static void prepare() throws Exception {
+        SecurityTools.keytool("-keystore", keystoreFileName, "-genkeypair",
+                "-storepass", "changeit", "-keypass", "changeit", "-storetype",
+                "JKS", "-alias", alias, "-dname", "CN=X", "-validity", "366")
+            .shouldHaveExitValue(0);
+
+        Files.write(Paths.get(manifestFileName), (Name.
+                MANIFEST_VERSION.toString() + ": 1.0\r\n").getBytes(UTF_8));
+    }
+
+    static void testSignJar(String jarFileName) throws Exception {
+        JarUtils.createJar(jarFileName, manifestFileName, testClassName);
+        verifyJarSignature(jarFileName);
+    }
+
+    static void testSignJarNoManifest(String jarFileName) throws Exception {
+        JarUtils.createJar(jarFileName, testClassName);
+        verifyJarSignature(jarFileName);
+    }
+
+    static void testSignJarUpdate(
+            String initialFileName, String updatedFileName) throws Exception {
+        JarUtils.createJar(initialFileName, manifestFileName, anotherName);
+        SecurityTools.jarsigner("-keystore", keystoreFileName, "-storetype",
+                "JKS", "-storepass", "changeit", "-debug", initialFileName,
+                alias).shouldHaveExitValue(0);
+        JarUtils.updateJar(initialFileName, updatedFileName, testClassName);
+        verifyJarSignature(updatedFileName);
+    }
+
+    static void verifyJarSignature(String jarFileName) throws Exception {
+        // actually sign the jar
+        SecurityTools.jarsigner("-keystore", keystoreFileName, "-storetype",
+                "JKS", "-storepass", "changeit", "-debug", jarFileName, alias)
+            .shouldHaveExitValue(0);
+
+        try (
+            JarFile jar = new JarFile(jarFileName);
+        ) {
+            verifyClassNameLineBroken(jar, testClassName);
+            verifyCodeSigners(jar, jar.getJarEntry(testClassName));
+        }
+    }
+
+    /**
+     * it would be too easy to miss the actual test case by just renaming an
+     * identifier so that the multi-byte encoded character would not any longer
+     * be broken across a line break.
+     *
+     * this check here verifies that the actual test case is tested based on
+     * the manifest and not based on the signature file because at the moment,
+     * the signature file does not even contain the desired entry at all.
+     *
+     * this relies on {@link java.util.jar.Manifest} breaking lines unaware
+     * of bytes that belong to the same multi-byte utf characters.
+     */
+    static void verifyClassNameLineBroken(JarFile jar, String className)
+            throws IOException {
+        byte[] eAcute = "\u00E9".getBytes(UTF_8);
+        byte[] eAcuteBroken =
+                new byte[] {eAcute[0], '\r', '\n', ' ', eAcute[1]};
+
+        if (jar.getManifest().getAttributes(className) == null) {
+            throw new AssertionError(className + " not found in manifest");
+        }
+
+        JarEntry manifestEntry = jar.getJarEntry(JarFile.MANIFEST_NAME);
+        try (
+            InputStream manifestIs = jar.getInputStream(manifestEntry);
+        ) {
+            int bytesMatched = 0;
+            for (int b = manifestIs.read(); b > -1; b = manifestIs.read()) {
+                if ((byte) b == eAcuteBroken[bytesMatched]) {
+                    bytesMatched++;
+                    if (bytesMatched == eAcuteBroken.length) {
+                        break;
+                    }
+                } else {
+                    bytesMatched = 0;
+                }
+            }
+            if (bytesMatched < eAcuteBroken.length) {
+                throw new AssertionError("self-test failed: multi-byte "
+                        + "utf-8 character not broken across lines");
+            }
+        }
+    }
+
+    static void verifyCodeSigners(JarFile jar, JarEntry jarEntry)
+            throws IOException {
+        // codeSigners is initialized only after the entry has been read
+        try (
+            InputStream inputStream = jar.getInputStream(jarEntry);
+        ) {
+            inputStream.readAllBytes();
+        }
+
+        // a check for the presence of code signers is sufficient to check
+        // bug JDK-6695402. no need to also verify the actual code signers
+        // attributes here.
+        if (jarEntry.getCodeSigners() == null
+                || jarEntry.getCodeSigners().length == 0) {
+            throw new AssertionError(
+                    "no signing certificate found for " + jarEntry.getName());
+        }
+    }
+
+}