File: PreserveRawManifestEntryAndDigest.java

package info (click to toggle)
openjdk-21 21.0.8%2B9-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 823,976 kB
  • sloc: java: 5,613,338; xml: 1,643,607; cpp: 1,296,296; ansic: 420,291; asm: 404,850; objc: 20,994; sh: 15,271; javascript: 11,245; python: 6,895; makefile: 2,362; perl: 357; awk: 351; sed: 172; jsp: 24; csh: 3
file content (1018 lines) | stat: -rw-r--r-- 45,556 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
/*
 * Copyright (c) 2019, 2022, 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.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.function.Function;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
import java.util.zip.ZipEntry;
import jdk.security.jarsigner.JarSigner;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.Platform;
import jdk.test.lib.SecurityTools;
import jdk.test.lib.util.JarUtils;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.*;

/**
 * @test
 * @bug 8217375 8267319
 * @library /test/lib
 * @modules jdk.jartool/sun.security.tools.jarsigner
 * @run testng/timeout=1200 PreserveRawManifestEntryAndDigest
 * @summary Verifies that JarSigner does not change manifest file entries
 * in a binary view if its decoded map view does not change so that an
 * unchanged (individual section) entry continues to produce the same digest.
 * The same manifest (in terms of {@link Manifest#equals}) could be encoded
 * with different line breaks ("{@code \r}", "{@code \n}", or "{@code \r\n}")
 * or with arbitrary line break positions (as is also the case with the change
 * of the default line width in JDK 11, bug 6372077) resulting in a different
 * digest for manifest entries with identical values.
 *
 * <p>See also:<ul>
 * <li>{@code oldsig.sh} and {@code diffend.sh} in
 * {@code /test/jdk/sun/security/tools/jarsigner/}</li>
 * <li>{@code Compatibility.java} in
 * {@code /test/jdk/sun/security/tools/jarsigner/compatibility}</li>
 * <li>{@link ReproduceRaw} testing relevant
 * {@sun.security.util.ManifestDigester} api in much more detail</li>
 * </ul>
 */
/*
 * debug with "run testng" += "/othervm -Djava.security.debug=jar"
 */
public class PreserveRawManifestEntryAndDigest {

    static final String KEYSTORE_FILENAME = "test.jks";
    static final String FILENAME_INITIAL_CONTENTS = "initial-contents";
    static final String FILENAME_UPDATED_CONTENTS = "updated-contents";
    private static final String DEF_DIGEST_STR =
            JarSigner.Builder.getDefaultDigestAlgorithm() + "-Digest";

    /**
     * @see sun.security.tools.jarsigner.Main#run
     */
    static final int NOTSIGNEDBYALIASORALIASNOTINSTORE = 32;

    @BeforeTest
    public void prepareContentFiles() throws IOException {
        Files.write(Path.of(FILENAME_INITIAL_CONTENTS),
                FILENAME_INITIAL_CONTENTS.getBytes(UTF_8));
        Files.write(Path.of(FILENAME_UPDATED_CONTENTS),
                FILENAME_UPDATED_CONTENTS.getBytes(UTF_8));
    }

