manifestManipulation,
+ String digestalg, boolean assertMainAttrsDigestsUnchanged,
+ boolean assertFirstAddedFileDigestsUnchanged)
+ throws Exception {
+ String digOpts = (digestalg != null ? "-digestalg " + digestalg : "");
+ String jarFilename1 = "test-" + name + "-step1.jar";
+ createSignedJarA(jarFilename1,
+ /* no manifest will let jarsigner create a default one */ null,
+ digOpts, firstAddedFilename);
+
+ // manipulate the manifest, write it back, and sign the jar again with
+ // the same certificate a as before overwriting the first signature
+ String jarFilename2 = "test-" + name + "-step2.jar";
+ String jarFilename3 = "test-" + name + "-step3.jar";
+ manipulateManifestSignAgainA(jarFilename1, jarFilename2, jarFilename3,
+ digOpts, manifestManipulation);
+
+ // add another file, sign it with the other certificate, and verify it
+ String jarFilename4 = "test-" + name + "-step4.jar";
+ JarUtils.updateJar(jarFilename3, jarFilename4,
+ secondAddedFilename != null ?
+ Map.of(secondAddedFilename, secondAddedFilename)
+ : Collections.EMPTY_MAP);
+ OutputAnalyzer o = signB(jarFilename4, digOpts,
+ secondAddedFilename != null ? NOTSIGNEDBYALIASORALIASNOTINSTORE : 0);
+ // check that secondAddedFilename is the only entry which is not signed
+ // by signer with alias "a" unless secondAddedFilename is null
+ assertMatchByLines(
+ fromFirstToSecondEmptyLine(o.getStdout().split("\\R")),
+ getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
+ jarFilename4, digestalg,
+ firstAddedFilename, secondAddedFilename));
+
+ // double-check reading the files with a verifying JarFile
+ try (JarFile jar = new JarFile(jarFilename4, true)) {
+ if (firstAddedFilename != null) {
+ JarEntry je1 = jar.getJarEntry(firstAddedFilename);
+ jar.getInputStream(je1).readAllBytes();
+ assertTrue(je1.getCodeSigners().length > 0);
+ }
+ if (secondAddedFilename != null) {
+ JarEntry je2 = jar.getJarEntry(secondAddedFilename);
+ jar.getInputStream(je2).readAllBytes();
+ assertTrue(je2.getCodeSigners().length > 0);
+ }
+ }
+
+ // assert that the signature of firstAddedFilename signed by signer
+ // with alias "a" is not lost and its digest remains the same
+ try (ZipFile zip = new ZipFile(jarFilename4)) {
+ ZipEntry ea = zip.getEntry("META-INF/A.SF");
+ Manifest sfa = new Manifest(zip.getInputStream(ea));
+ ZipEntry eb = zip.getEntry("META-INF/B.SF");
+ Manifest sfb = new Manifest(zip.getInputStream(eb));
+ if (assertMainAttrsDigestsUnchanged) {
+ String mainAttrsDigKey =
+ (digestalg != null ? digestalg : "SHA-256") +
+ "-Digest-Manifest-Main-Attributes";
+ assertEquals(sfa.getMainAttributes().getValue(mainAttrsDigKey),
+ sfb.getMainAttributes().getValue(mainAttrsDigKey));
+ }
+ if (assertFirstAddedFileDigestsUnchanged) {
+ assertEquals(sfa.getAttributes(firstAddedFilename),
+ sfb.getAttributes(firstAddedFilename));
+ }
+ }
+
+ return jarFilename4;
+ }
+
+ /**
+ * Test that signing a jar with manifest entries with arbitrary line break
+ * positions in individual section headers does not destroy an existing
+ * signature
+ * - create two self-signed certificates
+ * - sign a jar with at least one non-META-INF file in it with a JDK
+ * before 11 or place line breaks not at 72 bytes in an individual section
+ * header
+ * - add a new file to the jar
+ * - sign the jar with a JDK 11, 12, or 13 with bug 8217375 not yet
+ * resolved with a different signer
+ *
→ first signature will not validate anymore even though it
+ * should.
+ */
+ @Test
+ public void arbitraryLineBreaksSectionName() throws Exception {
+ test("arbitraryLineBreaksSectionName", m -> {
+ return (
+ Name.MANIFEST_VERSION + ": 1.0\r\n" +
+ "Created-By: " +
+ m.getMainAttributes().getValue("Created-By") + "\r\n" +
+ "\r\n" +
+ "Name: Test\r\n" +
+ " -\r\n" +
+ " Section\r\n" +
+ "Key: Value \r\n" +
+ "\r\n" +
+ "Name: " + FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n" +
+ " " + FILENAME_INITIAL_CONTENTS.substring(1, 8) + "\r\n" +
+ " " + FILENAME_INITIAL_CONTENTS.substring(8) + "\r\n" +
+ "SHA-256-Digest: " + m.getAttributes(FILENAME_INITIAL_CONTENTS)
+ .getValue("SHA-256-Digest") + "\r\n" +
+ "\r\n"
+ ).getBytes(UTF_8);
+ });
+ }
+
+ /**
+ * Test that signing a jar with manifest entries with arbitrary line break
+ * positions in individual section headers does not destroy an existing
+ * signature
+ * - create two self-signed certificates
+ * - sign a jar with at least one non-META-INF file in it with a JDK
+ * before 11 or place line breaks not at 72 bytes in an individual section
+ * header
+ * - add a new file to the jar
+ * - sign the jar with a JDK 11 or 12 with a different signer
+ *
→ first signature will not validate anymore even though it
+ * should.
+ */
+ @Test
+ public void arbitraryLineBreaksHeader() throws Exception {
+ test("arbitraryLineBreaksHeader", m -> {
+ String digest = m.getAttributes(FILENAME_INITIAL_CONTENTS)
+ .getValue("SHA-256-Digest");
+ return (
+ Name.MANIFEST_VERSION + ": 1.0\r\n" +
+ "Created-By: " +
+ m.getMainAttributes().getValue("Created-By") + "\r\n" +
+ "\r\n" +
+ "Name: Test-Section\r\n" +
+ "Key: Value \r\n" +
+ " with\r\n" +
+ " strange \r\n" +
+ " line breaks.\r\n" +
+ "\r\n" +
+ "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+ "SHA-256-Digest: " + digest.substring(0, 11) + "\r\n" +
+ " " + digest.substring(11) + "\r\n" +
+ "\r\n"
+ ).getBytes(UTF_8);
+ });
+ }
+
+ /**
+ * Breaks {@code line} at 70 bytes even though the name says 72 but when
+ * also counting the line delimiter ("{@code \r\n}") the line totals to 72
+ * bytes.
+ * Borrowed from {@link Manifest#make72Safe} before JDK 11
+ *
+ * @see Manifest#make72Safe
+ */
+ static void make72Safe(StringBuffer line) {
+ int length = line.length();
+ if (length > 72) {
+ int index = 70;
+ while (index < length - 2) {
+ line.insert(index, "\r\n ");
+ index += 72;
+ length += 3;
+ }
+ }
+ return;
+ }
+
+ /**
+ * Test that signing a jar with manifest entries with line breaks at
+ * position where Manifest would not place them now anymore (72 instead of
+ * 70 bytes after line starts) does not destroy an existing signature
+ * - create two self-signed certificates
+ * - simulate a manifest as it would have been written by a JDK before 11
+ * by re-positioning line breaks at 70 bytes (which makes a difference by
+ * digests that grow headers longer than 70 characters such as SHA-512 as
+ * opposed to default SHA-256, long file names, or manual editing)
+ * - add a new file to the jar
+ * - sign the jar with a JDK 11 or 12 with a different signer
+ *
→
+ * The first signature will not validate anymore even though it should.
+ */
+ public void lineWidth70(String name, String digestalg) throws Exception {
+ Files.write(Path.of(name), name.getBytes(UTF_8));
+ test(name, name, FILENAME_UPDATED_CONTENTS, m -> {
+ // force a line break with a header exceeding line width limit
+ m.getEntries().put("Test-Section", new Attributes());
+ m.getAttributes("Test-Section").put(
+ Name.IMPLEMENTATION_VERSION, "1" + "0".repeat(100));
+
+ StringBuilder sb = new StringBuilder();
+ StringBuffer[] buf = new StringBuffer[] { null };
+ manifestToString(m).lines().forEach(line -> {
+ if (line.startsWith(" ")) {
+ buf[0].append(line.substring(1));
+ } else {
+ if (buf[0] != null) {
+ make72Safe(buf[0]);
+ sb.append(buf[0].toString());
+ sb.append("\r\n");
+ }
+ buf[0] = new StringBuffer();
+ buf[0].append(line);
+ }
+ });
+ make72Safe(buf[0]);
+ sb.append(buf[0].toString());
+ sb.append("\r\n");
+ return sb.toString().getBytes(UTF_8);
+ }, digestalg, false, false);
+ }
+
+ @Test
+ public void lineWidth70Filename() throws Exception {
+ lineWidth70(
+ "lineWidth70".repeat(6) /* 73 chars total with "Name: " */, null);
+ }
+
+ @Test
+ public void lineWidth70Digest() throws Exception {
+ lineWidth70("lineWidth70digest", "SHA-512");
+ }
+
+ /**
+ * Test that signing a jar with a manifest with line delimiter other than
+ * "{@code \r\n}" does not destroy an existing signature
+ * - create two self-signed certificates
+ * - sign a jar with at least one non-META-INF file in it
+ * - extract the manifest, and change its line delimiters
+ * (for example dos2unix)
+ * - update the jar with the updated manifest
+ * - sign it again with the same signer as before
+ * - add a new file to the jar
+ * - sign the jar with a JDK before 13 with a different signer
-
+ *
→
+ * The first signature will not validate anymore even though it should.
+ */
+ public void lineBreak(String lineBreak) throws Exception {
+ test("lineBreak" + byteArrayToIntList(lineBreak.getBytes(UTF_8)).stream
+ ().map(i -> "" + i).collect(Collectors.joining("")), m -> {
+ StringBuilder sb = new StringBuilder();
+ manifestToString(m).lines().forEach(l -> {
+ sb.append(l);
+ sb.append(lineBreak);
+ });
+ return sb.toString().getBytes(UTF_8);
+ });
+ }
+
+ @Test
+ public void lineBreakCr() throws Exception {
+ lineBreak("\r");
+ }
+
+ @Test
+ public void lineBreakLf() throws Exception {
+ lineBreak("\n");
+ }
+
+ @Test
+ public void lineBreakCrLf() throws Exception {
+ lineBreak("\r\n");
+ }
+
+ @Test
+ public void testAdjacentRepeatedSection() throws Exception {
+ test("adjacent", m -> {
+ return (manifestToString(m) +
+ "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+ "Foo: Bar\r\n" +
+ "\r\n"
+ ).getBytes(UTF_8);
+ });
+ }
+
+ @Test
+ public void testIntermittentRepeatedSection() throws Exception {
+ test("intermittent", m -> {
+ return (manifestToString(m) +
+ "Name: don't know\r\n" +
+ "Foo: Bar\r\n" +
+ "\r\n" +
+ "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
+ "Foo: Bar\r\n" +
+ "\r\n"
+ ).getBytes(UTF_8);
+ });
+ }
+
+ @Test
+ public void testNameImmediatelyContinued() throws Exception {
+ test("testNameImmediatelyContinued", m -> {
+ // places a continuation line break and space at the first allowed
+ // position after ": " and before the first character of the value
+ return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+ "\r\n " + FILENAME_INITIAL_CONTENTS + "\r\nFoo: Bar")
+ ).getBytes(UTF_8);
+ });
+ }
+
+ /*
+ * "malicious" '\r' after continuation line continued
+ */
+ @Test
+ public void testNameContinuedContinuedWithCr() throws Exception {
+ test("testNameContinuedContinuedWithCr", m -> {
+ return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+ FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
+ FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r " +
+ FILENAME_INITIAL_CONTENTS.substring(4) + "\r\n" +
+ "Foo: Bar")
+ ).getBytes(UTF_8);
+ });
+ }
+
+ /*
+ * "malicious" '\r' after continued continuation line
+ */
+ @Test
+ public void testNameContinuedContinuedEndingWithCr() throws Exception {
+ test("testNameContinuedContinuedEndingWithCr", m -> {
+ return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
+ FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
+ FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r\n " +
+ FILENAME_INITIAL_CONTENTS.substring(4) + "\r" + // no '\n'
+ "Foo: Bar")
+ ).getBytes(UTF_8);
+ });
+ }
+
+ @DataProvider(name = "trailingSeqParams", parallel = true)
+ public static Object[][] trailingSeqParams() {
+ return new Object[][] {
+ {""},
+ {"\r"},
+ {"\n"},
+ {"\r\n"},
+ {"\r\r"},
+ {"\n\n"},
+ {"\n\r"},
+ {"\r\r\r"},
+ {"\r\r\n"},
+ {"\r\n\r"},
+ {"\r\n\n"},
+ {"\n\r\r"},
+ {"\n\r\n"},
+ {"\n\n\r"},
+ {"\n\n\n"},
+ {"\r\r\r\n"},
+ {"\r\r\n\r"},
+ {"\r\r\n\n"},
+ {"\r\n\r\r"},
+ {"\r\n\r\n"},
+ {"\r\n\n\r"},
+ {"\r\n\n\n"},
+ {"\n\r\r\n"},
+ {"\n\r\n\r"},
+ {"\n\r\n\n"},
+ {"\n\n\r\n"},
+ {"\r\r\n\r\n"},
+ {"\r\n\r\r\n"},
+ {"\r\n\r\n\r"},
+ {"\r\n\r\n\n"},
+ {"\r\n\n\r\n"},
+ {"\n\r\n\r\n"},
+ {"\r\n\r\n\r\n"},
+ {"\r\n\r\n\r\n\r\n"}
+ };
+ }
+
+ boolean isSufficientSectionDelimiter(String trailingSeq) {
+ if (trailingSeq.length() < 2) return false;
+ if (trailingSeq.startsWith("\r\n")) {
+ trailingSeq = trailingSeq.substring(2);
+ } else if (trailingSeq.startsWith("\r") ||
+ trailingSeq.startsWith("\n")) {
+ trailingSeq = trailingSeq.substring(1);
+ } else {
+ return false;
+ }
+ if (trailingSeq.startsWith("\r\n")) {
+ return true;
+ } else if (trailingSeq.startsWith("\r") ||
+ trailingSeq.startsWith("\n")) {
+ return true;
+ }
+ return false;
+ }
+
+ Function replaceTrailingLineBreaksManipulation(
+ String trailingSeq) {
+ return m -> {
+ StringBuilder sb = new StringBuilder(manifestToString(m));
+ // cut off default trailing line break characters
+ while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ // and instead add another trailing sequence
+ sb.append(trailingSeq);
+ return sb.toString().getBytes(UTF_8);
+ };
+ }
+
+ boolean abSigFilesEqual(String jarFilename,
+ Function getter) throws IOException {
+ try (ZipFile zip = new ZipFile(jarFilename)) {
+ ZipEntry ea = zip.getEntry("META-INF/A.SF");
+ Manifest sfa = new Manifest(zip.getInputStream(ea));
+ ZipEntry eb = zip.getEntry("META-INF/B.SF");
+ Manifest sfb = new Manifest(zip.getInputStream(eb));
+ return getter.apply(sfa).equals(getter.apply(sfb));
+ }
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the main attributes and no individual section and hence no file contained
+ * within the JAR file in order not to produce an individual section,
+ * then add no other file and sign it with a different signer.
+ * The manifest is not expected to be changed during the second signature.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void emptyJarTrailingSeq(String trailingSeq) throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ if (trailingSeq.isEmpty()) {
+ return; // invalid manifest without trailing line break
+ }
+
+ test("emptyJarTrailingSeq" + trailingSeqEscaped, null, null,
+ replaceTrailingLineBreaksManipulation(trailingSeq));
+
+ // test called above already asserts by default that the main attributes
+ // digests have not changed.
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the main attributes and no individual section and hence no file contained
+ * within the JAR file in order not to produce an individual section,
+ * then add another file and sign it with a different signer so that the
+ * originally trailing sequence after the main attributes might have to be
+ * completed to a full section delimiter or reproduced only partially
+ * before the new individual section with the added file digest can be
+ * appended. The main attributes digests are expected to change if the
+ * first signed trailing sequence did not contain a blank line and are not
+ * expected to change if superfluous parts of the trailing sequence were
+ * not reproduced. All digests are expected to validate either with digest
+ * or with digestWorkaround.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void emptyJarTrailingSeqAddFile(String trailingSeq) throws Exception{
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ if (!isSufficientSectionDelimiter(trailingSeq)) {
+ return; // invalid manifest without trailing blank line
+ }
+ boolean expectUnchangedDigests =
+ isSufficientSectionDelimiter(trailingSeq);
+ System.out.println("expectUnchangedDigests = " + expectUnchangedDigests);
+ String jarFilename = test("emptyJarTrailingSeqAddFile" +
+ trailingSeqEscaped, null, FILENAME_UPDATED_CONTENTS,
+ replaceTrailingLineBreaksManipulation(trailingSeq),
+ null, expectUnchangedDigests, false);
+
+ // Check that the digests have changed only if another line break had
+ // to be added before a new individual section. That both also are valid
+ // with either digest or digestWorkaround has been checked by test
+ // before.
+ assertEquals(abSigFilesEqual(jarFilename, sf -> sf.getMainAttributes()
+ .getValue("SHA-256-Digest-Manifest-Main-Attributes")),
+ expectUnchangedDigests);
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the only individual section holding the digest of the only file contained
+ * within the JAR file,
+ * then add no other file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature only
+ * by removing superfluous line break characters which are not digested
+ * and the manifest entry digest is expected not to change.
+ * The individual section is expected to be reproduced without additional
+ * line breaks even if the trailing sequence does not properly delimit
+ * another section.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void singleIndividualSectionTrailingSeq(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ if (trailingSeq.isEmpty()) {
+ return; // invalid manifest without trailing line break
+ }
+ String jarFilename = test("singleIndividualSectionTrailingSeq"
+ + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null,
+ replaceTrailingLineBreaksManipulation(trailingSeq));
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the first individual section holding the digest of the only file
+ * contained within the JAR file and a second individual section with the
+ * same name to be both digested into the same entry digest,
+ * then add no other file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature
+ * by removing superfluous line break characters which are not digested
+ * anyway or if the trailingSeq is not a sufficient delimiter that both
+ * intially provided sections are treated as only one which is maybe not
+ * perfect but does at least not result in an invalid signed jar file.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void firstIndividualSectionTrailingSeq(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ String jarFilename;
+ jarFilename = test("firstIndividualSectionTrailingSeq"
+ + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
+ StringBuilder sb = new StringBuilder(manifestToString(m));
+ // cut off default trailing line break characters
+ while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ // and instead add another trailing sequence
+ sb.append(trailingSeq);
+ // now add another section with the same name assuming sb
+ // already contains one entry for FILENAME_INITIAL_CONTENTS
+ sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+ sb.append("Foo: Bar\r\n");
+ sb.append("\r\n");
+ return sb.toString().getBytes(UTF_8);
+ });
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ /**
+ * Create a signed JAR file with two individual sections for the same
+ * contained file (corresponding by name) the first of which properly
+ * delimited and the second of which followed by a strange sequence of
+ * line breaks both digested into the same entry digest,
+ * then add no other file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature
+ * by removing superfluous line break characters which are not digested
+ * anyway.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void secondIndividualSectionTrailingSeq(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ String jarFilename = test("secondIndividualSectionTrailingSeq" +
+ trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
+ StringBuilder sb = new StringBuilder(manifestToString(m));
+ sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+ sb.append("Foo: Bar");
+ sb.append(trailingSeq);
+ return sb.toString().getBytes(UTF_8);
+ });
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the only individual section holding the digest of the only file contained
+ * within the JAR file,
+ * then add another file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature by
+ * removing superfluous line break characters which are not digested
+ * anyway or adding another line break to complete to a proper section
+ * delimiter blank line.
+ * The first file entry digest is expected to change only if another
+ * line break has been added.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void singleIndividualSectionTrailingSeqAddFile(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ if (!isSufficientSectionDelimiter(trailingSeq)) {
+ return; // invalid manifest without trailing blank line
+ }
+ String jarFilename = test("singleIndividualSectionTrailingSeqAddFile"
+ + trailingSeqEscaped,
+ FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
+ replaceTrailingLineBreaksManipulation(trailingSeq),
+ null, true, true);
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ /**
+ * Create a signed JAR file with a strange sequence of line breaks after
+ * the first individual section holding the digest of the only file
+ * contained within the JAR file and a second individual section with the
+ * same name to be both digested into the same entry digest,
+ * then add another file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature
+ * by removing superfluous line break characters which are not digested
+ * anyway or if the trailingSeq is not a sufficient delimiter that both
+ * intially provided sections are treated as only one which is maybe not
+ * perfect but does at least not result in an invalid signed jar file.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void firstIndividualSectionTrailingSeqAddFile(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ String jarFilename = test("firstIndividualSectionTrailingSeqAddFile"
+ + trailingSeqEscaped,
+ FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
+ StringBuilder sb = new StringBuilder(manifestToString(m));
+ // cut off default trailing line break characters
+ while ("\r\n".contains(sb.substring(sb.length() - 1))) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ // and instead add another trailing sequence
+ sb.append(trailingSeq);
+ // now add another section with the same name assuming sb
+ // already contains one entry for FILENAME_INITIAL_CONTENTS
+ sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+ sb.append("Foo: Bar\r\n");
+ sb.append("\r\n");
+ return sb.toString().getBytes(UTF_8);
+ });
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ /**
+ * Create a signed JAR file with two individual sections for the same
+ * contained file (corresponding by name) the first of which properly
+ * delimited and the second of which followed by a strange sequence of
+ * line breaks both digested into the same entry digest,
+ * then add another file and sign it with a different signer.
+ * The manifest is expected to be changed during the second signature
+ * by removing superfluous line break characters which are not digested
+ * anyway or by adding a proper section delimiter.
+ * The digests are expected to be changed only if another line break is
+ * added to properly delimit the next section both digests of which are
+ * expected to validate with either digest or digestWorkaround.
+ */
+ @Test(dataProvider = "trailingSeqParams")
+ public void secondIndividualSectionTrailingSeqAddFile(String trailingSeq)
+ throws Exception {
+ String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
+ UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
+ System.out.println("trailingSeq = " + trailingSeqEscaped);
+ if (!isSufficientSectionDelimiter(trailingSeq)) {
+ return; // invalid manifest without trailing blank line
+ }
+ String jarFilename = test("secondIndividualSectionTrailingSeqAddFile" +
+ trailingSeqEscaped,
+ FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
+ StringBuilder sb = new StringBuilder(manifestToString(m));
+ sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
+ sb.append("Foo: Bar");
+ sb.append(trailingSeq);
+ return sb.toString().getBytes(UTF_8);
+ }, null, true, true);
+
+ assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
+ FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
+ }
+
+ String manifestToString(Manifest mf) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ mf.write(out);
+ return new String(out.toByteArray(), UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static List byteArrayToIntList(byte[] bytes) {
+ List list = new ArrayList<>();
+ for (int i = 0; i < bytes.length; i++) {
+ list.add((int) bytes[i]);
+ }
+ return list;
+ }
+
+}