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