Great.

Some suggestions, just my own habit, you are free to decide yourself:

1. In ManifestDigester.java, I'd rather define nameBuf inside the if 
(isNameAttr(bytes, start)) block.

2. I normally don't use "import static" unless I can save a lot of keystrokes 
and still do not confuse anyone. Both in the test and in ManifestDigester.java.

3. In the test, there is no need to write "@run main MultibyteUnicodeName" if 
it is the only action (i.e. no other build/compile) and there is no modifier on 
main (i.e. no othervm/timeout etc).

Several tiny problems:

1. No need for @modules in the test. Maybe you used some internal classes in 
your previous version?

2. In the new consolidated repo, the test should be inside test/jdk/sun/..., 
please note the "jdk" after "test".

3. Please update the copyright years.

4. In the test, there should be no "JDK-" in the @test tag.

Thanks
Max

> On Sep 24, 2017, at 7:51 PM, Philipp Kunz <philipp.k...@paratix.ch> wrote:
> 
> Hi Max and Vincent
> 
> Thank you for your suggestions. It sure looks better now. I hope this time I 
> got the patch added in the right format.
> 
> diff -r ddc25f646c2e 
> src/java.base/share/classes/sun/security/util/ManifestDigester.java
> --- a/src/java.base/share/classes/sun/security/util/ManifestDigester.java    
> Thu Aug 31 22:21:20 2017 -0700
> +++ b/src/java.base/share/classes/sun/security/util/ManifestDigester.java    
> Sun Sep 24 10:34:00 2017 +0200
> @@ -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,7 +113,7 @@
>          rawBytes = bytes;
>          entries = new HashMap<>();
>  
> -        ByteArrayOutputStream baos = new ByteArrayOutputStream();
> +        ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
>  
>          Position pos = new Position();
>  
> @@ -131,50 +132,40 @@
>  
>              if (len > 6) {
>                  if (isNameAttr(bytes, start)) {
> -                    StringBuilder nameBuf = new StringBuilder(sectionLen);
> +                    nameBuf.reset();
> +                    nameBuf.write(bytes, start+6, len-6);
>  
> -                    try {
> -                        nameBuf.append(
> -                            new String(bytes, start+6, len-6, "UTF8"));
> +                    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;
> +
> +                            nameBuf.write(bytes, wrapStart, wrapLen);
> +                        } else {
> +                            break;
>                          }
> +                    }
>  
> -                        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;
> -                            }
> -                        }
> -
> -                        entries.put(nameBuf.toString(),
> -                            new Entry(start, sectionLen, sectionLenWithBlank,
> -                                rawBytes));
> -
> -                    } catch (java.io.UnsupportedEncodingException uee) {
> -                        throw new IllegalStateException(
> -                            "UTF8 not available on platform");
> -                    }
> +                    entries.put(new String(nameBuf.toByteArray(), UTF_8),
> +                        new Entry(start, sectionLen, sectionLenWithBlank, 
> rawBytes));
>                  }
>              }
>              start = pos.startOfNext;
> diff -r ddc25f646c2e 
> test/sun/security/tools/jarsigner/MultibyteUnicodeName.java
> --- /dev/null    Thu Jan 01 00:00:00 1970 +0000
> +++ b/test/sun/security/tools/jarsigner/MultibyteUnicodeName.java    Sun Sep 
> 24 10:34:00 2017 +0200
> @@ -0,0 +1,209 @@
> +/*
> + * Copyright (c) 1999, 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 JDK-6695402
> + * @summary verify signatures of jars containing classes with names with 
> multi-
> + *          byte unicode characters broken across lines
> + * @library /test/lib
> + * @modules jdk.jartool/sun.tools.jar
> + *          jdk.jartool/sun.security.tools.jarsigner
> + * @run main MultibyteUnicodeName
> + */
> +
> +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.Manifest;
> +import java.util.Arrays;
> +import java.util.Map;
> +import java.util.jar.Attributes.Name;
> +import java.util.jar.JarEntry;
> +
> +import static java.nio.charset.StandardCharsets.UTF_8;
> +
> +import static jdk.test.lib.SecurityTools.jarsigner;
> +import static jdk.test.lib.SecurityTools.keytool;
> +import static jdk.test.lib.util.JarUtils.createJar;
> +import static jdk.test.lib.util.JarUtils.updateJar;
> +
> +public class MultibyteUnicodeName {
> +
> +    /**
> +     * this class's name will break across lines in the jar manifest at the 
> middle of
> +     * a two-byte utf encoded character due to its e acute letter at its 
> exact position.
> +     * 
> +     * @see also eAcute in {@link 
> MultibyteUnicodeName#verifyClassNameLineBroken(String)}
> +     */
> +    static class A1234567890B1234567890C1234567890D12345678éxyz { }
> +    static class A1234567890B1234567890C1234567890D12345678exyz { }
> +
> +    static final String refClassFilename = 
> MultibyteUnicodeName.class.getSimpleName() + 
> +            "$" + 
> A1234567890B1234567890C1234567890D12345678exyz.class.getSimpleName() + 
> ".class";
> +    static final String testClassFilename = 
> MultibyteUnicodeName.class.getSimpleName() + 
> +            "$" + 
> A1234567890B1234567890C1234567890D12345678éxyz.class.getSimpleName() + 
> ".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 {
> +        try {
> +            prepare();
> +            
> +            testSignJar("test.jar");
> +            testSignJarNoManifest("test-no-manifest.jar");
> +            testSignJarUpdate("test-update.jar", "test-updated.jar");
> +            
> +        } finally {
> +            Files.deleteIfExists(Paths.get(keystoreFileName));
> +            Files.deleteIfExists(Paths.get(ManifestFileName));
> +            Files.deleteIfExists(Paths.get(refClassFilename));
> +            Files.deleteIfExists(Paths.get(testClassFilename));
> +        }
> +    }
> +    
> +    static void prepare() throws Exception {
> +        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.name()));
> +        
> Files.copy(MultibyteUnicodeName.class.getResourceAsStream(refClassFilename), 
> +                Paths.get(refClassFilename));
> +        
> Files.copy(MultibyteUnicodeName.class.getResourceAsStream(testClassFilename), 
> +                Paths.get(testClassFilename));
> +    }
> +    
> +    static void testSignJar(String jarFileName) throws Exception {
> +        try {
> +            createJar(jarFileName, ManifestFileName, refClassFilename, 
> testClassFilename);
> +            verifyJarSignature(jarFileName);
> +
> +        } finally {
> +            Files.deleteIfExists(Paths.get(jarFileName));
> +        }
> +    }
> +
> +    static void testSignJarNoManifest(String jarFileName) throws Exception {
> +        try {
> +            createJar(jarFileName, refClassFilename, testClassFilename);
> +            verifyJarSignature(jarFileName);
> +
> +        } finally {
> +            Files.deleteIfExists(Paths.get(jarFileName));
> +        }
> +    }
> +    
> +    static void testSignJarUpdate(String initialFileName, String 
> updatedFileName) throws Exception {
> +        try {
> +            createJar(initialFileName, ManifestFileName, refClassFilename);
> +            jarsigner("-keystore", keystoreFileName, "-storetype", "JKS",
> +                    "-storepass", "changeit", "-debug", initialFileName, 
> alias)
> +                .shouldHaveExitValue(0);
> +            updateJar(initialFileName, updatedFileName, testClassFilename);
> +            verifyJarSignature(updatedFileName);
> +
> +        } finally {
> +            Files.deleteIfExists(Paths.get(initialFileName));
> +            Files.deleteIfExists(Paths.get(updatedFileName));
> +        }
> +    }
> +    
> +    static void verifyJarSignature(String jarFileName) throws Exception {
> +        // actually sign the jar
> +        jarsigner("-keystore", keystoreFileName, "-storetype", "JKS",
> +                "-storepass", "changeit", "-debug", jarFileName, alias)
> +                .shouldHaveExitValue(0);
> +
> +        try (
> +            JarFile jar = new JarFile(jarFileName);
> +        ) {
> +            verifyClassNameLineBroken(jar, testClassFilename);
> +            verifyCodeSigners(jar, jar.getJarEntry(testClassFilename));
> +            verifyCodeSigners(jar, jar.getJarEntry(refClassFilename));
> +        }
> +    }
> +    
> +    /**
> +     * 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 test 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 the Manifest breaking lines unaware of bytes that 
> belong to the same
> +     * multi-ybte utf characters.
> +     */
> +    static void verifyClassNameLineBroken(JarFile jar, String className) 
> throws IOException {
> +        byte[] eAcute = "é".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);
> +        ) {
> +            while (inputStream.read() > -1);
> +            
> +            if (jarEntry.getCodeSigners() == null || 
> jarEntry.getCodeSigners().length == 0) {
> +                throw new AssertionError("no signing certificate found for " 
> + jarEntry.getName());
> +            }
> +        }
> +        
> +        // 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.
> +    }
> +    
> +}
> 
> Regards,
> Philipp
> 
> 
> 
> On 20.09.2017 03:41, Weijun Wang wrote:
>> Hi Philipp
>> 
>> The change mostly looks fine. You might want to put everything into a patch 
>> file so Vincent can recreate a webrev and post it to cr.openjdk.java.net.
>> 
>> One thing I would suggest for the test is that instead of using jarsigner 
>> -verify and check the text in output you can open it as a JarFile, consume 
>> the content of an entry, and look into its getCertificates().
>> 
>> Also, you might want to reuse test/lib/jdk/test/lib/SecurityTools.java, and 
>> there is also JarUtils.java you can use. Maybe Files.copy(InputStream,Path) 
>> can be used in copyClassToFile().
>> 
>> Thanks
>> Max
>> 
>> 
>>> On Sep 20, 2017, at 7:41 AM, Philipp Kunz <philipp.k...@paratix.ch>
>>>  wrote:
>>> 
>>> Hello Vincent
>>> 
>>> Here may be the fix for JDK-6695402. First a test.
>>> 
>>> BEGIN /jdk10/jdk/test/sun/security/tools/jarsigner/MultibyteUnicodeName.java
>>> /*
>>>  * Copyright (c) 1999, 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 JDK-6695402
>>>  * @summary verify signatures of jars containing classes with names with 
>>> multi-
>>>  *          byte unicode characters broken across lines
>>>  * @library /test/lib
>>>  * @modules jdk.jartool/sun.tools.jar
>>>  *          jdk.jartool/sun.security.tools.jarsigner
>>>  * @build jdk.test.lib.JDKToolLauncher
>>>  *        jdk.test.lib.process.*
>>>  * @run main MultibyteUnicodeName
>>>  */
>>> 
>>> import java.io.ByteArrayOutputStream;
>>> import java.io.IOException;
>>> import java.io.InputStream;
>>> import java.io.UnsupportedEncodingException;
>>> import java.nio.file.Files;
>>> import java.nio.file.Paths;
>>> import java.util.jar.JarFile;
>>> import java.util.jar.Manifest;
>>> import java.util.stream.Collectors;
>>> import java.util.Arrays;
>>> import java.util.Map;
>>> import java.util.jar.Attributes.Name;
>>> import java.util.jar.JarEntry;
>>> 
>>> import static java.nio.charset.StandardCharsets.UTF_8;
>>> 
>>> import sun.security.tools.jarsigner.Resources;
>>> 
>>> import jdk.test.lib.JDKToolLauncher;
>>> import jdk.test.lib.process.OutputAnalyzer;
>>> import jdk.test.lib.process.ProcessTools;
>>> 
>>> public class MultibyteUnicodeName {
>>>     
>>>     public static void main(String[] args) throws Throwable {
>>>         try {
>>>             prepare();
>>>             
>>>             testSignJar("test.jar");
>>>             testSignJarNoManifest("test-no-manifest.jar");
>>>             testSignJarUpdate("test-update.jar");
>>>             testSignJarWithIndex("test-index.jar");
>>>             testSignJarAddIndex("test-add-index.jar");
>>>             
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(keystoreFileName));
>>>             Files.deleteIfExists(Paths.get(ManifestFileName));
>>>             Files.deleteIfExists(Paths.get(refClassFilename));
>>>             Files.deleteIfExists(Paths.get(testClassFilename));
>>>         }
>>>     }
>>>     
>>>     static final String alias = "a";
>>>     static final String keystoreFileName = "test.jks";
>>>     static final String ManifestFileName = "MANIFEST.MF";
>>>     
>>>     static class A1234567890B1234567890C1234567890D12345678exyz { }
>>>     static class A1234567890B1234567890C1234567890D12345678éxyz { }
>>>     
>>>     static final String refClassFilename = 
>>> MultibyteUnicodeName.class.getSimpleName() + 
>>>             "$" + 
>>> A1234567890B1234567890C1234567890D12345678exyz.class.getSimpleName() + 
>>> ".class";
>>>     static final String testClassFilename = 
>>> MultibyteUnicodeName.class.getSimpleName() + 
>>>             "$" + 
>>> A1234567890B1234567890C1234567890D12345678éxyz.class.getSimpleName() + 
>>> ".class";
>>>     
>>>     static void prepare() throws Throwable {
>>>         tool("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.name()));
>>>         copyClassToFile(refClassFilename);
>>>         copyClassToFile(testClassFilename);
>>>     }
>>>     
>>>     static void copyClassToFile(String classFilename) throws IOException {
>>>         try (
>>>             InputStream asStream = 
>>> MultibyteUnicodeName.class.getResourceAsStream(classFilename);
>>>             ByteArrayOutputStream buf = new ByteArrayOutputStream();
>>>         ) {
>>>             int b;
>>>             while ((b = asStream.read()) != -1) buf.write(b);
>>>             Files.write(Paths.get(classFilename), buf.toByteArray());
>>>         }
>>>     }
>>> 
>>>     static void testSignJar(String jarFileName) throws Throwable {
>>>         try {
>>>             tool("jar", "cvfm", jarFileName, 
>>>                     ManifestFileName, refClassFilename, testClassFilename)
>>>                     .shouldHaveExitValue(0);
>>>             verifyJarSignature(jarFileName);
>>> 
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(jarFileName));
>>>         }
>>>     }
>>> 
>>>     static void testSignJarNoManifest(String jarFileName) throws Throwable {
>>>         try {
>>>             tool("jar", "cvf", jarFileName, refClassFilename, 
>>> testClassFilename)
>>>                 .shouldHaveExitValue(0);
>>>             verifyJarSignature(jarFileName);
>>> 
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(jarFileName));
>>>         }
>>>     }
>>>     
>>>     static void testSignJarUpdate(String jarFileName) throws Throwable {
>>>         try {
>>>             tool("jar", "cvfm", jarFileName, ManifestFileName, 
>>> refClassFilename)
>>>                 .shouldHaveExitValue(0);
>>>             tool("jarsigner", "-keystore", keystoreFileName, "-storetype", 
>>> "JKS",
>>>                     "-storepass", "changeit", "-debug", jarFileName, alias)
>>>                 .shouldHaveExitValue(0);
>>>             tool("jar", "uvf", jarFileName, testClassFilename)
>>>                 .shouldHaveExitValue(0);
>>>             verifyJarSignature(jarFileName);
>>> 
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(jarFileName));
>>>         }
>>>     }
>>>     
>>>     static void testSignJarWithIndex(String jarFileName) throws Throwable {
>>>         try {
>>>             tool("jar", "cvfm", jarFileName, 
>>>                     ManifestFileName, refClassFilename, testClassFilename)
>>>                 .shouldHaveExitValue(0);
>>>             tool("jar", "iv", jarFileName).shouldHaveExitValue(0);
>>>             verifyJarSignature(jarFileName);
>>> 
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(jarFileName));
>>>         }
>>>     }
>>> 
>>>     static void testSignJarAddIndex(String jarFileName) throws Throwable {
>>>         try {
>>>             tool("jar", "cvfm", jarFileName, 
>>>                     ManifestFileName, refClassFilename, testClassFilename)
>>>                 .shouldHaveExitValue(0);
>>>             tool("jarsigner", "-keystore", keystoreFileName, "-storetype", 
>>> "JKS",
>>>                     "-storepass", "changeit", "-debug", jarFileName, alias)
>>>                 .shouldHaveExitValue(0);
>>>             tool("jar", "iv", jarFileName).shouldHaveExitValue(0);
>>>             verifyJarSignature(jarFileName);
>>> 
>>>         } finally {
>>>             Files.deleteIfExists(Paths.get(jarFileName));
>>>         }
>>>     }
>>>     
>>>     static Map<String, String> jarsignerResources = Arrays.stream(new 
>>> Resources().getContents()).
>>>             collect(Collectors.toMap(e -> (String) e[0], e -> (String) 
>>> e[1]));
>>>             
>>>     static void verifyJarSignature(String jarFileName) throws Throwable {
>>>         // actually sign the jar
>>>         tool("jarsigner", "-keystore", keystoreFileName, "-storetype", 
>>> "JKS",
>>>                 "-storepass", "changeit", "-debug", jarFileName, alias)
>>>                 .shouldHaveExitValue(0);
>>>         
>>>         verifyClassNameLineBroken(jarFileName);
>>>         
>>>         // check for no unsigned entries
>>>         tool("jarsigner", "-verify", "-keystore", keystoreFileName, 
>>> "-storetype", "JKS",
>>>                 "-storepass", "changeit", "-debug", jarFileName, alias)
>>>                 .shouldHaveExitValue(0)
>>>                 .shouldNotContain(jarsignerResources.get(
>>>                         
>>> "This.jar.contains.unsigned.entries.which.have.not.been.integrity.checked."));
>>>         
>>>         // check that both classes with and without multi-byte unicode 
>>> characters in their names are signed
>>>         tool("jarsigner", "-verify", "-verbose", "-keystore", 
>>> keystoreFileName, "-storetype", "JKS",
>>>                 "-storepass", "changeit", "-debug", jarFileName, alias)
>>>                 .shouldHaveExitValue(0)
>>>                 .shouldMatch("^" + jarsignerResources.get("s") + 
>>> jarsignerResources.get("m") + jarsignerResources.get("k") + ".+" + 
>>> refClassFilename.replaceAll("\\$", "\\\\\\$") + "$")
>>>                 .shouldMatch("^" + jarsignerResources.get("s") + 
>>> jarsignerResources.get("m") + jarsignerResources.get("k") + ".+" + 
>>> testClassFilename.replaceAll("\\$", "\\\\\\$") + "$");
>>>         
>>>         // check that both classes with and without multi-byte unicode 
>>> characters in their names have signing certificates
>>>         tool("jarsigner", "-verify", "-verbose", "-certs", "-keystore", 
>>> keystoreFileName, 
>>>                 "-storetype", "JKS", "-storepass", "changeit", "-debug", 
>>> jarFileName, alias)
>>>                 .shouldHaveExitValue(0)
>>>                 .shouldMatch(refClassFilename.replaceAll("\\$", "\\\\\\$") 
>>> + "\\s*\\R*\\s*(\\[" + jarsignerResources.get("entry.was.signed.on") + " 
>>> .*\\]\\R*)?\\s*X.509, CN=X \\(" + alias + "\\)")
>>>                 .shouldMatch(testClassFilename.replaceAll("\\$", "\\\\\\$") 
>>> + "\\s*\\R*\\s*(\\[" + jarsignerResources.get("entry.was.signed.on") + " 
>>> .*\\]\\R*)?\\s*X.509, CN=X \\(" + alias + "\\)");
>>>     }
>>>     
>>>     /**
>>>      * 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 test 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 the Manifest breaking lines unaware of bytes that 
>>> belong to the same
>>>      * multi-ybte utf characters.
>>>      */
>>>     static void verifyClassNameLineBroken(String jarFileName) throws 
>>> IOException {
>>>         byte[] eAcute = "é".getBytes(UTF_8);
>>>         byte[] eAcuteBroken = new byte[] {eAcute[0], '\r', '\n', ' ', 
>>> eAcute[1]};
>>>         
>>>         try (
>>>             JarFile jar = new JarFile(jarFileName);
>>>         ) {
>>>             if (jar.getManifest().getAttributes(testClassFilename) == null) 
>>> {
>>>                 throw new AssertionError(testClassFilename + " 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 OutputAnalyzer tool(String tool, String... args)
>>>             throws Throwable {
>>>         JDKToolLauncher l = JDKToolLauncher.createUsingTestJDK(tool);
>>>         for (String arg : args) {
>>>             if (arg .startsWith("-J")) {
>>>                 l.addVMArg(arg.substring(2));
>>>             } else {
>>>                 l.addToolArg(arg);
>>>             }
>>>         }
>>>         return ProcessTools.executeCommand(l.getCommand());
>>>     }
>>>     
>>> }
>>> END /jdk10/jdk/test/sun/security/tools/jarsigner/MultibyteUnicodeName.java
>>> 
>>> 
>>> There are three e with acutes, on lines 84, 89, and 227, just in case the 
>>> mail suffers bad encoding which might not be absolutely unconceivable 
>>> regarding the bug's subject.
>>> 
>>> In contrast to the bug description it uses a nested class with a two-byte 
>>> UTF-8 character rather than one in its own file. I chose to do it that way 
>>> because then the complete test goes into one file and therefore I assume 
>>> the overview is kept easier. The test still demonstrates exactly 
>>> JDK-6695402's problem.
>>> 
>>> It may not have been necessary to also test jar files with indexes but i 
>>> consider it necessary to have signing unsigned as well as partially signed 
>>> jars tested, so why not have one more case tested, too?
>>> 
>>> There might also be a more elegant approach to get class files into a jar 
>>> file as to get their contents through the class loader. I chose to use real 
>>> class files rather than dummy contents in files named *.class or for 
>>> instance plain text files in the jar the signing of which to be tested in 
>>> order to stay as close to the original bug problem as possible even though 
>>> I don't have any notion that it would make a difference. The amount of code 
>>> used to copy the class file contents around is comparatively small with 
>>> respect to the whole test case amount of code.
>>>   
>>> For the demonstration that the multi-byte character actually makes the 
>>> alleged difference, I concluded that it was necessary to have another class 
>>> name not previously affected by the bug in order to compare the effects of 
>>> just different names. Otherwise the signing could fail to any other reason 
>>> undetectably.
>>> 
>>> In order to express a condition to tell successful from failed test runs 
>>> the best approach I found was to analyze the jarsigner tool's output which 
>>> is in my opinion not the most desirable option. I'd have preferred output 
>>> from a clearer structured api but then I guess the output format will not 
>>> change too often in the foreseeable future. For instance the check for the 
>>> s, m, and k flags is more pragmatical approach than obviously perfectly 
>>> failsafe when operating with regular expressions to match it with the 
>>> output. Other parts such as 'X.509' and 'CN=' are not even jarsigner tool 
>>> resources. I put at least a reference to 
>>> sun.security.tools.jarsigner.Resources.
>>> 
>>> Finally, I afforded kind of a self-test that protects the test from 
>>> undetected failing because renaming the main class which would move the 
>>> two-byte unicode character to a position not suitable for the test. It may 
>>> not be absolutely necessary.
>>> 
>>> 
>>> 
>>> BEGIN PATH
>>> diff -r ddc25f646c2e 
>>> src/java.base/share/classes/sun/security/util/ManifestDigester.java
>>> --- a/src/java.base/share/classes/sun/security/util/ManifestDigester.java   
>>>  Thu Aug 31 22:21:20 2017 -0700
>>> +++ b/src/java.base/share/classes/sun/security/util/ManifestDigester.java   
>>>  Wed Sep 20 00:56:15 2017 +0200
>>> @@ -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,7 +113,7 @@
>>>          rawBytes = bytes;
>>>          entries = new HashMap<>();
>>>  
>>> -        ByteArrayOutputStream baos = new ByteArrayOutputStream();
>>> +        ByteArrayOutputStream nameBuf = new ByteArrayOutputStream();
>>>  
>>>          Position pos = new Position();
>>>  
>>> @@ -131,50 +132,40 @@
>>>  
>>>              if (len > 6) {
>>>                  if (isNameAttr(bytes, start)) {
>>> -                    StringBuilder nameBuf = new StringBuilder(sectionLen);
>>> +                    nameBuf.reset();
>>> +                    nameBuf.write(bytes, start+6, len-6);
>>>  
>>> -                    try {
>>> -                        nameBuf.append(
>>> -                            new String(bytes, start+6, len-6, "UTF8"));
>>> +                    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;
>>> +
>>> +                            nameBuf.write(bytes, wrapStart, wrapLen);
>>> +                        } else {
>>> +                            break;
>>>                          }
>>> +                    }
>>>  
>>> -                        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;
>>> -                            }
>>> -                        }
>>> -
>>> -                        entries.put(nameBuf.toString(),
>>> -                            new Entry(start, sectionLen, 
>>> sectionLenWithBlank,
>>> -                                rawBytes));
>>> -
>>> -                    } catch (java.io.UnsupportedEncodingException uee) {
>>> -                        throw new IllegalStateException(
>>> -                            "UTF8 not available on platform");
>>> -                    }
>>> +                    entries.put(new String(nameBuf.toByteArray(), UTF_8),
>>> +                        new Entry(start, sectionLen, sectionLenWithBlank, 
>>> rawBytes));
>>>                  }
>>>              }
>>>              start = pos.startOfNext;
>>> END PATCH
>>> 
>>> 
>>> The patch is mostly so big because indentation has changed in many lines.
>>> 
>>> There was baos there before without apparent use. The patch renames it and 
>>> puts it into use. It came in very handy because if it would not have been 
>>> there already, I would have had to defined one of my own.
>>> 
>>> The main point of the patch is that manifest section names that are broken 
>>> across lines at possibly arbitrary bytes are no longer converted into 
>>> strings for each manifest line and then joined. Parts of names broken 
>>> across lines are now joined first when they are still byte sequences and 
>>> only when the complete byte sequence is available after processing all 
>>> manifest lines that contain the same manifest section name decoded into a 
>>> unicode string.
>>> 
>>> For decoding the bytes into a string I chose a different string constructor 
>>> than was used before that does not any longer declare 
>>> UnsupportedEncodingException rendering the try/catch redundant. It couldn't 
>>> have ever occurred anyway taking into consideration that UTF-8 is mandatory 
>>> for every Java platform, StandardCharsets says. The difference according to 
>>> the documentation is that the previously     used String constructor 
>>> returned undefined strings for invalid byte sequences whereas the one used 
>>> in the patch will replace unparseable portions with valid 'unknown' 
>>> characters.
>>> 
>>> One question I cannot still answer yet is how the ManifestDigester can be 
>>> changed at all without complete test coverage or risking to break existing 
>>> signatures. I already started on that but it's quite a piece of work to 
>>> almost formally prove that all manifests will continue to produce identical 
>>> digests, except of course for the ones now fixed.
>>> 
>>> Regards,
>>> Philipp
>>> 
>>> 
>>> On 17.09.2017 21:25, Philipp Kunz wrote:
>>> 
>>>> Hello Vincent
>>>> 
>>>> I narrowed the error down so far and suspect now that it is an effect of 
>>>> sun.security.util.ManifestDigester's constructor, lines 134 and 163-164, 
>>>> in combination with java.util.jar.Manifest.make72Safe. Manifest breaks 
>>>> multi-byte characters in manifest section names across lines and 
>>>> ManifestDigester fails to restore them correctly, which both sounds wrong 
>>>> but ManifestDigester sure is.
>>>> 
>>>> Just fixing it would be too tempting but then I would not know (I mean not 
>>>> know for sure with evidence from tests) if any existing jar-signature 
>>>> would still be valid and I don't want to break existing signatures. When I 
>>>> had a look at the tests specific for Manifest and ManifestDigester I found 
>>>> very little. There are many more different situations and corner cases and 
>>>> combinations thereof to cover with tests than it looks like at first 
>>>> glance so that writing tests that cover all relevant cases looks to me 
>>>> like quite an effort. I have the impression that test coverage was not a 
>>>> priority when the subjected code was developed or at least the tests might 
>>>> not have been contributed to the open JDK project. Hence, while I'm 
>>>> continuing to complete these tests I miss, if       someone has an idea 
>>>> how to simplify that so that I can still convince at least myself that no 
>>>> existing signature will break or where possibly more testcases are that I 
>>>> haven't discovered so far, please let me know.
>>>> 
>>>> I'm also wondering how much performance should be taken into 
>>>> consideration. There are a few hints such as reusing byte arrays that 
>>>> suggest that it is an issue. Will a patch be rejected if it slows down 
>>>> some tests beyond a certain limit for so little added value or what might 
>>>> be the acceptance criteria here? I don't expect insuperable difficulties 
>>>> with performance but would still appreciate some general idea.
>>>> 
>>>> My desired or currently preferred approach to fix JDK-6695402 would be to 
>>>> let ManifestDigester any kind of use or extend Manifest in order to let 
>>>> Manifest identify the manifest sections thereby preventing the parsing 
>>>> duplicated that identifies the manifest sections.
>>>> 
>>>> As a new contributor I could use and would appreciate some guidance 
>>>> particularly about what kind and completeness of test coverage and 
>>>> performance tuning applies or is suggested in the context of the given 
>>>> bug. Otherwise I fear I might contribute too many tests which would have 
>>>> to be reviewed and maintained or quite some work would be for nothing if a 
>>>> patch would not satisfy performance requirements.
>>>> 
>>>> Regards,
>>>> Philipp
>>>> 
>>>> 
>>>> 
>>>> On 01.09.2017 15:20, Vincent Ryan wrote:
>>>> 
>>>>> That all sounds fine. Let me know when your patch is ready to submit.
>>>>> Thanks.
>>>>> 
>>>>> 
>>>>> 
>>>>>> On 1 Sep 2017, at 13:15, Philipp Kunz <philipp.k...@paratix.ch>
>>>>>>  wrote:
>>>>>> 
>>>>>> Hello Vincent
>>>>>> 
>>>>>> Thank you for sponsoring!
>>>>>> So far, I have become a contributor by signing the OCA which has been 
>>>>>> accepted. Dalibor Topic wrote that he can confirm and it's also here: 
>>>>>> http://www.oracle.com/technetwork/community/oca-486395.html#p
>>>>>>  -> Paratix GmbH
>>>>>> Therefore I think I have followed the steps in 
>>>>>> http://openjdk.java.net/contribute/
>>>>>>  at least the ones before actual patch submission.
>>>>>> 
>>>>>> Currently, I'm working on getting the existing test cases running 
>>>>>> locally. Unfortunately, I started with jdk 9 and switched to 10 now. I 
>>>>>> figured these commands run at least the relevant tests with 9 and hope 
>>>>>> this also applies to 10:
>>>>>> make run-test-tier1
>>>>>> make run-test TEST="jdk/test"
>>>>>> they report errors and failures but it hasn't been completed and 
>>>>>> released which might explain it. If I run the tests I assume relevant 
>>>>>> for my patch
>>>>>> make run-test TEST="jtreg:jdk/test/sun/security/tools/jarsigner 
>>>>>> jtreg:jdk/test/java/util/jar"
>>>>>> then it reports zero errors and failures which may be a good starting 
>>>>>> point.
>>>>>> Do you think that sounds reasonable or do you have another suggestion 
>>>>>> how to run the tests?
>>>>>> 
>>>>>> Next, I will add a test for JDK-6695402 before actually fixing it. As an 
>>>>>> example, I'll try something in the style of 
>>>>>> http://hg.openjdk.java.net/jdk9/dev/jdk/file/65464a307408/test/java/util/jar/Manifest/CreateManifest.java
>>>>>> . This way, I try to demonstrate the improvement.
>>>>>> 
>>>>>> I guess I have identified the following line as the cause: value = new 
>>>>>> String(vb, 0, 0, vb.length);
>>>>>> 
>>>>>> http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/51f5d60713b5/src/java.base/share/classes/java/util/jar/Manifest.java#l157
>>>>>> 
>>>>>> So I'll try to remove it first including the whole four line if block.
>>>>>> 
>>>>>> Philipp
>>>>>> 
>>>>>> 
>>>>>> On 01.09.2017 10:00, Vincent Ryan wrote:
>>>>>> 
>>>>>>> Hello Philipp,
>>>>>>> 
>>>>>>> I’m happy to sponsor your fix for JDK 10. Have you followed these 
>>>>>>> steps: 
>>>>>>> http://openjdk.java.net/contribute/
>>>>>>>  ?
>>>>>>> 
>>>>>>> Thanks.
>>>>>>> 
>>>>>>> 
>>>>>>> 
>>>>>>>> On 1 Sep 2017, at 08:58, Vincent Ryan <vincent.x.r...@oracle.com>
>>>>>>>>  wrote:
>>>>>>>> 
>>>>>>>> Moved to security-dev
>>>>>>>> 
>>>>>>>> 
>>>>>>>> 
>>>>>>>>> On 1 Sep 2017, at 08:28, Philipp Kunz <philipp.k...@paratix.ch>
>>>>>>>>>  wrote:
>>>>>>>>> 
>>>>>>>>> Hello everyone
>>>>>>>>> 
>>>>>>>>> I have been developing with Java for around 17 years now and when I 
>>>>>>>>> encountered some bug I decided to attempt to fix it: 
>>>>>>>>> https://bugs.openjdk.java.net/browse/JDK-6695402
>>>>>>>>> . This also looks like it may not be too big a piece for a first 
>>>>>>>>> contribution.
>>>>>>>>> 
>>>>>>>>> I read through quite some guides and all kinds of documents but could 
>>>>>>>>> not yet help myself with the following questions:
>>>>>>>>> 
>>>>>>>>> May I login to jira to add comments to bugs? If so, how would I 
>>>>>>>>> request or receive credentials? Or are mailing lists preferred?
>>>>>>>>> 
>>>>>>>>> Another question is whether I should apply it to jdk9, but it may be 
>>>>>>>>> too late now, or to jdk10, and backporting can be considered later. 
>>>>>>>>> Probably it wouldn't even make much a difference for the patch itself.
>>>>>>>>> 
>>>>>>>>> One more question I have is how or where to find the sources from 
>>>>>>>>> before migration to mercurial. Because some lines of code I intend to 
>>>>>>>>> change go back farther and in the history I find only 'initial 
>>>>>>>>> commit'. With such a history I might be able better to understand why 
>>>>>>>>> it's there and prevent to make the same mistake again.
>>>>>>>>> 
>>>>>>>>> I guess the appropriate mailing list for above mentioned bug is 
>>>>>>>>> security-dev. Is it correct that I can send a patch there and just 
>>>>>>>>> hope for some sponsor to pick it up? Of course I'd be glad if some 
>>>>>>>>> sponsor would contact me and maybe provide some assistance or if 
>>>>>>>>> someone would confirm that sending a patch to the mailing list is the 
>>>>>>>>> right way to find a sponsor.
>>>>>>>>> 
>>>>>>>>> Philipp Kunz
>>>>>>>>> 
>>>> 
>>>> 
>>>> 
>>>> 
>>>> <Mail Attachment.png>
>>>> 
>>>> Paratix GmbH
>>>> St Peterhofstatt 11
>>>> 8001 Zürich
>>>> 
>>>> +41 (0)76 397 79 35
>>>> 
>>>> philipp.k...@paratix.ch
> 
> -- 
> 
> 
> Gruss Philipp
> 
> 
> 
> 
> <Mail Attachment.png>
> 
> Paratix GmbH
> St Peterhofstatt 11
> 8001 Zürich
> 
> +41 (0)76 397 79 35
> philipp.k...@paratix.ch

Reply via email to