    @BeforeTest
    public void prepareCertificates() throws Exception {
        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
                + " -alias a -dname CN=A").shouldHaveExitValue(0);
        SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "
                + KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"
                + " -alias b -dname CN=B").shouldHaveExitValue(0);
    }

    static class TeeOutputStream extends FilterOutputStream {
        final OutputStream tee; // don't flush or close

        public TeeOutputStream(OutputStream out, OutputStream tee) {
            super(out);
            this.tee = tee;
        }

        @Override
        public void write(int b) throws IOException {
            super.write(b);
            tee.write(b);
        }
    }

    /**
     * runs jarsigner in its own child process and captures exit code and the
     * output of stdout and stderr, as opposed to {@link #karsignerMain}
     */
    OutputAnalyzer jarsignerProc(String args) throws Exception {
        long start = System.currentTimeMillis();
        try {
            return SecurityTools.jarsigner(args);
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("jarsignerProc duration [ms]: " + (end - start));
        }
    }

    /**
     * assume non-zero exit code would call System.exit but is faster than
     * {@link #jarsignerProc}
     */
    void jarsignerMain(String args) throws Exception {
        long start = System.currentTimeMillis();
        try {
            new sun.security.tools.jarsigner.Main().run(args.split("\\s+"));
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("jarsignerMain duration [ms]: " + (end - start));
        }
    }

    void createSignedJarA(String jarFilename, Manifest manifest,
            String additionalJarsignerOptions, String dummyContentsFilename)
                    throws Exception {
        JarUtils.createJarFile(Path.of(jarFilename), manifest, Path.of("."),
                dummyContentsFilename == null ? new Path[]{} :
                    new Path[] { Path.of(dummyContentsFilename) });
        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
                + (additionalJarsignerOptions == null ? "" :
                    " " + additionalJarsignerOptions) +
                " -verbose -debug " + jarFilename + " a");
        Utils.echoManifest(Utils.readJarManifestBytes(
                jarFilename), "original signed jar by signer a");
        // check assumption that jar is valid at this point
        jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME +
                " -storepass changeit -verbose -debug " + jarFilename + " a");
    }

    void manipulateManifestSignAgainA(String srcJarFilename, String tmpFilename,
            String dstJarFilename, String additionalJarsignerOptions,
            Function<Manifest, byte[]> manifestManipulation) throws Exception {
        Manifest mf;
        try (JarFile jar = new JarFile(srcJarFilename)) {
            mf = jar.getManifest();
        }
        byte[] manipulatedManifest = manifestManipulation.apply(mf);
        Utils.echoManifest(manipulatedManifest, "manipulated manifest");
        JarUtils.updateJar(srcJarFilename, tmpFilename, Map.of(
                JarFile.MANIFEST_NAME, manipulatedManifest,
                // add a fake sig-related file to trigger wasSigned in JarSigner
                "META-INF/.SF", Name.SIGNATURE_VERSION + ": 1.0\r\n"));
        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
                + (additionalJarsignerOptions == null ? "" :
                    " " + additionalJarsignerOptions) +
                " -verbose -debug " + tmpFilename + " a");
        // remove META-INF/.SF from signed jar again which would not validate
        JarUtils.updateJar(tmpFilename, dstJarFilename,
                Map.of("META-INF/.SF", false));

        Utils.echoManifest(Utils.readJarManifestBytes(
                dstJarFilename), "manipulated jar signed again with a");
        // check assumption that jar is valid at this point
        jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME + " " +
                "-storepass changeit -verbose -debug " + dstJarFilename + " a");
    }

    OutputAnalyzer signB(String jarFilename, String additionalJarsignerOptions,
            int updateExitCodeVerifyA) throws Exception {
        jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"
                + (additionalJarsignerOptions == null ? "" :
                    " " + additionalJarsignerOptions)
                + " -verbose -debug " + jarFilename + " b");
        Utils.echoManifest(Utils.readJarManifestBytes(
                jarFilename), "signed again with signer b");
        // check assumption that jar is valid at this point with any alias
        jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
                " -storepass changeit -debug -verbose " + jarFilename);
        // check assumption that jar is valid at this point with b just signed
        jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +
                " -storepass changeit -debug -verbose " + jarFilename + " b");
        // return result of verification of signature by a before update
        return jarsignerProc("-verify -strict " + "-keystore " +
                KEYSTORE_FILENAME + " -storepass changeit " + "-debug " +
                "-verbose " + jarFilename + " a")
                .shouldHaveExitValue(updateExitCodeVerifyA);
    }

    String[] fromFirstToSecondEmptyLine(String[] lines) {
        int from = 0;
        for (int i = 0; i < lines.length; i++) {
            if ("".equals(lines[i])) {
                from = i + 1;
                break;
            }
        }

        int to = lines.length - 1;
        for (int i = from; i < lines.length; i++) {
            if ("".equals(lines[i])) {
                to = i - 1;
                break;
            }
        }

        return Arrays.copyOfRange(lines, from, to + 1);
    }

    /**
     * @see "concise_jarsigner.sh"
     */
    String[] getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(
            String firstAddedFilename, String secondAddedFilename) {
        final String TS = ".{28,34}"; // matches a timestamp
        List<String> expLines = new ArrayList<>();
        expLines.add("s k   *\\d+ " + TS + " META-INF/MANIFEST[.]MF");
        expLines.add("      *\\d+ " + TS + " META-INF/B[.]SF");
        expLines.add("      *\\d+ " + TS + " META-INF/B[.]DSA");
        expLines.add("      *\\d+ " + TS + " META-INF/A[.]SF");
        expLines.add("      *\\d+ " + TS + " META-INF/A[.]DSA");
        if (firstAddedFilename != null) {
            expLines.add("smk   *\\d+ " + TS + " " + firstAddedFilename);
        }
        if (secondAddedFilename != null) {
            expLines.add("smkX  *\\d+ " + TS + " " + secondAddedFilename);
        }
        return expLines.toArray(new String[expLines.size()]);
    }

    void assertMatchByLines(String[] actLines, String[] expLines) {
        for (int i = 0; i < actLines.length && i < expLines.length; i++) {
            String actLine = actLines[i];
            String expLine = expLines[i];
            assertTrue(actLine.matches("^" + expLine + "$"),
                "\"" + actLine + "\" should have matched \"" + expLine + "\"");
        }
        assertEquals(actLines.length, expLines.length);
    }

    String test(String name, Function<Manifest, byte[]> mm) throws Exception {
        return test(name, FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,
                mm);
    }

    String test(String name,
            String firstAddedFilename, String secondAddedFilename,
            Function<Manifest, byte[]> mm) throws Exception {
        return test(name, firstAddedFilename, secondAddedFilename, mm, null,
                true, true);
    }

    /**
     * Essentially, creates a first signed JAR file with a single contained
     * file or without and a manipulation applied to its manifest signed by
     * signer a and then signes it again with a different signer b.
     * The jar file is signed twice with signer a in order to make the digests
     * available to the manipulation function that might use it.
     *
     * @param name Prefix for the JAR filenames used throughout the test.
     * @param firstAddedFilename Name of a file to add before the first
     * signature by signer a or null. The name will also become the contents
     * if not null.
     * @param secondAddedFilename Name of a file to add after the first
     * signature by signer a and before the second signature by signer b or
     * null. The name will also become the contents if not null.
     * @param manifestManipulation A callback hook to manipulate the manifest
     * after the first signature by signer a and before the second signature by
     * signer b.
     * @param digestalg The digest algorithm name to be used or null for
     * default.
     * @param assertMainAttrsDigestsUnchanged Assert that the
     * manifest main attributes digests have not changed. In any case the test
     * also checks that the digests are still valid whether changed or not
     * by {@code jarsigner -verify} which might use
     * {@link ManifestDigester.Entry#digestWorkaround}
     * @param assertFirstAddedFileDigestsUnchanged Assert that the
     * digest of the file firstAddedFilename has not changed with the second
     * signature. In any case the test checks that the digests are valid whether
     * changed or not by {@code jarsigner -verify} which might use
     * {@link ManifestDigester.Entry#digestWorkaround}
     * @return The name of the resulting JAR file that has passed the common
     * assertions ready for further examination
     */
    String test(String name,
            String firstAddedFilename, String secondAddedFilename,
            Function<Manifest, byte[]> 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(
                        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 + "-Digest") : DEF_DIGEST_STR) +
                        "-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<ol>
     * <li>create two self-signed certificates</li>
     * <li>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</li>
     * <li>add a new file to the jar</li>
     * <li>sign the jar with a JDK 11, 12, or 13 with bug 8217375 not yet
     * resolved with a different signer</li>
     * </ol>&rarr; 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" +
                DEF_DIGEST_STR + ": " +
                m.getAttributes(FILENAME_INITIAL_CONTENTS)
                        .getValue(DEF_DIGEST_STR) + "\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<ol>
     * <li>create two self-signed certificates</li>
     * <li>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</li>
     * <li>add a new file to the jar</li>
     * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
     * </ol>&rarr; 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(DEF_DIGEST_STR);
            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" +
                DEF_DIGEST_STR + ": " + 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<ol>
     * <li>create two self-signed certificates</li>
     * <li>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-384, long file names, or manual editing)</li>
     * <li>add a new file to the jar</li>
     * <li>sign the jar with a JDK 11 or 12 with a different signer</li>
     * </ol><p>&rarr;
     * 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<ol>
     * <li>create two self-signed certificates</li>
     * <li>sign a jar with at least one non-META-INF file in it</li>
     * <li>extract the manifest, and change its line delimiters
     * (for example dos2unix)</li>
     * <li>update the jar with the updated manifest</li>
     * <li>sign it again with the same signer as before</li>
     * <li>add a new file to the jar</li>
     * <li>sign the jar with a JDK before 13 with a different signer<li>
     * </ol><p>&rarr;
     * 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<Manifest, byte[]> 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<Manifest,Object> 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(DEF_DIGEST_STR + "-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(DEF_DIGEST_STR)));
    }

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

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

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

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

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

    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<Integer> byteArrayToIntList(byte[] bytes) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < bytes.length; i++) {
            list.add((int) bytes[i]);
        }
        return list;
    }

}