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>→ 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>→ 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>→
* 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>→
* 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;
}
}
|