| /* | |
| * Copyright (c) 2019, 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. | |
| */ | |
| import java.io.ByteArrayInputStream; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.security.MessageDigest; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.stream.Collectors; | |
| import java.util.jar.Manifest; | |
| import sun.security.util.ManifestDigester; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.Factory; | |
| import org.testng.annotations.BeforeMethod; | |
| import org.testng.annotations.Test; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.testng.Assert.*; | |
| /** | |
| * @test | |
| * @bug 8217375 | |
| * @modules java.base/sun.security.util | |
| * @compile ../../tools/jarsigner/Utils.java | |
| * @run testng ReproduceRaw | |
| * @summary Verifies that {@link ManifestDigester} can reproduce parts of | |
| * manifests in their binary form so that {@link JarSigner} can rely on | |
| * {@link ManifestDigester.Entry#reproduceRaw} to write in a map view | |
| * unmodified entries back also unmodified in their binary form. | |
| * <p> | |
| * See also<ul> | |
| * <li>{@link PreserveRawManifestEntryAndDigest} with end to end tests | |
| * with {@code jarsigner} tool and</li> | |
| * <li>{@link FindHeaderEndVsManifestDigesterFindFirstSection} about | |
| * identifying the binary portion of only main attributes and more extensive | |
| * main attributes digesting tests while this one test here is more about | |
| * reproducing individual sections and that they result in the same | |
| * digests.</li> | |
| * </ul> | |
| */ | |
| public class ReproduceRaw { | |
| static final boolean VERBOSE = false; | |
| @DataProvider(name = "parameters") | |
| public static Object[][] parameters() { | |
| List<Object[]> tests = new ArrayList<>(); | |
| for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) { | |
| for (boolean oldStyle : new Boolean[] { false, true }) { | |
| for (boolean workaround : new Boolean[] { false, true }) { | |
| tests.add(new Object[] { lineBreak, oldStyle, workaround }); | |
| } | |
| } | |
| } | |
| return tests.toArray(new Object[tests.size()][]); | |
| } | |
| @Factory(dataProvider = "parameters") | |
| public static Object[] createTests(String lineBreak, | |
| boolean oldStyle, boolean digestWorkaround) { | |
| return new Object[]{ | |
| new ReproduceRaw(lineBreak, oldStyle, digestWorkaround) | |
| }; | |
| } | |
| final String lineBreak; | |
| final boolean oldStyle; | |
| final boolean digestWorkaround; | |
| public ReproduceRaw(String lineBreak, | |
| boolean oldStyle, boolean digestWorkaround) { | |
| this.lineBreak = lineBreak; | |
| this.oldStyle = oldStyle; | |
| this.digestWorkaround = digestWorkaround; | |
| } | |
| @BeforeMethod | |
| public void verbose() { | |
| System.out.println("lineBreak = " + | |
| Utils.escapeStringWithNumbers(lineBreak)); | |
| System.out.println("oldStyle = " + oldStyle); | |
| System.out.println("digestWorkaround = " + digestWorkaround); | |
| } | |
| class EchoMessageDigest extends MessageDigest { | |
| ByteArrayOutputStream buf; | |
| EchoMessageDigest() { | |
| super("echo"); | |
| } | |
| @Override | |
| protected void engineReset() { | |
| buf = new ByteArrayOutputStream(); | |
| } | |
| @Override | |
| protected void engineUpdate(byte input) { | |
| buf.write(input); | |
| } | |
| @Override | |
| protected void engineUpdate(byte[] i, int o, int l) { | |
| buf.write(i, o, l); | |
| } | |
| @Override protected byte[] engineDigest() { | |
| return buf.toByteArray(); | |
| } | |
| } | |
| /** | |
| * similar to corresponding part of {@link JarSigner#sign0} | |
| * (stripped down to the code for reproducing the old manifest entry by | |
| * entry which was too difficult to achieve using the real JarSigner code | |
| * in the test here) | |
| */ | |
| byte[] reproduceRawManifest(byte[] mfRawBytes, | |
| boolean mainAttsProperlyDelimited, | |
| boolean sectionProperlyDelimited) throws IOException { | |
| Manifest manifest = new Manifest(new ByteArrayInputStream(mfRawBytes)); | |
| ManifestDigester oldMd = new ManifestDigester(mfRawBytes); | |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| // main attributes | |
| assertEquals(oldMd.getMainAttsEntry().isProperlyDelimited(), | |
| mainAttsProperlyDelimited); | |
| oldMd.getMainAttsEntry().reproduceRaw(baos); | |
| // individual sections | |
| for (String key : manifest.getEntries().keySet()) { | |
| assertEquals(oldMd.get(key).isProperlyDelimited(), | |
| sectionProperlyDelimited); | |
| oldMd.get(key).reproduceRaw(baos); | |
| } | |
| return baos.toByteArray(); | |
| } | |
| static String regExscape(String expr) { | |
| for (int i = 0; i < expr.length(); i++) { | |
| if (expr.charAt(i) == '\r' || expr.charAt(i) == '\n') { | |
| expr = expr.substring(0, i) + "\\" + expr.substring(i++); | |
| } | |
| } | |
| return expr; | |
| } | |
| byte[] digest(byte[] manifest, String section) { | |
| MessageDigest digester = new EchoMessageDigest(); | |
| ManifestDigester md = new ManifestDigester(manifest); | |
| ManifestDigester.Entry entry = section == null ? | |
| md.getMainAttsEntry(oldStyle) : md.get(section, oldStyle); | |
| return digestWorkaround ? | |
| entry.digestWorkaround(digester) : | |
| entry.digest(digester); | |
| } | |
| void test(byte[] originalManifest, boolean mainAttsProperlyDelimited, | |
| boolean sectionProperlyDelimited) throws Exception { | |
| Utils.echoManifest(originalManifest, "original manifest"); | |
| byte[] reproducedManifest = reproduceRawManifest(originalManifest, | |
| mainAttsProperlyDelimited, sectionProperlyDelimited); | |
| Utils.echoManifest(reproducedManifest, "reproduced manifest"); | |
| // The reproduced manifest is not necessarily completely identical to | |
| // the original if it contained superfluous blank lines. | |
| // It's sufficient that the digests are equal and as an additional | |
| // check, the reproduced manifest is here compared to the original | |
| // without more than double line breaks. | |
| if (!lineBreak.repeat(2).equals(new String(originalManifest, UTF_8))) { | |
| assertEquals( | |
| new String(reproducedManifest, UTF_8), | |
| new String(originalManifest, UTF_8).replaceAll( | |
| regExscape(lineBreak) + "(" + regExscape(lineBreak) + ")+", | |
| lineBreak.repeat(2))); | |
| } | |
| // compare digests of reproduced manifest entries with digests of | |
| // original manifest entries | |
| assertEquals(digest(originalManifest, null), | |
| digest(reproducedManifest, null)); | |
| for (String key : new Manifest(new ByteArrayInputStream( | |
| originalManifest)).getEntries().keySet()) { | |
| assertEquals(digest(originalManifest, key), | |
| digest(reproducedManifest, key)); | |
| } | |
| // parse and compare original and reproduced manifests as manifests | |
| assertEquals(new Manifest(new ByteArrayInputStream(originalManifest)), | |
| new Manifest(new ByteArrayInputStream(reproducedManifest))); | |
| } | |
| void test(byte[] originalManifest, boolean mainAttsProperlyDelimited) | |
| throws Exception { | |
| // assert all individual sections properly delimited particularly useful | |
| // when no individual sections present | |
| test(originalManifest, mainAttsProperlyDelimited, true); | |
| } | |
| @Test | |
| public void testManifestStartsWithBlankLine() throws Exception { | |
| test(lineBreak.getBytes(UTF_8), true); | |
| test(lineBreak.repeat(2).getBytes(UTF_8), true); | |
| } | |
| @Test | |
| public void testEOFAndNoLineBreakAfterMainAttributes() throws Exception { | |
| assertThrows(RuntimeException.class, () -> | |
| test("Manifest-Version: 1.0".getBytes(UTF_8), false) | |
| ); | |
| } | |
| @Test | |
| public void testEOFAndNoBlankLineAfterMainAttributes() throws Exception { | |
| test(("Manifest-Version: 1.0" + lineBreak).getBytes(UTF_8), false); | |
| } | |
| @Test | |
| public void testNormalMainAttributes() throws Exception { | |
| test(("Manifest-Version: 1.0" + | |
| lineBreak.repeat(2)).getBytes(UTF_8), true); | |
| } | |
| @Test | |
| public void testExtraLineBreakAfterMainAttributes() throws Exception { | |
| test(("Manifest-Version: 1.0" + | |
| lineBreak.repeat(3)).getBytes(UTF_8), true); | |
| } | |
| @Test | |
| public void testIndividualSectionNoLineBreak() throws Exception { | |
| assertNull(new ManifestDigester(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" | |
| ).getBytes(UTF_8)).get("Section-Name")); | |
| } | |
| @Test | |
| public void testIndividualSectionOneLineBreak() throws Exception { | |
| test(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak | |
| ).getBytes(UTF_8), true, false); | |
| } | |
| @Test | |
| public void testNormalIndividualSectionTwoLineBreak() throws Exception { | |
| test(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak.repeat(2) | |
| ).getBytes(UTF_8), true, true); | |
| } | |
| @Test | |
| public void testExtraLineBreakAfterIndividualSection() throws Exception { | |
| test(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak.repeat(3) | |
| ).getBytes(UTF_8), true, true); | |
| } | |
| @Test | |
| public void testIndividualSections() throws Exception { | |
| test(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak + | |
| lineBreak | |
| ).getBytes(UTF_8), true, true); | |
| } | |
| @Test | |
| public void testExtraLineBreakBetweenIndividualSections() throws Exception { | |
| test(( | |
| "Manifest-Version: 1.0" + lineBreak + | |
| lineBreak + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak + | |
| lineBreak.repeat(2) + | |
| "Name: Section-Name" + lineBreak + | |
| "Key: Value" + lineBreak + | |
| lineBreak | |
| ).getBytes(UTF_8), true, true); | |
| } | |
| } |