6695402: Jarsigner with multi-byte characters in class names
Reviewed-by: weijun
Contributed-by: Philipp Kunz <philipp.kunz@paratix.ch>
--- 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());
+ }
+ }
+
+}