Skip to content

Commit 710baba

Browse files
Roman Marchenkognu-andrew
authored andcommitted
8309841: Jarsigner should print a warning if an entry is removed
Reviewed-by: abakhtin, andrew Backport-of: bdfb41f977258831e4b0ceaef5d016d095ab6e7f
1 parent 198aef4 commit 710baba

File tree

5 files changed

+242
-1
lines changed

5 files changed

+242
-1
lines changed

src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ public static void main(String args[]) throws Exception {
175175
private boolean hasExpiringCert = false;
176176
private boolean hasExpiringTsaCert = false;
177177
private boolean noTimestamp = true;
178+
private boolean hasNonexistentEntries = false;
178179

179180
// Expiration date. The value could be null if signed by a trusted cert.
180181
private Date expireDate = null;
@@ -695,6 +696,7 @@ void verifyJar(String jarName)
695696
Map<String,PKCS7> sigMap = new HashMap<>();
696697
Map<String,String> sigNameMap = new HashMap<>();
697698
Map<String,String> unparsableSignatures = new HashMap<>();
699+
Map<String,Set<String>> entriesInSF = new HashMap<>();
698700

699701
try {
700702
jf = new JarFile(jarName, true);
@@ -731,6 +733,7 @@ void verifyJar(String jarName)
731733
break;
732734
}
733735
}
736+
entriesInSF.put(alias, sf.getEntries().keySet());
734737
if (!found) {
735738
unparsableSignatures.putIfAbsent(alias,
736739
String.format(
@@ -829,6 +832,9 @@ void verifyJar(String jarName)
829832
sb.append('\n');
830833
}
831834
}
835+
for (var signed : entriesInSF.values()) {
836+
signed.remove(name);
837+
}
832838
} else if (showcerts && !verbose.equals("all")) {
833839
// Print no info for unsigned entries when -verbose:all,
834840
// to be consistent with old behavior.
@@ -1020,6 +1026,13 @@ void verifyJar(String jarName)
10201026
if (verbose != null) {
10211027
System.out.println(history);
10221028
}
1029+
var signed = entriesInSF.get(s);
1030+
if (!signed.isEmpty()) {
1031+
if (verbose != null) {
1032+
System.out.println(rb.getString("history.nonexistent.entries") + signed);
1033+
}
1034+
hasNonexistentEntries = true;
1035+
}
10231036
} else {
10241037
unparsableSignatures.putIfAbsent(s, String.format(
10251038
rb.getString("history.nobk"), s));
@@ -1243,6 +1256,7 @@ private void displayMessagesAndResult(boolean isSigning) {
12431256
(hasExpiringTsaCert && expireDate != null) ||
12441257
(noTimestamp && expireDate != null) ||
12451258
(hasExpiredTsaCert && signerNotExpired) ||
1259+
hasNonexistentEntries ||
12461260
extraAttrsDetected) {
12471261

12481262
if (hasExpiredTsaCert && signerNotExpired) {
@@ -1280,6 +1294,9 @@ private void displayMessagesAndResult(boolean isSigning) {
12801294
: "no.timestamp.verifying"), expireDate));
12811295
}
12821296
}
1297+
if (hasNonexistentEntries) {
1298+
warnings.add(rb.getString("nonexistent.entries.found"));
1299+
}
12831300
if (extraAttrsDetected) {
12841301
warnings.add(rb.getString("extra.attributes.detected"));
12851302
}

