Path: blob/master/test/jdk/sun/security/tools/jarsigner/PreserveRawManifestEntryAndDigest.java
41152 views
/*1* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223import java.io.ByteArrayOutputStream;24import java.io.File;25import java.io.FilterOutputStream;26import java.io.IOException;27import java.io.InputStream;28import java.io.OutputStream;29import java.io.PrintStream;30import java.nio.file.Files;31import java.nio.file.Path;32import java.util.ArrayList;33import java.util.Arrays;34import java.util.List;35import java.util.Map;36import java.util.Collections;37import java.util.stream.Collectors;38import java.util.function.Function;39import java.util.jar.Attributes;40import java.util.jar.Attributes.Name;41import java.util.jar.Manifest;42import java.util.jar.JarEntry;43import java.util.jar.JarFile;44import java.util.zip.ZipFile;45import java.util.zip.ZipEntry;46import jdk.test.lib.process.OutputAnalyzer;47import jdk.test.lib.Platform;48import jdk.test.lib.SecurityTools;49import jdk.test.lib.util.JarUtils;50import org.testng.annotations.BeforeTest;51import org.testng.annotations.DataProvider;52import org.testng.annotations.Test;5354import static java.nio.charset.StandardCharsets.UTF_8;55import static org.testng.Assert.*;5657/**58* @test59* @bug 821737560* @library /test/lib61* @modules jdk.jartool/sun.security.tools.jarsigner62* @run testng/timeout=1200 PreserveRawManifestEntryAndDigest63* @summary Verifies that JarSigner does not change manifest file entries64* in a binary view if its decoded map view does not change so that an65* unchanged (individual section) entry continues to produce the same digest.66* The same manifest (in terms of {@link Manifest#equals}) could be encoded67* with different line breaks ("{@code \r}", "{@code \n}", or "{@code \r\n}")68* or with arbitrary line break positions (as is also the case with the change69* of the default line width in JDK 11, bug 6372077) resulting in a different70* digest for manifest entries with identical values.71*72* <p>See also:<ul>73* <li>{@code oldsig.sh} and {@code diffend.sh} in74* {@code /test/jdk/sun/security/tools/jarsigner/}</li>75* <li>{@code Compatibility.java} in76* {@code /test/jdk/sun/security/tools/jarsigner/compatibility}</li>77* <li>{@link ReproduceRaw} testing relevant78* {@sun.security.util.ManifestDigester} api in much more detail</li>79* </ul>80*/81/*82* debug with "run testng" += "/othervm -Djava.security.debug=jar"83*/84public class PreserveRawManifestEntryAndDigest {8586static final String KEYSTORE_FILENAME = "test.jks";87static final String FILENAME_INITIAL_CONTENTS = "initial-contents";88static final String FILENAME_UPDATED_CONTENTS = "updated-contents";8990/**91* @see sun.security.tools.jarsigner.Main#run92*/93static final int NOTSIGNEDBYALIASORALIASNOTINSTORE = 32;9495@BeforeTest96public void prepareContentFiles() throws IOException {97Files.write(Path.of(FILENAME_INITIAL_CONTENTS),98FILENAME_INITIAL_CONTENTS.getBytes(UTF_8));99Files.write(Path.of(FILENAME_UPDATED_CONTENTS),100FILENAME_UPDATED_CONTENTS.getBytes(UTF_8));101}102103@BeforeTest104public void prepareCertificates() throws Exception {105SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "106+ KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"107+ " -alias a -dname CN=A").shouldHaveExitValue(0);108SecurityTools.keytool("-genkeypair -keyalg DSA -keystore "109+ KEYSTORE_FILENAME + " -storepass changeit -keypass changeit"110+ " -alias b -dname CN=B").shouldHaveExitValue(0);111}112113static class TeeOutputStream extends FilterOutputStream {114final OutputStream tee; // don't flush or close115116public TeeOutputStream(OutputStream out, OutputStream tee) {117super(out);118this.tee = tee;119}120121@Override122public void write(int b) throws IOException {123super.write(b);124tee.write(b);125}126}127128/**129* runs jarsigner in its own child process and captures exit code and the130* output of stdout and stderr, as opposed to {@link #karsignerMain}131*/132OutputAnalyzer jarsignerProc(String args) throws Exception {133long start = System.currentTimeMillis();134try {135return SecurityTools.jarsigner(args);136} finally {137long end = System.currentTimeMillis();138System.out.println("jarsignerProc duration [ms]: " + (end - start));139}140}141142/**143* assume non-zero exit code would call System.exit but is faster than144* {@link #jarsignerProc}145*/146void jarsignerMain(String args) throws Exception {147long start = System.currentTimeMillis();148try {149new sun.security.tools.jarsigner.Main().run(args.split("\\s+"));150} finally {151long end = System.currentTimeMillis();152System.out.println("jarsignerMain duration [ms]: " + (end - start));153}154}155156void createSignedJarA(String jarFilename, Manifest manifest,157String additionalJarsignerOptions, String dummyContentsFilename)158throws Exception {159JarUtils.createJarFile(Path.of(jarFilename), manifest, Path.of("."),160dummyContentsFilename == null ? new Path[]{} :161new Path[] { Path.of(dummyContentsFilename) });162jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"163+ (additionalJarsignerOptions == null ? "" :164" " + additionalJarsignerOptions) +165" -verbose -debug " + jarFilename + " a");166Utils.echoManifest(Utils.readJarManifestBytes(167jarFilename), "original signed jar by signer a");168// check assumption that jar is valid at this point169jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME +170" -storepass changeit -verbose -debug " + jarFilename + " a");171}172173void manipulateManifestSignAgainA(String srcJarFilename, String tmpFilename,174String dstJarFilename, String additionalJarsignerOptions,175Function<Manifest, byte[]> manifestManipulation) throws Exception {176Manifest mf;177try (JarFile jar = new JarFile(srcJarFilename)) {178mf = jar.getManifest();179}180byte[] manipulatedManifest = manifestManipulation.apply(mf);181Utils.echoManifest(manipulatedManifest, "manipulated manifest");182JarUtils.updateJar(srcJarFilename, tmpFilename, Map.of(183JarFile.MANIFEST_NAME, manipulatedManifest,184// add a fake sig-related file to trigger wasSigned in JarSigner185"META-INF/.SF", Name.SIGNATURE_VERSION + ": 1.0\r\n"));186jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"187+ (additionalJarsignerOptions == null ? "" :188" " + additionalJarsignerOptions) +189" -verbose -debug " + tmpFilename + " a");190// remove META-INF/.SF from signed jar again which would not validate191JarUtils.updateJar(tmpFilename, dstJarFilename,192Map.of("META-INF/.SF", false));193194Utils.echoManifest(Utils.readJarManifestBytes(195dstJarFilename), "manipulated jar signed again with a");196// check assumption that jar is valid at this point197jarsignerMain("-verify -keystore " + KEYSTORE_FILENAME + " " +198"-storepass changeit -verbose -debug " + dstJarFilename + " a");199}200201OutputAnalyzer signB(String jarFilename, String additionalJarsignerOptions,202int updateExitCodeVerifyA) throws Exception {203jarsignerMain("-keystore " + KEYSTORE_FILENAME + " -storepass changeit"204+ (additionalJarsignerOptions == null ? "" :205" " + additionalJarsignerOptions)206+ " -verbose -debug " + jarFilename + " b");207Utils.echoManifest(Utils.readJarManifestBytes(208jarFilename), "signed again with signer b");209// check assumption that jar is valid at this point with any alias210jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +211" -storepass changeit -debug -verbose " + jarFilename);212// check assumption that jar is valid at this point with b just signed213jarsignerMain("-verify -strict -keystore " + KEYSTORE_FILENAME +214" -storepass changeit -debug -verbose " + jarFilename + " b");215// return result of verification of signature by a before update216return jarsignerProc("-verify -strict " + "-keystore " +217KEYSTORE_FILENAME + " -storepass changeit " + "-debug " +218"-verbose " + jarFilename + " a")219.shouldHaveExitValue(updateExitCodeVerifyA);220}221222String[] fromFirstToSecondEmptyLine(String[] lines) {223int from = 0;224for (int i = 0; i < lines.length; i++) {225if ("".equals(lines[i])) {226from = i + 1;227break;228}229}230231int to = lines.length - 1;232for (int i = from; i < lines.length; i++) {233if ("".equals(lines[i])) {234to = i - 1;235break;236}237}238239return Arrays.copyOfRange(lines, from, to + 1);240}241242/**243* @see "concise_jarsigner.sh"244*/245String[] getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(246String jarFilename, String digestalg,247String firstAddedFilename, String secondAddedFilename) {248final String TS = ".{28,29}"; // matches a timestamp249List<String> expLines = new ArrayList<>();250expLines.add("s k *\\d+ " + TS + " META-INF/MANIFEST[.]MF");251expLines.add(" *\\d+ " + TS + " META-INF/B[.]SF");252expLines.add(" *\\d+ " + TS + " META-INF/B[.]DSA");253expLines.add(" *\\d+ " + TS + " META-INF/A[.]SF");254expLines.add(" *\\d+ " + TS + " META-INF/A[.]DSA");255if (firstAddedFilename != null) {256expLines.add("smk *\\d+ " + TS + " " + firstAddedFilename);257}258if (secondAddedFilename != null) {259expLines.add("smkX *\\d+ " + TS + " " + secondAddedFilename);260}261return expLines.toArray(new String[expLines.size()]);262}263264void assertMatchByLines(String[] actLines, String[] expLines) {265for (int i = 0; i < actLines.length && i < expLines.length; i++) {266String actLine = actLines[i];267String expLine = expLines[i];268assertTrue(actLine.matches("^" + expLine + "$"),269"\"" + actLine + "\" should have matched \"" + expLine + "\"");270}271assertEquals(actLines.length, expLines.length);272}273274String test(String name, Function<Manifest, byte[]> mm) throws Exception {275return test(name, FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,276mm);277}278279String test(String name,280String firstAddedFilename, String secondAddedFilename,281Function<Manifest, byte[]> mm) throws Exception {282return test(name, firstAddedFilename, secondAddedFilename, mm, null,283true, true);284}285286/**287* Essentially, creates a first signed JAR file with a single contained288* file or without and a manipulation applied to its manifest signed by289* signer a and then signes it again with a different signer b.290* The jar file is signed twice with signer a in order to make the digests291* available to the manipulation function that might use it.292*293* @param name Prefix for the JAR filenames used throughout the test.294* @param firstAddedFilename Name of a file to add before the first295* signature by signer a or null. The name will also become the contents296* if not null.297* @param secondAddedFilename Name of a file to add after the first298* signature by signer a and before the second signature by signer b or299* null. The name will also become the contents if not null.300* @param manifestManipulation A callback hook to manipulate the manifest301* after the first signature by signer a and before the second signature by302* signer b.303* @param digestalg The digest algorithm name to be used or null for304* default.305* @param assertMainAttrsDigestsUnchanged Assert that the306* manifest main attributes digests have not changed. In any case the test307* also checks that the digests are still valid whether changed or not308* by {@code jarsigner -verify} which might use309* {@link ManifestDigester.Entry#digestWorkaround}310* @param assertFirstAddedFileDigestsUnchanged Assert that the311* digest of the file firstAddedFilename has not changed with the second312* signature. In any case the test checks that the digests are valid whether313* changed or not by {@code jarsigner -verify} which might use314* {@link ManifestDigester.Entry#digestWorkaround}315* @return The name of the resulting JAR file that has passed the common316* assertions ready for further examination317*/318String test(String name,319String firstAddedFilename, String secondAddedFilename,320Function<Manifest, byte[]> manifestManipulation,321String digestalg, boolean assertMainAttrsDigestsUnchanged,322boolean assertFirstAddedFileDigestsUnchanged)323throws Exception {324String digOpts = (digestalg != null ? "-digestalg " + digestalg : "");325String jarFilename1 = "test-" + name + "-step1.jar";326createSignedJarA(jarFilename1,327/* no manifest will let jarsigner create a default one */ null,328digOpts, firstAddedFilename);329330// manipulate the manifest, write it back, and sign the jar again with331// the same certificate a as before overwriting the first signature332String jarFilename2 = "test-" + name + "-step2.jar";333String jarFilename3 = "test-" + name + "-step3.jar";334manipulateManifestSignAgainA(jarFilename1, jarFilename2, jarFilename3,335digOpts, manifestManipulation);336337// add another file, sign it with the other certificate, and verify it338String jarFilename4 = "test-" + name + "-step4.jar";339JarUtils.updateJar(jarFilename3, jarFilename4,340secondAddedFilename != null ?341Map.of(secondAddedFilename, secondAddedFilename)342: Collections.EMPTY_MAP);343OutputAnalyzer o = signB(jarFilename4, digOpts,344secondAddedFilename != null ? NOTSIGNEDBYALIASORALIASNOTINSTORE : 0);345// check that secondAddedFilename is the only entry which is not signed346// by signer with alias "a" unless secondAddedFilename is null347assertMatchByLines(348fromFirstToSecondEmptyLine(o.getStdout().split("\\R")),349getExpectedJarSignerOutputUpdatedContentNotValidatedBySignerA(350jarFilename4, digestalg,351firstAddedFilename, secondAddedFilename));352353// double-check reading the files with a verifying JarFile354try (JarFile jar = new JarFile(jarFilename4, true)) {355if (firstAddedFilename != null) {356JarEntry je1 = jar.getJarEntry(firstAddedFilename);357jar.getInputStream(je1).readAllBytes();358assertTrue(je1.getCodeSigners().length > 0);359}360if (secondAddedFilename != null) {361JarEntry je2 = jar.getJarEntry(secondAddedFilename);362jar.getInputStream(je2).readAllBytes();363assertTrue(je2.getCodeSigners().length > 0);364}365}366367// assert that the signature of firstAddedFilename signed by signer368// with alias "a" is not lost and its digest remains the same369try (ZipFile zip = new ZipFile(jarFilename4)) {370ZipEntry ea = zip.getEntry("META-INF/A.SF");371Manifest sfa = new Manifest(zip.getInputStream(ea));372ZipEntry eb = zip.getEntry("META-INF/B.SF");373Manifest sfb = new Manifest(zip.getInputStream(eb));374if (assertMainAttrsDigestsUnchanged) {375String mainAttrsDigKey =376(digestalg != null ? digestalg : "SHA-256") +377"-Digest-Manifest-Main-Attributes";378assertEquals(sfa.getMainAttributes().getValue(mainAttrsDigKey),379sfb.getMainAttributes().getValue(mainAttrsDigKey));380}381if (assertFirstAddedFileDigestsUnchanged) {382assertEquals(sfa.getAttributes(firstAddedFilename),383sfb.getAttributes(firstAddedFilename));384}385}386387return jarFilename4;388}389390/**391* Test that signing a jar with manifest entries with arbitrary line break392* positions in individual section headers does not destroy an existing393* signature<ol>394* <li>create two self-signed certificates</li>395* <li>sign a jar with at least one non-META-INF file in it with a JDK396* before 11 or place line breaks not at 72 bytes in an individual section397* header</li>398* <li>add a new file to the jar</li>399* <li>sign the jar with a JDK 11, 12, or 13 with bug 8217375 not yet400* resolved with a different signer</li>401* </ol>→ first signature will not validate anymore even though it402* should.403*/404@Test405public void arbitraryLineBreaksSectionName() throws Exception {406test("arbitraryLineBreaksSectionName", m -> {407return (408Name.MANIFEST_VERSION + ": 1.0\r\n" +409"Created-By: " +410m.getMainAttributes().getValue("Created-By") + "\r\n" +411"\r\n" +412"Name: Test\r\n" +413" -\r\n" +414" Section\r\n" +415"Key: Value \r\n" +416"\r\n" +417"Name: " + FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n" +418" " + FILENAME_INITIAL_CONTENTS.substring(1, 8) + "\r\n" +419" " + FILENAME_INITIAL_CONTENTS.substring(8) + "\r\n" +420"SHA-256-Digest: " + m.getAttributes(FILENAME_INITIAL_CONTENTS)421.getValue("SHA-256-Digest") + "\r\n" +422"\r\n"423).getBytes(UTF_8);424});425}426427/**428* Test that signing a jar with manifest entries with arbitrary line break429* positions in individual section headers does not destroy an existing430* signature<ol>431* <li>create two self-signed certificates</li>432* <li>sign a jar with at least one non-META-INF file in it with a JDK433* before 11 or place line breaks not at 72 bytes in an individual section434* header</li>435* <li>add a new file to the jar</li>436* <li>sign the jar with a JDK 11 or 12 with a different signer</li>437* </ol>→ first signature will not validate anymore even though it438* should.439*/440@Test441public void arbitraryLineBreaksHeader() throws Exception {442test("arbitraryLineBreaksHeader", m -> {443String digest = m.getAttributes(FILENAME_INITIAL_CONTENTS)444.getValue("SHA-256-Digest");445return (446Name.MANIFEST_VERSION + ": 1.0\r\n" +447"Created-By: " +448m.getMainAttributes().getValue("Created-By") + "\r\n" +449"\r\n" +450"Name: Test-Section\r\n" +451"Key: Value \r\n" +452" with\r\n" +453" strange \r\n" +454" line breaks.\r\n" +455"\r\n" +456"Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +457"SHA-256-Digest: " + digest.substring(0, 11) + "\r\n" +458" " + digest.substring(11) + "\r\n" +459"\r\n"460).getBytes(UTF_8);461});462}463464/**465* Breaks {@code line} at 70 bytes even though the name says 72 but when466* also counting the line delimiter ("{@code \r\n}") the line totals to 72467* bytes.468* Borrowed from {@link Manifest#make72Safe} before JDK 11469*470* @see Manifest#make72Safe471*/472static void make72Safe(StringBuffer line) {473int length = line.length();474if (length > 72) {475int index = 70;476while (index < length - 2) {477line.insert(index, "\r\n ");478index += 72;479length += 3;480}481}482return;483}484485/**486* Test that signing a jar with manifest entries with line breaks at487* position where Manifest would not place them now anymore (72 instead of488* 70 bytes after line starts) does not destroy an existing signature<ol>489* <li>create two self-signed certificates</li>490* <li>simulate a manifest as it would have been written by a JDK before 11491* by re-positioning line breaks at 70 bytes (which makes a difference by492* digests that grow headers longer than 70 characters such as SHA-512 as493* opposed to default SHA-256, long file names, or manual editing)</li>494* <li>add a new file to the jar</li>495* <li>sign the jar with a JDK 11 or 12 with a different signer</li>496* </ol><p>→497* The first signature will not validate anymore even though it should.498*/499public void lineWidth70(String name, String digestalg) throws Exception {500Files.write(Path.of(name), name.getBytes(UTF_8));501test(name, name, FILENAME_UPDATED_CONTENTS, m -> {502// force a line break with a header exceeding line width limit503m.getEntries().put("Test-Section", new Attributes());504m.getAttributes("Test-Section").put(505Name.IMPLEMENTATION_VERSION, "1" + "0".repeat(100));506507StringBuilder sb = new StringBuilder();508StringBuffer[] buf = new StringBuffer[] { null };509manifestToString(m).lines().forEach(line -> {510if (line.startsWith(" ")) {511buf[0].append(line.substring(1));512} else {513if (buf[0] != null) {514make72Safe(buf[0]);515sb.append(buf[0].toString());516sb.append("\r\n");517}518buf[0] = new StringBuffer();519buf[0].append(line);520}521});522make72Safe(buf[0]);523sb.append(buf[0].toString());524sb.append("\r\n");525return sb.toString().getBytes(UTF_8);526}, digestalg, false, false);527}528529@Test530public void lineWidth70Filename() throws Exception {531lineWidth70(532"lineWidth70".repeat(6) /* 73 chars total with "Name: " */, null);533}534535@Test536public void lineWidth70Digest() throws Exception {537lineWidth70("lineWidth70digest", "SHA-512");538}539540/**541* Test that signing a jar with a manifest with line delimiter other than542* "{@code \r\n}" does not destroy an existing signature<ol>543* <li>create two self-signed certificates</li>544* <li>sign a jar with at least one non-META-INF file in it</li>545* <li>extract the manifest, and change its line delimiters546* (for example dos2unix)</li>547* <li>update the jar with the updated manifest</li>548* <li>sign it again with the same signer as before</li>549* <li>add a new file to the jar</li>550* <li>sign the jar with a JDK before 13 with a different signer<li>551* </ol><p>→552* The first signature will not validate anymore even though it should.553*/554public void lineBreak(String lineBreak) throws Exception {555test("lineBreak" + byteArrayToIntList(lineBreak.getBytes(UTF_8)).stream556().map(i -> "" + i).collect(Collectors.joining("")), m -> {557StringBuilder sb = new StringBuilder();558manifestToString(m).lines().forEach(l -> {559sb.append(l);560sb.append(lineBreak);561});562return sb.toString().getBytes(UTF_8);563});564}565566@Test567public void lineBreakCr() throws Exception {568lineBreak("\r");569}570571@Test572public void lineBreakLf() throws Exception {573lineBreak("\n");574}575576@Test577public void lineBreakCrLf() throws Exception {578lineBreak("\r\n");579}580581@Test582public void testAdjacentRepeatedSection() throws Exception {583test("adjacent", m -> {584return (manifestToString(m) +585"Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +586"Foo: Bar\r\n" +587"\r\n"588).getBytes(UTF_8);589});590}591592@Test593public void testIntermittentRepeatedSection() throws Exception {594test("intermittent", m -> {595return (manifestToString(m) +596"Name: don't know\r\n" +597"Foo: Bar\r\n" +598"\r\n" +599"Name: " + FILENAME_INITIAL_CONTENTS + "\r\n" +600"Foo: Bar\r\n" +601"\r\n"602).getBytes(UTF_8);603});604}605606@Test607public void testNameImmediatelyContinued() throws Exception {608test("testNameImmediatelyContinued", m -> {609// places a continuation line break and space at the first allowed610// position after ": " and before the first character of the value611return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,612"\r\n " + FILENAME_INITIAL_CONTENTS + "\r\nFoo: Bar")613).getBytes(UTF_8);614});615}616617/*618* "malicious" '\r' after continuation line continued619*/620@Test621public void testNameContinuedContinuedWithCr() throws Exception {622test("testNameContinuedContinuedWithCr", m -> {623return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,624FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +625FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r " +626FILENAME_INITIAL_CONTENTS.substring(4) + "\r\n" +627"Foo: Bar")628).getBytes(UTF_8);629});630}631632/*633* "malicious" '\r' after continued continuation line634*/635@Test636public void testNameContinuedContinuedEndingWithCr() throws Exception {637test("testNameContinuedContinuedEndingWithCr", m -> {638return (manifestToString(m).replaceAll(FILENAME_INITIAL_CONTENTS,639FILENAME_INITIAL_CONTENTS.substring(0, 1) + "\r\n " +640FILENAME_INITIAL_CONTENTS.substring(1, 4) + "\r\n " +641FILENAME_INITIAL_CONTENTS.substring(4) + "\r" + // no '\n'642"Foo: Bar")643).getBytes(UTF_8);644});645}646647@DataProvider(name = "trailingSeqParams", parallel = true)648public static Object[][] trailingSeqParams() {649return new Object[][] {650{""},651{"\r"},652{"\n"},653{"\r\n"},654{"\r\r"},655{"\n\n"},656{"\n\r"},657{"\r\r\r"},658{"\r\r\n"},659{"\r\n\r"},660{"\r\n\n"},661{"\n\r\r"},662{"\n\r\n"},663{"\n\n\r"},664{"\n\n\n"},665{"\r\r\r\n"},666{"\r\r\n\r"},667{"\r\r\n\n"},668{"\r\n\r\r"},669{"\r\n\r\n"},670{"\r\n\n\r"},671{"\r\n\n\n"},672{"\n\r\r\n"},673{"\n\r\n\r"},674{"\n\r\n\n"},675{"\n\n\r\n"},676{"\r\r\n\r\n"},677{"\r\n\r\r\n"},678{"\r\n\r\n\r"},679{"\r\n\r\n\n"},680{"\r\n\n\r\n"},681{"\n\r\n\r\n"},682{"\r\n\r\n\r\n"},683{"\r\n\r\n\r\n\r\n"}684};685}686687boolean isSufficientSectionDelimiter(String trailingSeq) {688if (trailingSeq.length() < 2) return false;689if (trailingSeq.startsWith("\r\n")) {690trailingSeq = trailingSeq.substring(2);691} else if (trailingSeq.startsWith("\r") ||692trailingSeq.startsWith("\n")) {693trailingSeq = trailingSeq.substring(1);694} else {695return false;696}697if (trailingSeq.startsWith("\r\n")) {698return true;699} else if (trailingSeq.startsWith("\r") ||700trailingSeq.startsWith("\n")) {701return true;702}703return false;704}705706Function<Manifest, byte[]> replaceTrailingLineBreaksManipulation(707String trailingSeq) {708return m -> {709StringBuilder sb = new StringBuilder(manifestToString(m));710// cut off default trailing line break characters711while ("\r\n".contains(sb.substring(sb.length() - 1))) {712sb.deleteCharAt(sb.length() - 1);713}714// and instead add another trailing sequence715sb.append(trailingSeq);716return sb.toString().getBytes(UTF_8);717};718}719720boolean abSigFilesEqual(String jarFilename,721Function<Manifest,Object> getter) throws IOException {722try (ZipFile zip = new ZipFile(jarFilename)) {723ZipEntry ea = zip.getEntry("META-INF/A.SF");724Manifest sfa = new Manifest(zip.getInputStream(ea));725ZipEntry eb = zip.getEntry("META-INF/B.SF");726Manifest sfb = new Manifest(zip.getInputStream(eb));727return getter.apply(sfa).equals(getter.apply(sfb));728}729}730731/**732* Create a signed JAR file with a strange sequence of line breaks after733* the main attributes and no individual section and hence no file contained734* within the JAR file in order not to produce an individual section,735* then add no other file and sign it with a different signer.736* The manifest is not expected to be changed during the second signature.737*/738@Test(dataProvider = "trailingSeqParams")739public void emptyJarTrailingSeq(String trailingSeq) throws Exception {740String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(741UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));742System.out.println("trailingSeq = " + trailingSeqEscaped);743if (trailingSeq.isEmpty()) {744return; // invalid manifest without trailing line break745}746747test("emptyJarTrailingSeq" + trailingSeqEscaped, null, null,748replaceTrailingLineBreaksManipulation(trailingSeq));749750// test called above already asserts by default that the main attributes751// digests have not changed.752}753754/**755* Create a signed JAR file with a strange sequence of line breaks after756* the main attributes and no individual section and hence no file contained757* within the JAR file in order not to produce an individual section,758* then add another file and sign it with a different signer so that the759* originally trailing sequence after the main attributes might have to be760* completed to a full section delimiter or reproduced only partially761* before the new individual section with the added file digest can be762* appended. The main attributes digests are expected to change if the763* first signed trailing sequence did not contain a blank line and are not764* expected to change if superfluous parts of the trailing sequence were765* not reproduced. All digests are expected to validate either with digest766* or with digestWorkaround.767*/768@Test(dataProvider = "trailingSeqParams")769public void emptyJarTrailingSeqAddFile(String trailingSeq) throws Exception{770String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(771UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));772System.out.println("trailingSeq = " + trailingSeqEscaped);773if (!isSufficientSectionDelimiter(trailingSeq)) {774return; // invalid manifest without trailing blank line775}776boolean expectUnchangedDigests =777isSufficientSectionDelimiter(trailingSeq);778System.out.println("expectUnchangedDigests = " + expectUnchangedDigests);779String jarFilename = test("emptyJarTrailingSeqAddFile" +780trailingSeqEscaped, null, FILENAME_UPDATED_CONTENTS,781replaceTrailingLineBreaksManipulation(trailingSeq),782null, expectUnchangedDigests, false);783784// Check that the digests have changed only if another line break had785// to be added before a new individual section. That both also are valid786// with either digest or digestWorkaround has been checked by test787// before.788assertEquals(abSigFilesEqual(jarFilename, sf -> sf.getMainAttributes()789.getValue("SHA-256-Digest-Manifest-Main-Attributes")),790expectUnchangedDigests);791}792793/**794* Create a signed JAR file with a strange sequence of line breaks after795* the only individual section holding the digest of the only file contained796* within the JAR file,797* then add no other file and sign it with a different signer.798* The manifest is expected to be changed during the second signature only799* by removing superfluous line break characters which are not digested800* and the manifest entry digest is expected not to change.801* The individual section is expected to be reproduced without additional802* line breaks even if the trailing sequence does not properly delimit803* another section.804*/805@Test(dataProvider = "trailingSeqParams")806public void singleIndividualSectionTrailingSeq(String trailingSeq)807throws Exception {808String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(809UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));810System.out.println("trailingSeq = " + trailingSeqEscaped);811if (trailingSeq.isEmpty()) {812return; // invalid manifest without trailing line break813}814String jarFilename = test("singleIndividualSectionTrailingSeq"815+ trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null,816replaceTrailingLineBreaksManipulation(trailingSeq));817818assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(819FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));820}821822/**823* Create a signed JAR file with a strange sequence of line breaks after824* the first individual section holding the digest of the only file825* contained within the JAR file and a second individual section with the826* same name to be both digested into the same entry digest,827* then add no other file and sign it with a different signer.828* The manifest is expected to be changed during the second signature829* by removing superfluous line break characters which are not digested830* anyway or if the trailingSeq is not a sufficient delimiter that both831* intially provided sections are treated as only one which is maybe not832* perfect but does at least not result in an invalid signed jar file.833*/834@Test(dataProvider = "trailingSeqParams")835public void firstIndividualSectionTrailingSeq(String trailingSeq)836throws Exception {837String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(838UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));839System.out.println("trailingSeq = " + trailingSeqEscaped);840String jarFilename;841jarFilename = test("firstIndividualSectionTrailingSeq"842+ trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {843StringBuilder sb = new StringBuilder(manifestToString(m));844// cut off default trailing line break characters845while ("\r\n".contains(sb.substring(sb.length() - 1))) {846sb.deleteCharAt(sb.length() - 1);847}848// and instead add another trailing sequence849sb.append(trailingSeq);850// now add another section with the same name assuming sb851// already contains one entry for FILENAME_INITIAL_CONTENTS852sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");853sb.append("Foo: Bar\r\n");854sb.append("\r\n");855return sb.toString().getBytes(UTF_8);856});857858assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(859FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));860}861862/**863* Create a signed JAR file with two individual sections for the same864* contained file (corresponding by name) the first of which properly865* delimited and the second of which followed by a strange sequence of866* line breaks both digested into the same entry digest,867* then add no other file and sign it with a different signer.868* The manifest is expected to be changed during the second signature869* by removing superfluous line break characters which are not digested870* anyway.871*/872@Test(dataProvider = "trailingSeqParams")873public void secondIndividualSectionTrailingSeq(String trailingSeq)874throws Exception {875String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(876UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));877System.out.println("trailingSeq = " + trailingSeqEscaped);878String jarFilename = test("secondIndividualSectionTrailingSeq" +879trailingSeqEscaped, FILENAME_INITIAL_CONTENTS, null, m -> {880StringBuilder sb = new StringBuilder(manifestToString(m));881sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");882sb.append("Foo: Bar");883sb.append(trailingSeq);884return sb.toString().getBytes(UTF_8);885});886887assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(888FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));889}890891/**892* Create a signed JAR file with a strange sequence of line breaks after893* the only individual section holding the digest of the only file contained894* within the JAR file,895* then add another file and sign it with a different signer.896* The manifest is expected to be changed during the second signature by897* removing superfluous line break characters which are not digested898* anyway or adding another line break to complete to a proper section899* delimiter blank line.900* The first file entry digest is expected to change only if another901* line break has been added.902*/903@Test(dataProvider = "trailingSeqParams")904public void singleIndividualSectionTrailingSeqAddFile(String trailingSeq)905throws Exception {906String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(907UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));908System.out.println("trailingSeq = " + trailingSeqEscaped);909if (!isSufficientSectionDelimiter(trailingSeq)) {910return; // invalid manifest without trailing blank line911}912String jarFilename = test("singleIndividualSectionTrailingSeqAddFile"913+ trailingSeqEscaped,914FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS,915replaceTrailingLineBreaksManipulation(trailingSeq),916null, true, true);917918assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(919FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));920}921922/**923* Create a signed JAR file with a strange sequence of line breaks after924* the first individual section holding the digest of the only file925* contained within the JAR file and a second individual section with the926* same name to be both digested into the same entry digest,927* then add another file and sign it with a different signer.928* The manifest is expected to be changed during the second signature929* by removing superfluous line break characters which are not digested930* anyway or if the trailingSeq is not a sufficient delimiter that both931* intially provided sections are treated as only one which is maybe not932* perfect but does at least not result in an invalid signed jar file.933*/934@Test(dataProvider = "trailingSeqParams")935public void firstIndividualSectionTrailingSeqAddFile(String trailingSeq)936throws Exception {937String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(938UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));939System.out.println("trailingSeq = " + trailingSeqEscaped);940String jarFilename = test("firstIndividualSectionTrailingSeqAddFile"941+ trailingSeqEscaped,942FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {943StringBuilder sb = new StringBuilder(manifestToString(m));944// cut off default trailing line break characters945while ("\r\n".contains(sb.substring(sb.length() - 1))) {946sb.deleteCharAt(sb.length() - 1);947}948// and instead add another trailing sequence949sb.append(trailingSeq);950// now add another section with the same name assuming sb951// already contains one entry for FILENAME_INITIAL_CONTENTS952sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");953sb.append("Foo: Bar\r\n");954sb.append("\r\n");955return sb.toString().getBytes(UTF_8);956});957958assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(959FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));960}961962/**963* Create a signed JAR file with two individual sections for the same964* contained file (corresponding by name) the first of which properly965* delimited and the second of which followed by a strange sequence of966* line breaks both digested into the same entry digest,967* then add another file and sign it with a different signer.968* The manifest is expected to be changed during the second signature969* by removing superfluous line break characters which are not digested970* anyway or by adding a proper section delimiter.971* The digests are expected to be changed only if another line break is972* added to properly delimit the next section both digests of which are973* expected to validate with either digest or digestWorkaround.974*/975@Test(dataProvider = "trailingSeqParams")976public void secondIndividualSectionTrailingSeqAddFile(String trailingSeq)977throws Exception {978String trailingSeqEscaped = byteArrayToIntList(trailingSeq.getBytes(979UTF_8)).stream().map(i -> "" + i).collect(Collectors.joining(""));980System.out.println("trailingSeq = " + trailingSeqEscaped);981if (!isSufficientSectionDelimiter(trailingSeq)) {982return; // invalid manifest without trailing blank line983}984String jarFilename = test("secondIndividualSectionTrailingSeqAddFile" +985trailingSeqEscaped,986FILENAME_INITIAL_CONTENTS, FILENAME_UPDATED_CONTENTS, m -> {987StringBuilder sb = new StringBuilder(manifestToString(m));988sb.append("Name: " + FILENAME_INITIAL_CONTENTS + "\r\n");989sb.append("Foo: Bar");990sb.append(trailingSeq);991return sb.toString().getBytes(UTF_8);992}, null, true, true);993994assertTrue(abSigFilesEqual(jarFilename, sf -> sf.getAttributes(995FILENAME_INITIAL_CONTENTS).getValue("SHA-256-Digest")));996}997998String manifestToString(Manifest mf) {999try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {1000mf.write(out);1001return new String(out.toByteArray(), UTF_8);1002} catch (IOException e) {1003throw new RuntimeException(e);1004}1005}10061007static List<Integer> byteArrayToIntList(byte[] bytes) {1008List<Integer> list = new ArrayList<>();1009for (int i = 0; i < bytes.length; i++) {1010list.add((int) bytes[i]);1011}1012return list;1013}10141015}101610171018