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