src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public class Resources extends java.util.ListResourceBundle {
164164

165165
{"history.with.ts", "- Signed by \"%1$s\"\n Digest algorithm: %2$s\n Signature algorithm: %3$s, %4$s\n Timestamped by \"%6$s\" on %5$tc\n Timestamp digest algorithm: %7$s\n Timestamp signature algorithm: %8$s, %9$s"},
166166
{"history.without.ts", "- Signed by \"%1$s\"\n Digest algorithm: %2$s\n Signature algorithm: %3$s, %4$s"},
167+
{"history.nonexistent.entries", " Warning: nonexistent signed entries: "},
167168
{"history.unparsable", "- Unparsable signature-related file %s"},
168169
{"history.nosf", "- Missing signature-related file META-INF/%s.SF"},
169170
{"history.nobk", "- Missing block file for signature-related file META-INF/%s.SF"},
@@ -174,6 +175,7 @@ public class Resources extends java.util.ListResourceBundle {
174175
{"key.bit.weak", "%d-bit key (weak)"},
175176
{"key.bit.disabled", "%d-bit key (disabled)"},
176177
{"unknown.size", "unknown size"},
178+
{"nonexistent.entries.found", "This jar contains signed entries for files that do not exist. See the -verbose output for more details."},
177179
{"extra.attributes.detected", "POSIX file permission and/or symlink attributes detected. These attributes are ignored when signing and are not protected by the signature."},
178180

179181
{"jarsigner.", "jarsigner: "},
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2024, 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+
/*
25+
* @test
26+
* @bug 8309841
27+
* @summary Jarsigner should print a warning if an entry is removed
28+
* @library /test/lib
29+
*/
30+
31+
import jdk.test.lib.SecurityTools;
32+
import jdk.test.lib.util.JarUtils;
33+
34+
import java.nio.file.Files;
35+
import java.nio.file.Path;
36+
import java.util.jar.Attributes;
37+
import java.util.jar.Manifest;
38+
39+
public class RemovedFiles {
40+
41+
private static final String NONEXISTENT_ENTRIES_FOUND
42+
= "This jar contains signed entries for files that do not exist. See the -verbose output for more details.";
43+
44+
public static void main(String[] args) throws Exception {
45+
JarUtils.createJarFile(
46+
Path.of("a.jar"),
47+
Path.of("."),
48+
Files.writeString(Path.of("a"), "a"),
49+
Files.writeString(Path.of("b"), "b"));
50+
SecurityTools.keytool("-genkeypair -storepass changeit -keystore ks -alias x -dname CN=x -keyalg RSA");
51+
SecurityTools.jarsigner("-storepass changeit -keystore ks a.jar x");
52+
53+
// All is fine at the beginning.
54+
SecurityTools.jarsigner("-verify a.jar")
55+
.shouldNotContain(NONEXISTENT_ENTRIES_FOUND);
56+
57+
// Remove an entry after signing. There will be a warning.
58+
JarUtils.deleteEntries(Path.of("a.jar"), "a");
59+
SecurityTools.jarsigner("-verify a.jar")
60+
.shouldContain(NONEXISTENT_ENTRIES_FOUND);
61+
SecurityTools.jarsigner("-verify -verbose a.jar")
62+
.shouldContain(NONEXISTENT_ENTRIES_FOUND)
63+
.shouldContain("Warning: nonexistent signed entries: [a]");
64+
65+
// Remove one more entry.
66+
JarUtils.deleteEntries(Path.of("a.jar"), "b");
67+
SecurityTools.jarsigner("-verify a.jar")
68+
.shouldContain(NONEXISTENT_ENTRIES_FOUND);
69+
SecurityTools.jarsigner("-verify -verbose a.jar")
70+
.shouldContain(NONEXISTENT_ENTRIES_FOUND)
71+
.shouldContain("Warning: nonexistent signed entries: [a, b]");
72+
73+
// Re-sign will not clear the warning.
74+
SecurityTools.jarsigner("-storepass changeit -keystore ks a.jar x");
75+
SecurityTools.jarsigner("-verify a.jar")
76+
.shouldContain(NONEXISTENT_ENTRIES_FOUND);
77+
78+
// Unfortunately, if there is a non-file entry in manifest, there will be
79+
// a false alarm. See https://bugs.openjdk.org/browse/JDK-8334261.
80+
var man = new Manifest();
81+
man.getMainAttributes().putValue("Manifest-Version", "1.0");
82+
man.getEntries().computeIfAbsent("Hello", key -> new Attributes())
83+
.putValue("Foo", "Bar");
84+
JarUtils.createJarFile(Path.of("b.jar"),
85+
man,
86+
Path.of("."),
87+
Path.of("a"));
88+
SecurityTools.jarsigner("-storepass changeit -keystore ks b.jar x");
89+
SecurityTools.jarsigner("-verbose -verify b.jar")
90+
.shouldContain("Warning: nonexistent signed entries: [Hello]")
91+
.shouldContain(NONEXISTENT_ENTRIES_FOUND);
92+
93+
}
94+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2024, 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+
/* @test
25+
* @bug 8309841
26+
* @summary Unit Test for a common Test API in jdk.test.lib.util.JarUtils
27+
* @library /test/lib
28+
*/
29+
30+
import jdk.test.lib.Asserts;
31+
import jdk.test.lib.util.JarUtils;
32+
33+
import java.io.IOException;
34+
import java.nio.file.Files;
35+
import java.nio.file.Path;
36+
import java.util.Set;
37+
import java.util.jar.JarEntry;
38+
import java.util.jar.JarFile;
39+
import java.util.stream.Collectors;
40+
41+
public class JarUtilsTest {
42+
public static void main(String[] args) throws Exception {
43+
Files.createDirectory(Path.of("bx"));
44+
JarUtils.createJarFile(Path.of("a.jar"),
45+
Path.of("."),
46+
Files.writeString(Path.of("a"), ""),
47+
Files.writeString(Path.of("b1"), ""),
48+
Files.writeString(Path.of("b2"), ""),
49+
Files.writeString(Path.of("bx/x"), ""),
50+
Files.writeString(Path.of("c"), ""),
51+
Files.writeString(Path.of("e1"), ""),
52+
Files.writeString(Path.of("e2"), ""));
53+
checkContent("a", "b1", "b2", "bx/x", "c", "e1", "e2");
54+
55+
JarUtils.deleteEntries(Path.of("a.jar"), "a");
56+
checkContent("b1", "b2", "bx/x", "c", "e1", "e2");
57+
58+
// Note: b* covers everything starting with b, even bx/x
59+
JarUtils.deleteEntries(Path.of("a.jar"), "b*");
60+
checkContent("c", "e1", "e2");
61+
62+
// d* does not match
63+
JarUtils.deleteEntries(Path.of("a.jar"), "d*");
64+
checkContent("c", "e1", "e2");
65+
66+
// multiple patterns
67+
JarUtils.deleteEntries(Path.of("a.jar"), "d*", "e*");
68+
checkContent("c");
69+
}
70+
71+
static void checkContent(String... expected) throws IOException {
72+
try (var jf = new JarFile("a.jar")) {
73+
Asserts.assertEquals(Set.of(expected),
74+
jf.stream().map(JarEntry::getName).collect(Collectors.toSet()));
75+
}
76+
}
77+
}

test/lib/jdk/test/lib/util/JarUtils.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -318,6 +318,57 @@ public static void updateManifest(String src, String dest, Manifest man)
318318
updateJar(src, dest, Map.of(JarFile.MANIFEST_NAME, bout.toByteArray()));
319319
}
320320

321+
/**
322+
* Remove entries from a ZIP file.
323+
*
324+
* Each entry can be a name or a name ending with "*".
325+
*
326+
* @return number of removed entries
327+
* @throws IOException if there is any I/O error
328+
*/
329+
public static int deleteEntries(Path jarfile, String... patterns)
330+
throws IOException {
331+
Path tmpfile = Files.createTempFile("jar", "jar");
332+
int count = 0;
333+
334+
try (OutputStream out = Files.newOutputStream(tmpfile);
335+
JarOutputStream jos = new JarOutputStream(out)) {
336+
try (JarFile jf = new JarFile(jarfile.toString())) {
337+
Enumeration<JarEntry> jentries = jf.entries();
338+
top: while (jentries.hasMoreElements()) {
339+
JarEntry jentry = jentries.nextElement();
340+
String name = jentry.getName();
341+
for (String pattern : patterns) {
342+
if (pattern.endsWith("*")) {
343+
if (name.startsWith(pattern.substring(
344+
0, pattern.length() - 1))) {
345+
// Go directly to next entry. This
346+
// one is not written into `jos` and
347+
// therefore removed.
348+
count++;
349+
continue top;
350+
}
351+
} else {
352+
if (name.equals(pattern)) {
353+
// Same as above
354+
count++;
355+
continue top;
356+
}
357+
}
358+
}
359+
// No pattern matched, file retained
360+
jos.putNextEntry(copyEntry(jentry));
361+
jf.getInputStream(jentry).transferTo(jos);
362+
}
363+
}
364+
}
365+
366+
// replace the original JAR file
367+
Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING);
368+
369+
return count;
370+
}
371+
321372
private static void updateEntry(JarOutputStream jos, String name, Object content)
322373
throws IOException {
323374
if (content instanceof Boolean) {

0 commit comments

Comments
 (0)