# HG changeset patch # User weijun # Date 1506492135 -28800 # Node ID f60a42d4b8cd309193b8d38576cca5b5ddafbcdc # Parent e0d686cdf608f7b59ef925ea551a06cd5050b17d 6695402: Jarsigner with multi-byte characters in class names Reviewed-by: weijun Contributed-by: Philipp Kunz diff -r e0d686cdf608 -r f60a42d4b8cd src/java.base/share/classes/sun/security/util/ManifestDigester.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; diff -r e0d686cdf608 -r f60a42d4b8cd test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java --- /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()); + } + } + +}