test/jdk/sun/security/util/ManifestDigester/DigestInput.java
author weijun
Thu, 18 Jul 2019 08:53:06 +0800
changeset 57488 94691d8e746f
permissions -rw-r--r--
8217375: jarsigner breaks old signature with long lines in manifest Reviewed-by: jjiang, weijun Contributed-by: Philipp Kunz <philipp.kunz@paratix.ch>

/*
 * 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.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Attributes.Name;
import java.util.stream.Collectors;

import sun.security.util.ManifestDigester;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Factory;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.AfterTest;
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 DigestInput
 * @summary Checks that the manifest main attributes and entry digests are the
 * same as before resolution of bug 8217375 which means they treat some white
 * space different for oldStyle or digestWorkaround except for the blank line
 * at the end of the manifest file for digestWorkaround.
 */
public class DigestInput {

    /**
     * Filters some test cases for calibrating expected digests with previous
     * implementation. TODO: Delete this after calibrating with old sources.
     */
    static final boolean FIXED_8217375 = true; // FIXME

    /**
     * {@link ManifestDigester.Entry#digestWorkaround} should not feed the
     * trailing blank line into the digester. Before resolution of 8217375 it
     * fed the trailing blank line into the digest if the second line break
     * was at the end of the file due to <pre>
     * if (allBlank || (i == len-1)) {
     *     if (i == len-1)
     *         pos.endOfSection = i;
     *     else
     *         pos.endOfSection = last;
     * </pre> in {@link ManifestDigester#findSection}. In that case at the end
     * of the manifest file, {@link ManifestDigester.Entry#digestWorkaround}
     * would have produced the same digest as
     * {@link ManifestDigester.Entry#digest} which was wrong and without effect
     * at best.
     * <p>
     * Once this fix is accepted, this flag can be removed along with
     * {@link #assertDigestEqualsCatchWorkaroundBroken}.
     */
    static final boolean FIXED_8217375_EOF_ENDOFSECTION = FIXED_8217375;

    static final String SECTION_NAME = "some individual section name";

    @DataProvider(name = "parameters")
    public static Object[][] parameters() {
        List<Object[]> tests = new ArrayList<>();
        for (String lineBreak : new String[] { "\n", "\r", "\r\n" }) {
            if ("\r".equals(lineBreak) && !FIXED_8217375) continue;
            for (int addLB = 0; addLB <= 4; addLB++) {
                for (int numSecs = 0; numSecs <= 4; numSecs++) {
                    for (boolean otherSec : new Boolean[] { false, true }) {
                        for (boolean oldStyle : new Boolean[] { false, true }) {
                            for (boolean workaround :
                                    new Boolean[] { false, true }) {
                                tests.add(new Object[] {
                                    lineBreak, addLB, numSecs, otherSec,
                                            oldStyle, workaround
                                });
                            }
                        }
                    }
                }
            }
        }
        return tests.toArray(new Object[tests.size()][]);
    }

    @Factory(dataProvider = "parameters")
    public static Object[] createTests(
            String lineBreak, int additionalLineBreaks,
            int numberOfSections, boolean hasOtherSection,
            boolean oldStyle, boolean digestWorkaround) {
        return new Object[] { new DigestInput(lineBreak,
                additionalLineBreaks, numberOfSections, hasOtherSection,
                oldStyle, digestWorkaround)
        };
    }

    final String lineBreak;
    final int additionalLineBreaks; // number of blank lines delimiting section
    final int numberOfSections;
    final boolean hasOtherSection;
    final boolean oldStyle;
    final boolean digestWorkaround;

    public DigestInput(
            String lineBreak, int additionalLineBreaks,
            int numberOfSections, boolean hasOtherSection,
            boolean oldStyle, boolean digestWorkaround) {
        this.lineBreak = lineBreak;
        this.additionalLineBreaks = additionalLineBreaks;
        this.numberOfSections = numberOfSections;
        this.hasOtherSection = hasOtherSection;
        this.oldStyle = oldStyle;
        this.digestWorkaround = digestWorkaround;
    }

