test/jdk/sun/security/tools/jarsigner/PreserveRawManifestEntryAndDigest.java
changeset 57488 94691d8e746f
equal deleted inserted replaced
57487:643978a35f6e 57488:94691d8e746f
       
     1 /*
       
     2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.
       
     8  *
       
     9  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    12  * version 2 for more details (a copy is included in the LICENSE file that
       
    13  * accompanied this code).
       
    14  *
       
    15  * You should have received a copy of the GNU General Public License version
       
    16  * 2 along with this work; if not, write to the Free Software Foundation,
       
    17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    18  *
       
    19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    20  * or visit www.oracle.com if you need additional information or have any
       
    21  * questions.
       
    22  */
       
    23 
       
    24 import java.io.ByteArrayOutputStream;
       
    25 import java.io.File;
       
    26 import java.io.FilterOutputStream;
       
    27 import java.io.IOException;
       
    28 import java.io.InputStream;
       
    29 import java.io.OutputStream;
       
    30 import java.io.PrintStream;
       
    31 import java.nio.file.Files;
       
    32 import java.nio.file.Path;
       
    33 import java.util.ArrayList;
       
    34 import java.util.Arrays;
       
    35 import java.util.List;
       
    36 import java.util.Map;
       
    37 import java.util.Collections;
       
    38 import java.util.stream.Collectors;
       
    39 import java.util.function.Function;
       
    40 import java.util.jar.Attributes;
       
    41 import java.util.jar.Attributes.Name;
       
    42 import java.util.jar.Manifest;
       
    43 import java.util.jar.JarEntry;
       
    44 import java.util.jar.JarFile;
       
    45 import java.util.zip.ZipFile;
       
    46 import java.util.zip.ZipEntry;
       
    47 import jdk.test.lib.process.OutputAnalyzer;
       
    48 import jdk.test.lib.Platform;
       
    49 import jdk.test.lib.SecurityTools;
       
    50 import jdk.test.lib.util.JarUtils;
       
    51 import org.testng.annotations.BeforeTest;
       
    52 import org.testng.annotations.DataProvider;
       
    53 import org.testng.annotations.Test;
       
    54 
       
    55 import static java.nio.charset.StandardCharsets.UTF_8;
       
    56 import static org.testng.Assert.*;
       
    57 
       
    58 /**
       
    59  * @test
       
    60  * @bug 8217375
       
    61  * @library /test/lib
       
    62  * @modules jdk.jartool/sun.security.tools.jarsigner
       
    63  * @run testng/timeout=1200 PreserveRawManifestEntryAndDigest
       
    64  * @summary Verifies that JarSigner does not change manifest file entries
       
    65  * in a binary view if its decoded map view does not change so that an
       
    66  * unchanged (individual section) entry continues to produce the same digest.
       
    67  * The same manifest (in terms of {@link Manifest#equals}) could be encoded
       
    68  * with different line breaks ("{@code \r}", "{@code \n}", or "{@code \r\n}")
       
    69  * or with arbitrary line break positions (as is also the case with the change
       
    70  * of the default line width in JDK 11, bug 6372077) resulting in a different
       
    71  * digest for manifest entries with identical values.
       
    72  *
       
    73  * <p>See also:<ul>
       
    74  * <li>{@code oldsig.sh} and {@code diffend.sh} in
       
    75  * {@code /test/jdk/sun/security/tools/jarsigner/}</li>
       
    76  * <li>{@code Compatibility.java} in
       
    77  * {@code /test/jdk/sun/security/tools/jarsigner/compatibility}</li>
       
    78  * <li>{@link ReproduceRaw} testing relevant
       
    79  * {@sun.security.util.ManifestDigester} api in much more detail</li>
       
    80  * </ul>
       
    81  */
       
    82 /*
       
    83  * debug with "run testng" += "/othervm -Djava.security.debug=jar"
       
    84  */
       
    85 public class PreserveRawManifestEntryAndDigest {
       
    86 
       
    87     static final String KEYSTORE_FILENAME = "test.jks";
       
    88     static final String FILENAME_INITIAL_CONTENTS = "initial-contents";
       
    89     static final String FILENAME_UPDATED_CONTENTS = "updated-contents";
       
    90 
       
    91     /**
       
    92      * @see sun.security.tools.jarsigner.Main#run
       
    93      */
       
    94     static final int NOTSIGNEDBYALIASORALIASNOTINSTORE = 32;
       
    95 
       
    96     @BeforeTest
       
    97     public void prepareContentFiles() throws IOException {
       
    98         Files.write(Path.of(FILENAME_INITIAL_CONTENTS),
       
    99                 FILENAME_INITIAL_CONTENTS.getBytes(UTF_8));
       
   100         Files.write(Path.of(FILENAME_UPDATED_CONTENTS),
       
   101                 FILENAME_UPDATED_CONTENTS.getBytes(UTF_8));
       
   102     }
       
   103 
       
   104     @BeforeTest
       
   105     public void prepareCertificates() throws Exception {
       
   106         SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
       
   107                 + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
       
   108                 + " -alias a -dname CN=A").shouldHaveExitValue(0);
       
   109         SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
       
   110                 + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
       
   111                 + " -alias b -dname CN=B").shouldHaveExitValue(0);
       
   112     }
       
   113 
       
   114     static class TeeOutputStream extends FilterOutputStream {
       
   115         final OutputStream tee; // don't flush or close
       
   116 
       
   117         public TeeOutputStream(OutputStream out, OutputStream tee) {
       
   118             super(out);
       
   119             this.tee = tee;
       
   120         }
       
   121 
       
   122         @Override
       
   123         public void write(int b) throws IOException {
       
   124             super.write(b);
       
   125             tee.write(b);
       
   126         }
       
   127     }
       
   128 
       
   129     /**
       
   130      * runs jarsigner in its own child process and captures exit code and the
       
   131      * output of stdout and stderr, as opposed to {@link #karsignerMain}
       
   132      */
       
   133     OutputAnalyzer jarsignerProc(String args) throws Exception {
       
   134         long start = System.currentTimeMillis();
       
   135         try {
       
   136             return SecurityTools.jarsigner(args);
       
   137         } finally {
       
   138             long end = System.currentTimeMillis();
       
   139             System.out.println("jarsignerProc duration [ms]: " + (end - start));
       
   140         }
       
   141     }
       
   142 
       
   143     /**
       
   144      * assume non-zero exit code would call System.exit but is faster than
       
   145      * {@link #jarsignerProc}
       
   146      */
       
   147     void jarsignerMain(String args) throws Exception {
       
   148         long start = System.currentTimeMillis();
       
   149         try {
       
   150             new sun.security.tools.jarsigner.Main().run(args.split("\\s+"));
       
   151         } finally {
       
   152             long end = System.currentTimeMillis();
       
   153             System.out.println("jarsignerMain duration [ms]: " + (end - start));
       
   154         }
       
   155     }
       
   156 
       
   157     void createSignedJarA(String jarFilename, Manifest manifest,
       
   158             String additionalJarsignerOptions, String dummyContentsFilename)
       
   159                     throws Exception {
       
   160         JarUtils.createJarFile(Path.of(jarFilename), manifest, Path.of("."),
       
   161                 dummyContentsFilename == null ? new Path[]{} :
       
   162                     new Path[] { Path.of(dummyContentsFilename) });
       
   163         jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
       
   164                 + (additionalJarsignerOptions == null ? "" :
       
   165                     " " + additionalJarsignerOptions) +
       
   166                 " -verbose -debug " + jarFilename + " a");
       
   167         Utils.echoManifest(Utils.readJarManifestBytes(
       
   168                 jarFilename), "original signed jar by signer a");
       
   169         // check assumption that jar is valid at this point
       
   170         jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME +
       
   171                 " -storepass changeit -verbose -debug " + jarFilename + " a");
       
   172     }
       
   173 
       
   174     void manipulateManifestSignAgainA(String srcJarFilename, String tmpFilename,
       
   175             String dstJarFilename, String additionalJarsignerOptions,
       
   176             Function<Manifest, byte[]> manifestManipulation) throws Exception {
       
   177         Manifest mf;
       
   178         try (JarFile jar = new JarFile(srcJarFilename)) {
       
   179             mf = jar.getManifest();
       
   180         }
       
   181         byte[] manipulatedManifest = manifestManipulation.apply(mf);
       
   182         Utils.echoManifest(manipulatedManifest, "manipulated manifest");
       
   183         JarUtils.updateJar(srcJarFilename, tmpFilename, Map.of(
       
   184                 JarFile.MANIFEST_NAME, manipulatedManifest,
       
   185                 // add a fake sig-related file to trigger wasSigned in JarSigner
       
   186                 "META-INF/.SF", Name.SIGNATURE_VERSION + ": 1.0\r\n"));
       
   187         jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
       
   188                 + (additionalJarsignerOptions == null ? "" :
       
   189                     " " + additionalJarsignerOptions) +
       
   190                 " -verbose -debug " + tmpFilename + " a");
       
   191         // remove META-INF/.SF from signed jar again which would not validate
       
   192         JarUtils.updateJar(tmpFilename, dstJarFilename,
       
   193                 Map.of("META-INF/.SF", false));
       
   194 
       
   195         Utils.echoManifest(Utils.readJarManifestBytes(
       
   196                 dstJarFilename), "manipulated jar signed again with a");
       
   197         // check assumption that jar is valid at this point
       
   198         jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME + " " +
       
   199                 "-storepass changeit -verbose -debug " + dstJarFilename + " a");
       
   200     }
       
   201 
       
   202     OutputAnalyzer signB(String jarFilename, String additionalJarsignerOptions,
       
   203             int updateExitCodeVerifyA) throws Exception {
       
   204         jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
       
   205                 + (additionalJarsignerOptions == null ? "" :
       
   206                     " " + additionalJarsignerOptions)
       
   207                 + " -verbose -debug " + jarFilename + " b");
       
   208         Utils.echoManifest(Utils.readJarManifestBytes(
       
   209                 jarFilename), "signed again with signer b");
       
   210         // check assumption that jar is valid at this point with any alias
       
   211         jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
       
   212                 " -storepass changeit -debug -verbose " + jarFilename);
       
   213         // check assumption that jar is valid at this point with b just signed
       
   214         jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
       
   215                 " -storepass changeit -debug -verbose " + jarFilename + " b");
       
   216         // return result of verification of signature by a before update
       
   217         return jarsignerProc("-verify -strict " + "-keystore " +
       
   218                 KEYSTORE_FILENAME + " -storepass changeit " + "-debug " +
       
   219                 "-verbose " + jarFilename + " a")
       
   220                 .shouldHaveExitValue(updateExitCodeVerifyA);
       
   221     }
       
   222 
       
   223     String[] fromFirstToSecondEmptyLine(String[] lines) {
       
   224         int from = 0;
       
   225         for (int i = 0; i < lines.length; i++) {
       
   226             if ("".equals(lines[i])) {
       
   227                 from = i + 1;
       
   228                 break;
       
   229             }
       
   230         }
       
   231 
       
   232         int to = lines.length - 1;
       
   233         for (int i = from; i < lines.length; i++) {
       
   234             if ("".equals(lines[i])) {
       
   235                 to = i - 1;
       
   236                 break;
       
   237             }
       
   238         }
       
   239 
       
   240         return Arrays.copyOfRange(lines, from, to + 1);
       
   241     }
       
   242 
       
   243     /**
       
   244      * @see "concise_jarsigner.sh"
       
   245      */
       
   246     String[] getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
       
   247             String jarFilename, String digestalg,
       
   248             String firstAddedFilename, String secondAddedFilename) {
       
   249         final String TS = ".{28,29}"; // matches a timestamp
       
   250         List<String> expLines = new ArrayList<>();
       
   251         expLines.add("s k   *\\d+ " + TS + " META-INF/MANIFEST[.]MF");
       
   252         expLines.add("      *\\d+ " + TS + " META-INF/B[.]SF");
       
   253         expLines.add("      *\\d+ " + TS + " META-INF/B[.]DSA");
       
   254         expLines.add("      *\\d+ " + TS + " META-INF/A[.]SF");
       
   255         expLines.add("      *\\d+ " + TS + " META-INF/A[.]DSA");
       
   256         if (firstAddedFilename != null) {
       
   257             expLines.add("smk   *\\d+ " + TS + " " + firstAddedFilename);
       
   258         }
       
   259         if (secondAddedFilename != null) {
       
   260             expLines.add("smkX  *\\d+ " + TS + " " + secondAddedFilename);
       
   261         }
       
   262         return expLines.toArray(new String[expLines.size()]);
       
   263     }
       
   264 
       
   265     void assertMatchByLines(String[] actLines, String[] expLines) {
       
   266         for (int i = 0; i < actLines.length && i < expLines.length; i++) {
       
   267             String actLine = actLines[i];
       
   268             String expLine = expLines[i];
       
   269             assertTrue(actLine.matches("^" + expLine + "$"),
       
   270                 "\"" + actLine + "\" should have matched \"" + expLine + "\"");
       
   271         }
       
   272         assertEquals(actLines.length, expLines.length);
       
   273     }
       
   274 
       
   275     String test(String name, Function<Manifest, byte[]> mm) throws Exception {
       
   276         return test(name, FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
       
   277                 mm);
       
   278     }
       
   279 
       
   280     String test(String name,
       
   281             String firstAddedFilename, String secondAddedFilename,
       
   282             Function<Manifest, byte[]> mm) throws Exception {
       
   283         return test(name, firstAddedFilename, secondAddedFilename, mm, null,
       
   284                 true, true);
       
   285     }
       
   286 
       
   287     /**
       
   288      * Essentially, creates a first signed JAR file with a single contained
       
   289      * file or without and a manipulation applied to its manifest signed by
       
   290      * signer a and then signes it again with a different signer b.
       
   291      * The jar file is signed twice with signer a in order to make the digests
       
   292      * available to the manipulation function that might use it.
       
   293      *
       
   294      * @param name Prefix for the JAR filenames used throughout the test.
       
   295      * @param firstAddedFilename Name of a file to add before the first
       
   296      * signature by signer a or null. The name will also become the contents
       
   297      * if not null.
       
   298      * @param secondAddedFilename Name of a file to add after the first
       
   299      * signature by signer a and before the second signature by signer b or
       
   300      * null. The name will also become the contents if not null.
       
   301      * @param manifestManipulation A callback hook to manipulate the manifest
       
   302      * after the first signature by signer a and before the second signature by
       
   303      * signer b.
       
   304      * @param digestalg The digest algorithm name to be used or null for
       
   305      * default.
       
   306      * @param assertMainAttrsDigestsUnchanged Assert that the
       
   307      * manifest main attributes digests have not changed. In any case the test
       
   308      * also checks that the digests are still valid whether changed or not
       
   309      * by {@code jarsigner -verify} which might use
       
   310      * {@link ManifestDigester.Entry#digestWorkaround}
       
   311      * @param assertFirstAddedFileDigestsUnchanged Assert that the
       
   312      * digest of the file firstAddedFilename has not changed with the second
       
   313      * signature. In any case the test checks that the digests are valid whether
       
   314      * changed or not by {@code jarsigner -verify} which might use
       
   315      * {@link ManifestDigester.Entry#digestWorkaround}
       
   316      * @return The name of the resulting JAR file that has passed the common
       
   317      * assertions ready for further examination
       
   318      */
       
   319     String test(String name,
       
   320             String firstAddedFilename, String secondAddedFilename,
       
   321             Function<Manifest, byte[]> manifestManipulation,
       
   322             String digestalg, boolean assertMainAttrsDigestsUnchanged,
       
   323             boolean assertFirstAddedFileDigestsUnchanged)
       
   324                     throws Exception {
       
   325         String digOpts = (digestalg != null ? "-digestalg " + digestalg : "");
       
   326         String jarFilename1 = "test-" + name + "-step1.jar";
       
   327         createSignedJarA(jarFilename1,
       
   328                 /* no manifest will let jarsigner create a default one */ null,
       
   329                 digOpts, firstAddedFilename);
       
   330 
       
   331         // manipulate the manifest, write it back, and sign the jar again with
       
   332         // the same certificate a as before overwriting the first signature
       
   333         String jarFilename2 = "test-" + name + "-step2.jar";
       
   334         String jarFilename3 = "test-" + name + "-step3.jar";
       
   335         manipulateManifestSignAgainA(jarFilename1, jarFilename2, jarFilename3,
       
   336                 digOpts, manifestManipulation);
       
   337 
       
   338         // add another file, sign it with the other certificate, and verify it
       
   339         String jarFilename4 = "test-" + name + "-step4.jar";
       
   340         JarUtils.updateJar(jarFilename3, jarFilename4,
       
   341                 secondAddedFilename != null ?
       
   342                 Map.of(secondAddedFilename, secondAddedFilename)
       
   343                 : Collections.EMPTY_MAP);
       
   344         OutputAnalyzer o = signB(jarFilename4, digOpts,
       
   345            secondAddedFilename != null ? NOTSIGNEDBYALIASORALIASNOTINSTORE : 0);
       
   346         // check that secondAddedFilename is the only entry which is not signed
       
   347         // by signer with alias "a" unless secondAddedFilename is null
       
   348         assertMatchByLines(
       
   349                 fromFirstToSecondEmptyLine(o.getStdout().split("\\R")),
       
   350                 getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
       
   351                         jarFilename4, digestalg,
       
   352                         firstAddedFilename, secondAddedFilename));
       
   353 
       
   354         // double-check reading the files with a verifying JarFile
       
   355         try (JarFile jar = new JarFile(jarFilename4, true)) {
       
   356             if (firstAddedFilename != null) {
       
   357                 JarEntry je1 = jar.getJarEntry(firstAddedFilename);
       
   358                 jar.getInputStream(je1).readAllBytes();
       
   359                 assertTrue(je1.getCodeSigners().length > 0);
       
   360             }
       
   361             if (secondAddedFilename != null) {
       
   362                 JarEntry je2 = jar.getJarEntry(secondAddedFilename);
       
   363                 jar.getInputStream(je2).readAllBytes();
       
   364                 assertTrue(je2.getCodeSigners().length > 0);
       
   365             }
       
   366         }
       
   367 
       
   368         // assert that the signature of firstAddedFilename signed by signer
       
   369         // with alias "a" is not lost and its digest remains the same
       
   370         try (ZipFile zip = new ZipFile(jarFilename4)) {
       
   371             ZipEntry ea = zip.getEntry("META-INF/A.SF");
       
   372             Manifest sfa = new Manifest(zip.getInputStream(ea));
       
   373             ZipEntry eb = zip.getEntry("META-INF/B.SF");
       
   374             Manifest sfb = new Manifest(zip.getInputStream(eb));
       
   375             if (assertMainAttrsDigestsUnchanged) {
       
   376                 String mainAttrsDigKey =
       
   377                         (digestalg != null ? digestalg : "SHA-256") +
       
   378                         "-Digest-Manifest-Main-Attributes";
       
   379                 assertEquals(sfa.getMainAttributes().getValue(mainAttrsDigKey),
       
   380                              sfb.getMainAttributes().getValue(mainAttrsDigKey));
       
   381             }
       
   382             if (assertFirstAddedFileDigestsUnchanged) {
       
   383                 assertEquals(sfa.getAttributes(firstAddedFilename),
       
   384                              sfb.getAttributes(firstAddedFilename));
       
   385             }
       
   386         }
       
   387 
       
   388         return jarFilename4;
       
   389     }
       
   390 
       
   391     /**
       
   392      * Test that signing a jar with manifest entries with arbitrary line break
       
   393      * positions in individual section headers does not destroy an existing
       
   394      * signature<ol>
       
   395      * <li>create two self-signed certificates</li>
       
   396      * <li>sign a jar with at least one non-META-INF file in it with a JDK
       
   397      * before 11 or place line breaks not at 72 bytes in an individual section
       
   398      * header</li>
       
   399      * <li>add a new file to the jar</li>
       
   400      * <li>sign the jar with a JDK 11, 12, or 13 with bug 8217375 not yet
       
   401      * resolved with a different signer</li>
       
   402      * </ol>&rarr; first signature will not validate anymore even though it
       
   403      * should.
       
   404      */
       
   405     @Test
       
   406     public void arbitraryLineBreaksSectionName() throws Exception {
       
   407         test("arbitraryLineBreaksSectionName", m -> {
       
   408             return (
       
   409                 Name.MANIFEST_VERSION + ": 1.0\r\n" +
       
   410                 "Created-By: " +
       
   411                         m.getMainAttributes().getValue("Created-By") + "\r\n" +
       
   412                 "\r\n" +
       
   413                 "Name: Test\r\n" +
       
   414                 " -\r\n" +
       
   415                 " Section\r\n" +
       
   416                 "Key: Value \r\n" +
       
   417                 "\r\n" +
       
   418                 "Name: " + FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n" +
       
   419                 " " + FILENAME_INITIAL_CONTENTS.substring(1, 8) + "\r\n" +
       
   420                 " " + FILENAME_INITIAL_CONTENTS.substring(8) + "\r\n" +
       
   421                 "SHA-256-Digest: " + m.getAttributes(FILENAME_INITIAL_CONTENTS)
       
   422                         .getValue("SHA-256-Digest") + "\r\n" +
       
   423                 "\r\n"
       
   424             ).getBytes(UTF_8);
       
   425         });
       
   426     }
       
   427 
       
   428     /**
       
   429      * Test that signing a jar with manifest entries with arbitrary line break
       
   430      * positions in individual section headers does not destroy an existing
       
   431      * signature<ol>
       
   432      * <li>create two self-signed certificates</li>
       
   433      * <li>sign a jar with at least one non-META-INF file in it with a JDK
       
   434      * before 11 or place line breaks not at 72 bytes in an individual section
       
   435      * header</li>
       
   436      * <li>add a new file to the jar</li>
       
   437      * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
       
   438      * </ol>&rarr; first signature will not validate anymore even though it
       
   439      * should.
       
   440      */
       
   441     @Test
       
   442     public void arbitraryLineBreaksHeader() throws Exception {
       
   443         test("arbitraryLineBreaksHeader", m -> {
       
   444             String digest = m.getAttributes(FILENAME_INITIAL_CONTENTS)
       
   445                     .getValue("SHA-256-Digest");
       
   446             return (
       
   447                 Name.MANIFEST_VERSION + ": 1.0\r\n" +
       
   448                 "Created-By: " +
       
   449                         m.getMainAttributes().getValue("Created-By") + "\r\n" +
       
   450                 "\r\n" +
       
   451                 "Name: Test-Section\r\n" +
       
   452                 "Key: Value \r\n" +
       
   453                 " with\r\n" +
       
   454                 "  strange \r\n" +
       
   455                 " line breaks.\r\n" +
       
   456                 "\r\n" +
       
   457                 "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
       
   458                 "SHA-256-Digest: " + digest.substring(0, 11) + "\r\n" +
       
   459                 " " + digest.substring(11) + "\r\n" +
       
   460                 "\r\n"
       
   461             ).getBytes(UTF_8);
       
   462         });
       
   463     }
       
   464 
       
   465     /**
       
   466      * Breaks {@code line} at 70 bytes even though the name says 72 but when
       
   467      * also counting the line delimiter ("{@code \r\n}") the line totals to 72
       
   468      * bytes.
       
   469      * Borrowed from {@link Manifest#make72Safe} before JDK 11
       
   470      *
       
   471      * @see Manifest#make72Safe
       
   472      */
       
   473     static void make72Safe(StringBuffer line) {
       
   474         int length = line.length();
       
   475         if (length > 72) {
       
   476             int index = 70;
       
   477             while (index < length - 2) {
       
   478                 line.insert(index, "\r\n ");
       
   479                 index += 72;
       
   480                 length += 3;
       
   481             }
       
   482         }
       
   483         return;
       
   484     }
       
   485 
       
   486     /**
       
   487      * Test that signing a jar with manifest entries with line breaks at
       
   488      * position where Manifest would not place them now anymore (72 instead of
       
   489      * 70 bytes after line starts) does not destroy an existing signature<ol>
       
   490      * <li>create two self-signed certificates</li>
       
   491      * <li>simulate a manifest as it would have been written by a JDK before 11
       
   492      * by re-positioning line breaks at 70 bytes (which makes a difference by
       
   493      * digests that grow headers longer than 70 characters such as SHA-512 as
       
   494      * opposed to default SHA-256, long file names, or manual editing)</li>
       
   495      * <li>add a new file to the jar</li>
       
   496      * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
       
   497      * </ol><p>&rarr;
       
   498      * The first signature will not validate anymore even though it should.
       
   499      */
       
   500     public void lineWidth70(String name, String digestalg) throws Exception {
       
   501         Files.write(Path.of(name), name.getBytes(UTF_8));
       
   502         test(name, name, FILENAME_UPDATED_CONTENTS, m -> {
       
   503             // force a line break with a header exceeding line width limit
       
   504             m.getEntries().put("Test-Section", new Attributes());
       
   505             m.getAttributes("Test-Section").put(
       
   506                     Name.IMPLEMENTATION_VERSION, "1" + "0".repeat(100));
       
   507 
       
   508             StringBuilder sb = new StringBuilder();
       
   509             StringBuffer[] buf = new StringBuffer[] { null };
       
   510             manifestToString(m).lines().forEach(line -> {
       
   511                 if (line.startsWith(" ")) {
       
   512                     buf[0].append(line.substring(1));
       
   513                 } else {
       
   514                     if (buf[0] != null) {
       
   515                         make72Safe(buf[0]);
       
   516                         sb.append(buf[0].toString());
       
   517                         sb.append("\r\n");
       
   518                     }
       
   519                     buf[0] = new StringBuffer();
       
   520                     buf[0].append(line);
       
   521                 }
       
   522             });
       
   523             make72Safe(buf[0]);
       
   524             sb.append(buf[0].toString());
       
   525             sb.append("\r\n");
       
   526             return sb.toString().getBytes(UTF_8);
       
   527         }, digestalg, false, false);
       
   528     }
       
   529 
       
   530     @Test
       
   531     public void lineWidth70Filename() throws Exception {
       
   532         lineWidth70(
       
   533             "lineWidth70".repeat(6) /* 73 chars total with "Name: " */, null);
       
   534     }
       
   535 
       
   536     @Test
       
   537     public void lineWidth70Digest() throws Exception {
       
   538         lineWidth70("lineWidth70digest", "SHA-512");
       
   539     }
       
   540 
       
   541     /**
       
   542      * Test that signing a jar with a manifest with line delimiter other than
       
   543      * "{@code \r\n}" does not destroy an existing signature<ol>
       
   544      * <li>create two self-signed certificates</li>
       
   545      * <li>sign a jar with at least one non-META-INF file in it</li>
       
   546      * <li>extract the manifest, and change its line delimiters
       
   547      * (for example dos2unix)</li>
       
   548      * <li>update the jar with the updated manifest</li>
       
   549      * <li>sign it again with the same signer as before</li>
       
   550      * <li>add a new file to the jar</li>
       
   551      * <li>sign the jar with a JDK before 13 with a different signer<li>
       
   552      * </ol><p>&rarr;
       
   553      * The first signature will not validate anymore even though it should.
       
   554      */
       
   555     public void lineBreak(String lineBreak) throws Exception {
       
   556         test("lineBreak" + byteArrayToIntList(lineBreak.getBytes(UTF_8)).stream
       
   557                 ().map(i -> "" + i).collect(Collectors.joining("")), m -> {
       
   558             StringBuilder sb = new StringBuilder();
       
   559             manifestToString(m).lines().forEach(l -> {
       
   560                 sb.append(l);
       
   561                 sb.append(lineBreak);
       
   562             });
       
   563             return sb.toString().getBytes(UTF_8);
       
   564         });
       
   565     }
       
   566 
       
   567     @Test
       
   568     public void lineBreakCr() throws Exception {
       
   569         lineBreak("\r");
       
   570     }
       
   571 
       
   572     @Test
       
   573     public void lineBreakLf() throws Exception {
       
   574         lineBreak("\n");
       
   575     }
       
   576 
       
   577     @Test
       
   578     public void lineBreakCrLf() throws Exception {
       
   579         lineBreak("\r\n");
       
   580     }
       
   581 
       
   582     @Test
       
   583     public void testAdjacentRepeatedSection() throws Exception {
       
   584         test("adjacent", m -> {
       
   585             return (manifestToString(m) +
       
   586                     "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
       
   587                     "Foo: Bar\r\n" +
       
   588                     "\r\n"
       
   589             ).getBytes(UTF_8);
       
   590         });
       
   591     }
       
   592 
       
   593     @Test
       
   594     public void testIntermittentRepeatedSection() throws Exception {
       
   595         test("intermittent", m -> {
       
   596             return (manifestToString(m) +
       
   597                     "Name: don't know\r\n" +
       
   598                     "Foo: Bar\r\n" +
       
   599                     "\r\n" +
       
   600                     "Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +
       
   601                     "Foo: Bar\r\n" +
       
   602                     "\r\n"
       
   603             ).getBytes(UTF_8);
       
   604         });
       
   605     }
       
   606 
       
   607     @Test
       
   608     public void testNameImmediatelyContinued() throws Exception {
       
   609         test("testNameImmediatelyContinued", m -> {
       
   610             // places a continuation line break and space at the first allowed
       
   611             // position after ": " and before the first character of the value
       
   612             return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
       
   613                     "\r\n " + FILENAME_INITIAL_CONTENTS + "\r\nFoo: Bar")
       
   614             ).getBytes(UTF_8);
       
   615         });
       
   616     }
       
   617 
       
   618     /*
       
   619      * "malicious" '\r' after continuation line continued
       
   620      */
       
   621     @Test
       
   622     public void testNameContinuedContinuedWithCr() throws Exception {
       
   623         test("testNameContinuedContinuedWithCr", m -> {
       
   624             return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
       
   625                     FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
       
   626                     FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r " +
       
   627                     FILENAME_INITIAL_CONTENTS.substring(4) + "\r\n" +
       
   628                     "Foo: Bar")
       
   629             ).getBytes(UTF_8);
       
   630         });
       
   631     }
       
   632 
       
   633     /*
       
   634      * "malicious" '\r' after continued continuation line
       
   635      */
       
   636     @Test
       
   637     public void testNameContinuedContinuedEndingWithCr() throws Exception {
       
   638         test("testNameContinuedContinuedEndingWithCr", m -> {
       
   639             return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,
       
   640                     FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +
       
   641                     FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r\n " +
       
   642                     FILENAME_INITIAL_CONTENTS.substring(4) + "\r" + // no '\n'
       
   643                     "Foo: Bar")
       
   644             ).getBytes(UTF_8);
       
   645         });
       
   646     }
       
   647 
       
   648     @DataProvider(name = "trailingSeqParams", parallel = true)
       
   649     public static Object[][] trailingSeqParams() {
       
   650         return new Object[][] {
       
   651             {""},
       
   652             {"\r"},
       
   653             {"\n"},
       
   654             {"\r\n"},
       
   655             {"\r\r"},
       
   656             {"\n\n"},
       
   657             {"\n\r"},
       
   658             {"\r\r\r"},
       
   659             {"\r\r\n"},
       
   660             {"\r\n\r"},
       
   661             {"\r\n\n"},
       
   662             {"\n\r\r"},
       
   663             {"\n\r\n"},
       
   664             {"\n\n\r"},
       
   665             {"\n\n\n"},
       
   666             {"\r\r\r\n"},
       
   667             {"\r\r\n\r"},
       
   668             {"\r\r\n\n"},
       
   669             {"\r\n\r\r"},
       
   670             {"\r\n\r\n"},
       
   671             {"\r\n\n\r"},
       
   672             {"\r\n\n\n"},
       
   673             {"\n\r\r\n"},
       
   674             {"\n\r\n\r"},
       
   675             {"\n\r\n\n"},
       
   676             {"\n\n\r\n"},
       
   677             {"\r\r\n\r\n"},
       
   678             {"\r\n\r\r\n"},
       
   679             {"\r\n\r\n\r"},
       
   680             {"\r\n\r\n\n"},
       
   681             {"\r\n\n\r\n"},
       
   682             {"\n\r\n\r\n"},
       
   683             {"\r\n\r\n\r\n"},
       
   684             {"\r\n\r\n\r\n\r\n"}
       
   685         };
       
   686     }
       
   687 
       
   688     boolean isSufficientSectionDelimiter(String trailingSeq) {
       
   689         if (trailingSeq.length() < 2) return false;
       
   690         if (trailingSeq.startsWith("\r\n")) {
       
   691             trailingSeq = trailingSeq.substring(2);
       
   692         } else if (trailingSeq.startsWith("\r") ||
       
   693                    trailingSeq.startsWith("\n")) {
       
   694             trailingSeq = trailingSeq.substring(1);
       
   695         } else {
       
   696             return false;
       
   697         }
       
   698         if (trailingSeq.startsWith("\r\n")) {
       
   699             return true;
       
   700         } else if (trailingSeq.startsWith("\r") ||
       
   701                 trailingSeq.startsWith("\n")) {
       
   702             return true;
       
   703         }
       
   704         return false;
       
   705     }
       
   706 
       
   707     Function<Manifest, byte[]> replaceTrailingLineBreaksManipulation(
       
   708             String trailingSeq) {
       
   709         return m -> {
       
   710             StringBuilder sb = new StringBuilder(manifestToString(m));
       
   711             // cut off default trailing line break characters
       
   712             while ("\r\n".contains(sb.substring(sb.length() - 1))) {
       
   713                 sb.deleteCharAt(sb.length() - 1);
       
   714             }
       
   715             // and instead add another trailing sequence
       
   716             sb.append(trailingSeq);
       
   717             return sb.toString().getBytes(UTF_8);
       
   718         };
       
   719     }
       
   720 
       
   721     boolean abSigFilesEqual(String jarFilename,
       
   722             Function<Manifest,Object> getter) throws IOException {
       
   723         try (ZipFile zip = new ZipFile(jarFilename)) {
       
   724             ZipEntry ea = zip.getEntry("META-INF/A.SF");
       
   725             Manifest sfa = new Manifest(zip.getInputStream(ea));
       
   726             ZipEntry eb = zip.getEntry("META-INF/B.SF");
       
   727             Manifest sfb = new Manifest(zip.getInputStream(eb));
       
   728             return getter.apply(sfa).equals(getter.apply(sfb));
       
   729         }
       
   730     }
       
   731 
       
   732     /**
       
   733      * Create a signed JAR file with a strange sequence of line breaks after
       
   734      * the main attributes and no individual section and hence no file contained
       
   735      * within the JAR file in order not to produce an individual section,
       
   736      * then add no other file and sign it with a different signer.
       
   737      * The manifest is not expected to be changed during the second signature.
       
   738      */
       
   739     @Test(dataProvider = "trailingSeqParams")
       
   740     public void emptyJarTrailingSeq(String trailingSeq) throws Exception {
       
   741         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   742               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   743         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   744         if (trailingSeq.isEmpty()) {
       
   745             return; // invalid manifest without trailing line break
       
   746         }
       
   747 
       
   748         test("emptyJarTrailingSeq" + trailingSeqEscaped, null, null,
       
   749                 replaceTrailingLineBreaksManipulation(trailingSeq));
       
   750 
       
   751         // test called above already asserts by default that the main attributes
       
   752         // digests have not changed.
       
   753     }
       
   754 
       
   755     /**
       
   756      * Create a signed JAR file with a strange sequence of line breaks after
       
   757      * the main attributes and no individual section and hence no file contained
       
   758      * within the JAR file in order not to produce an individual section,
       
   759      * then add another file and sign it with a different signer so that the
       
   760      * originally trailing sequence after the main attributes might have to be
       
   761      * completed to a full section delimiter or reproduced only partially
       
   762      * before the new individual section with the added file digest can be
       
   763      * appended. The main attributes digests are expected to change if the
       
   764      * first signed trailing sequence did not contain a blank line and are not
       
   765      * expected to change if superfluous parts of the trailing sequence were
       
   766      * not reproduced. All digests are expected to validate either with digest
       
   767      * or with digestWorkaround.
       
   768      */
       
   769     @Test(dataProvider = "trailingSeqParams")
       
   770     public void emptyJarTrailingSeqAddFile(String trailingSeq) throws Exception{
       
   771         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   772               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   773         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   774         if (!isSufficientSectionDelimiter(trailingSeq)) {
       
   775             return; // invalid manifest without trailing blank line
       
   776         }
       
   777         boolean expectUnchangedDigests =
       
   778                 isSufficientSectionDelimiter(trailingSeq);
       
   779         System.out.println("expectUnchangedDigests = " + expectUnchangedDigests);
       
   780         String jarFilename = test("emptyJarTrailingSeqAddFile" +
       
   781                 trailingSeqEscaped, null, FILENAME_UPDATED_CONTENTS,
       
   782                 replaceTrailingLineBreaksManipulation(trailingSeq),
       
   783                 null, expectUnchangedDigests, false);
       
   784 
       
   785         // Check that the digests have changed only if another line break had
       
   786         // to be added before a new individual section. That both also are valid
       
   787         // with either digest or digestWorkaround has been checked by test
       
   788         // before.
       
   789         assertEquals(abSigFilesEqual(jarFilename, sf -> sf.getMainAttributes()
       
   790                         .getValue("SHA-256-Digest-Manifest-Main-Attributes")),
       
   791                      expectUnchangedDigests);
       
   792     }
       
   793 
       
   794     /**
       
   795      * Create a signed JAR file with a strange sequence of line breaks after
       
   796      * the only individual section holding the digest of the only file contained
       
   797      * within the JAR file,
       
   798      * then add no other file and sign it with a different signer.
       
   799      * The manifest is expected to be changed during the second signature only
       
   800      * by removing superfluous line break characters which are not digested
       
   801      * and the manifest entry digest is expected not to change.
       
   802      * The individual section is expected to be reproduced without additional
       
   803      * line breaks even if the trailing sequence does not properly delimit
       
   804      * another section.
       
   805      */
       
   806     @Test(dataProvider = "trailingSeqParams")
       
   807     public void singleIndividualSectionTrailingSeq(String trailingSeq)
       
   808             throws Exception {
       
   809         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   810               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   811         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   812         if (trailingSeq.isEmpty()) {
       
   813             return; // invalid manifest without trailing line break
       
   814         }
       
   815         String jarFilename = test("singleIndividualSectionTrailingSeq"
       
   816                 + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null,
       
   817                 replaceTrailingLineBreaksManipulation(trailingSeq));
       
   818 
       
   819         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   820                 FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   821     }
       
   822 
       
   823     /**
       
   824      * Create a signed JAR file with a strange sequence of line breaks after
       
   825      * the first individual section holding the digest of the only file
       
   826      * contained within the JAR file and a second individual section with the
       
   827      * same name to be both digested into the same entry digest,
       
   828      * then add no other file and sign it with a different signer.
       
   829      * The manifest is expected to be changed during the second signature
       
   830      * by removing superfluous line break characters which are not digested
       
   831      * anyway or if the trailingSeq is not a sufficient delimiter that both
       
   832      * intially provided sections are treated as only one which is maybe not
       
   833      * perfect but does at least not result in an invalid signed jar file.
       
   834      */
       
   835     @Test(dataProvider = "trailingSeqParams")
       
   836     public void firstIndividualSectionTrailingSeq(String trailingSeq)
       
   837             throws Exception {
       
   838         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   839               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   840         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   841         String jarFilename;
       
   842         jarFilename =  test("firstIndividualSectionTrailingSeq"
       
   843                 + trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
       
   844                 StringBuilder sb = new StringBuilder(manifestToString(m));
       
   845                 // cut off default trailing line break characters
       
   846                 while ("\r\n".contains(sb.substring(sb.length() - 1))) {
       
   847                     sb.deleteCharAt(sb.length() - 1);
       
   848                 }
       
   849                 // and instead add another trailing sequence
       
   850                 sb.append(trailingSeq);
       
   851                 // now add another section with the same name assuming sb
       
   852                 // already contains one entry for FILENAME_INITIAL_CONTENTS
       
   853                 sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
       
   854                 sb.append("Foo: Bar\r\n");
       
   855                 sb.append("\r\n");
       
   856                 return sb.toString().getBytes(UTF_8);
       
   857         });
       
   858 
       
   859         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   860                 FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   861     }
       
   862 
       
   863     /**
       
   864      * Create a signed JAR file with two individual sections for the same
       
   865      * contained file (corresponding by name) the first of which properly
       
   866      * delimited and the second of which followed by a strange sequence of
       
   867      * line breaks both digested into the same entry digest,
       
   868      * then add no other file and sign it with a different signer.
       
   869      * The manifest is expected to be changed during the second signature
       
   870      * by removing superfluous line break characters which are not digested
       
   871      * anyway.
       
   872      */
       
   873     @Test(dataProvider = "trailingSeqParams")
       
   874     public void secondIndividualSectionTrailingSeq(String trailingSeq)
       
   875             throws Exception {
       
   876         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   877               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   878         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   879         String jarFilename = test("secondIndividualSectionTrailingSeq" +
       
   880                 trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {
       
   881             StringBuilder sb = new StringBuilder(manifestToString(m));
       
   882             sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
       
   883             sb.append("Foo: Bar");
       
   884             sb.append(trailingSeq);
       
   885             return sb.toString().getBytes(UTF_8);
       
   886         });
       
   887 
       
   888         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   889                 FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   890     }
       
   891 
       
   892     /**
       
   893      * Create a signed JAR file with a strange sequence of line breaks after
       
   894      * the only individual section holding the digest of the only file contained
       
   895      * within the JAR file,
       
   896      * then add another file and sign it with a different signer.
       
   897      * The manifest is expected to be changed during the second signature by
       
   898      * removing superfluous line break characters which are not digested
       
   899      * anyway or adding another line break to complete to a proper section
       
   900      * delimiter blank line.
       
   901      * The first file entry digest is expected to change only if another
       
   902      * line break has been added.
       
   903      */
       
   904     @Test(dataProvider = "trailingSeqParams")
       
   905     public void singleIndividualSectionTrailingSeqAddFile(String trailingSeq)
       
   906             throws Exception {
       
   907         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   908               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   909         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   910         if (!isSufficientSectionDelimiter(trailingSeq)) {
       
   911             return; // invalid manifest without trailing blank line
       
   912         }
       
   913         String jarFilename = test("singleIndividualSectionTrailingSeqAddFile"
       
   914                 + trailingSeqEscaped,
       
   915                 FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
       
   916                 replaceTrailingLineBreaksManipulation(trailingSeq),
       
   917                 null, true, true);
       
   918 
       
   919         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   920                         FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   921     }
       
   922 
       
   923     /**
       
   924      * Create a signed JAR file with a strange sequence of line breaks after
       
   925      * the first individual section holding the digest of the only file
       
   926      * contained within the JAR file and a second individual section with the
       
   927      * same name to be both digested into the same entry digest,
       
   928      * then add another file and sign it with a different signer.
       
   929      * The manifest is expected to be changed during the second signature
       
   930      * by removing superfluous line break characters which are not digested
       
   931      * anyway or if the trailingSeq is not a sufficient delimiter that both
       
   932      * intially provided sections are treated as only one which is maybe not
       
   933      * perfect but does at least not result in an invalid signed jar file.
       
   934      */
       
   935     @Test(dataProvider = "trailingSeqParams")
       
   936     public void firstIndividualSectionTrailingSeqAddFile(String trailingSeq)
       
   937             throws Exception {
       
   938         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   939               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   940         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   941         String jarFilename =  test("firstIndividualSectionTrailingSeqAddFile"
       
   942                 + trailingSeqEscaped,
       
   943                 FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
       
   944                 StringBuilder sb = new StringBuilder(manifestToString(m));
       
   945                 // cut off default trailing line break characters
       
   946                 while ("\r\n".contains(sb.substring(sb.length() - 1))) {
       
   947                     sb.deleteCharAt(sb.length() - 1);
       
   948                 }
       
   949                 // and instead add another trailing sequence
       
   950                 sb.append(trailingSeq);
       
   951                 // now add another section with the same name assuming sb
       
   952                 // already contains one entry for FILENAME_INITIAL_CONTENTS
       
   953                 sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
       
   954                 sb.append("Foo: Bar\r\n");
       
   955                 sb.append("\r\n");
       
   956                 return sb.toString().getBytes(UTF_8);
       
   957         });
       
   958 
       
   959         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   960                 FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   961     }
       
   962 
       
   963     /**
       
   964      * Create a signed JAR file with two individual sections for the same
       
   965      * contained file (corresponding by name) the first of which properly
       
   966      * delimited and the second of which followed by a strange sequence of
       
   967      * line breaks both digested into the same entry digest,
       
   968      * then add another file and sign it with a different signer.
       
   969      * The manifest is expected to be changed during the second signature
       
   970      * by removing superfluous line break characters which are not digested
       
   971      * anyway or by adding a proper section delimiter.
       
   972      * The digests are expected to be changed only if another line break is
       
   973      * added to properly delimit the next section both digests of which are
       
   974      * expected to validate with either digest or digestWorkaround.
       
   975      */
       
   976     @Test(dataProvider = "trailingSeqParams")
       
   977     public void secondIndividualSectionTrailingSeqAddFile(String trailingSeq)
       
   978             throws Exception {
       
   979         String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(
       
   980               UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));
       
   981         System.out.println("trailingSeq = " + trailingSeqEscaped);
       
   982         if (!isSufficientSectionDelimiter(trailingSeq)) {
       
   983             return; // invalid manifest without trailing blank line
       
   984         }
       
   985         String jarFilename = test("secondIndividualSectionTrailingSeqAddFile" +
       
   986                 trailingSeqEscaped,
       
   987                 FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {
       
   988             StringBuilder sb = new StringBuilder(manifestToString(m));
       
   989             sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");
       
   990             sb.append("Foo: Bar");
       
   991             sb.append(trailingSeq);
       
   992             return sb.toString().getBytes(UTF_8);
       
   993         }, null, true, true);
       
   994 
       
   995         assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(
       
   996                 FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));
       
   997     }
       
   998 
       
   999     String manifestToString(Manifest mf) {
       
  1000         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
       
  1001             mf.write(out);
       
  1002             return new String(out.toByteArray(), UTF_8);
       
  1003         } catch (IOException e) {
       
  1004             throw new RuntimeException(e);
       
  1005         }
       
  1006     }
       
  1007 
       
  1008     static List<Integer> byteArrayToIntList(byte[] bytes) {
       
  1009         List<Integer> list = new ArrayList<>();
       
  1010         for (int i = 0; i < bytes.length; i++) {
       
  1011             list.add((int) bytes[i]);
       
  1012         }
       
  1013         return list;
       
  1014     }
       
  1015 
       
  1016 }