    @BeforeMethod
    public void verbose() {
        System.out.println("-".repeat(72));
        System.out.println("lineBreak = " +
                Utils.escapeStringWithNumbers(lineBreak));
        System.out.println("additionalLineBreaks = " + additionalLineBreaks);
        System.out.println("numberOfSections = " + numberOfSections);
        System.out.println("hasOtherSection = " + hasOtherSection);
        System.out.println("oldStyle = " + oldStyle);
        System.out.println("digestWorkaround = " + digestWorkaround);
        System.out.println("-".repeat(72));
    }

    byte[] rawManifestBytes() {
        return (
            Name.MANIFEST_VERSION + ": 1.0" + lineBreak +
            "OldStyle0: no trailing space" + lineBreak +
            "OldStyle1: trailing space " + lineBreak +
            "OldStyle2: two trailing spaces  " + lineBreak +
                    lineBreak.repeat(additionalLineBreaks) +
            (
                "Name: " + SECTION_NAME + lineBreak +
                "OldStyle0: no trailing space" + lineBreak +
                "OldStyle1: trailing space " + lineBreak +
                "OldStyle2: two trailing spaces  " + lineBreak +
                lineBreak.repeat(additionalLineBreaks)
            ).repeat(numberOfSections) +
            (hasOtherSection ?
                "Name: unrelated trailing section" + lineBreak +
                "OldStyle0: no trailing space" + lineBreak +
                "OldStyle1: trailing space " + lineBreak +
                "OldStyle2: two trailing spaces  " + lineBreak +
                lineBreak.repeat(additionalLineBreaks)
            : "")
        ).getBytes(UTF_8);
    }

    byte[] expectedMainAttrsDigest(boolean digestWorkaround) {
        return (
            Name.MANIFEST_VERSION + ": 1.0" + lineBreak +
            "OldStyle0: no trailing space" + lineBreak +
            "OldStyle1: trailing space" +
                (!oldStyle || !lineBreak.startsWith("\r") || digestWorkaround ?
                    " " : "") + lineBreak +
            "OldStyle2: two trailing spaces " +
                (!oldStyle || !lineBreak.startsWith("\r") || digestWorkaround ?
                        " " : "") + lineBreak +
            (
                     (
                                 !digestWorkaround
                         || (
                                 additionalLineBreaks == 1
                              && numberOfSections == 0
                              && !hasOtherSection
                              && (
                                      digestWorkaround
                                   && !FIXED_8217375_EOF_ENDOFSECTION
                                 )
                         )
                ) && (
                            additionalLineBreaks > 0
                         || numberOfSections > 0
                         || hasOtherSection
                )
            ? lineBreak : "")
        ).getBytes(UTF_8);
    }

    byte[] expectedIndividualSectionDigest(boolean digestWorkaround) {
        if (numberOfSections == 0) return null;
        return (
            (
                "Name: " + SECTION_NAME + lineBreak +
                "OldStyle0: no trailing space" + lineBreak +
                "OldStyle1: trailing space" +
                    (!oldStyle || !lineBreak.startsWith("\r")
                            || digestWorkaround ? " " : "") + lineBreak +
                "OldStyle2: two trailing spaces " +
                    (!oldStyle || !lineBreak.startsWith("\r")
                            || digestWorkaround ? " " : "") + lineBreak +
                (
                    (
                           !digestWorkaround
                    ) && (
                           additionalLineBreaks > 0
                    )
                ? lineBreak : "")
            ).repeat(numberOfSections) +
            (
                   additionalLineBreaks == 1
                && !hasOtherSection
                && digestWorkaround
                && !FIXED_8217375_EOF_ENDOFSECTION
            ? lineBreak : "")
        ).getBytes(UTF_8);
    }

    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();
        }

    }

    byte[] digestMainAttributes(byte[] mfBytes) throws Exception {
        Utils.echoManifest(mfBytes, "going to digest main attributes of");

        ManifestDigester md = new ManifestDigester(mfBytes);
        ManifestDigester.Entry entry =
                md.get(ManifestDigester.MF_MAIN_ATTRS, oldStyle);
        MessageDigest digester = new EchoMessageDigest();
        return digestWorkaround ?
                entry.digestWorkaround(digester) : entry.digest(digester);
    }

    byte[] digestIndividualSection(byte[] mfBytes) throws Exception {
        Utils.echoManifest(mfBytes,
                "going to digest section " + SECTION_NAME + " of");

        ManifestDigester md = new ManifestDigester(mfBytes);
        ManifestDigester.Entry entry = md.get(SECTION_NAME, oldStyle);
        if (entry == null) {
            return null;
        }
        MessageDigest digester = new EchoMessageDigest();
        return digestWorkaround ?
                entry.digestWorkaround(digester) : entry.digest(digester);
    }


    /**
     * Checks that the manifest main attributes digest is the same as before.
     */
    @Test
    public void testMainAttributesDigest() throws Exception {
        byte[] mfRaw = rawManifestBytes();
        byte[] digest = digestMainAttributes(mfRaw);
        byte[] expectedDigest = expectedMainAttrsDigest(digestWorkaround);

        // the individual section will be digested along with the main
        // attributes if not properly delimited with a blank line
        if (additionalLineBreaks == 0
                && (numberOfSections > 0 || hasOtherSection)) {
            assertNotEquals(digest, expectedDigest);
            return;
        }

        byte[] expectedDigestNoWorkaround = expectedMainAttrsDigest(false);

//        assertDigestEquals(digest, expectedDigest); // FIXME
        assertDigestEqualsCatchWorkaroundBroken(
                digest, expectedDigest, expectedDigestNoWorkaround);
    }

    /**
     * Checks that an individual section digest is the same as before.
     */
    @Test
    public void testIndividualSectionDigest() throws Exception {
        byte[] mfRaw = rawManifestBytes();
        byte[] digest = digestIndividualSection(mfRaw);

        // no digest will be produced for an individual section that is not
        // properly section delimited with a blank line.
        byte[] expectedDigest =
                additionalLineBreaks == 0 ? null :
                    expectedIndividualSectionDigest(digestWorkaround);

        byte[] expectedDigestNoWorkaround =
                additionalLineBreaks == 0 ? null :
                    expectedIndividualSectionDigest(false);

//      assertDigestEquals(digest, expectedDigest); // FIXME
        assertDigestEqualsCatchWorkaroundBroken(
                digest, expectedDigest, expectedDigestNoWorkaround);
    }

    static int firstDiffPos = Integer.MAX_VALUE;

    /**
     * @see FIXED_8217375_EOF_ENDOFSECTION
     */
    void assertDigestEqualsCatchWorkaroundBroken(
            byte[] actual, byte[] expected, byte[] expectedNoWorkaround)
                    throws IOException {
        try {
            assertDigestEquals(actual, expected);
        } catch (AssertionError e) {
            if (digestWorkaround && FIXED_8217375_EOF_ENDOFSECTION &&
                    Arrays.equals(expected, expectedNoWorkaround)) {
                // if digests with and without workaround are the same anyway
                // the workaround has failed and could not have worked with
                // the same digest as produced without workaround before
                // which would not match either because equal.
                return;
            }
            fail("failed also without digestWorkaound", e);
        }
    }

    void assertDigestEquals(byte[] actual, byte[] expected) throws IOException {
        if (actual == null && expected == null) return;
        Utils.echoManifest(actual, "actual digest");
        Utils.echoManifest(expected, "expected digest");
        for (int i = 0; i < actual.length && i < expected.length; i++) {
            if (actual[i] != expected[i]) {
                firstDiffPos = Math.min(firstDiffPos, i);
                verbose();
                fail("found first difference in current test"
                        + " at position " + i);
            }
        }
        if (actual.length != expected.length) {
            int diffPos = Math.min(actual.length, expected.length);
            firstDiffPos = Math.min(firstDiffPos, diffPos);
            verbose();
            fail("found first difference in current test"
                    + " at position " + diffPos + " after one digest end");
        }
        assertEquals(actual, expected);
    }

    @AfterTest
    public void reportFirstDiffPos() {
        System.err.println("found first difference in all tests"
                + " at position " + firstDiffPos);
    }

}