Skip to content

Commit 9c3f51f

Browse files
Roman MarchenkoRealCLanger
authored andcommitted
8309841: Jarsigner should print a warning if an entry is removed
Reviewed-by: yan Backport-of: bdfb41f977258831e4b0ceaef5d016d095ab6e7f
1 parent 287b7c7 commit 9c3f51f

File tree

5 files changed

+241
-1
lines changed

5 files changed

+241
-1
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ public static void main(String args[]) throws Exception {
180180
private boolean hasExpiringCert = false;
181181
private boolean hasExpiringTsaCert = false;
182182
private boolean noTimestamp = true;
183+
private boolean hasNonexistentEntries = false;
183184

184185
// Expiration date. The value could be null if signed by a trusted cert.
185186
private Date expireDate = null;
@@ -706,6 +707,7 @@ void verifyJar(String jarName)
706707
Map<String,PKCS7> sigMap = new HashMap<>();
707708
Map<String,String> sigNameMap = new HashMap<>();
708709
Map<String,String> unparsableSignatures = new HashMap<>();
710+
Map<String,Set<String>> entriesInSF = new HashMap<>();
709711

710712
try {
711713
jf = new JarFile(jarName, true);
@@ -753,6 +755,7 @@ void verifyJar(String jarName)
753755
break;
754756
}
755757
}
758+
entriesInSF.put(alias, sf.getEntries().keySet());
756759
if (!found) {
757760
unparsableSignatures.putIfAbsent(alias,
758761
String.format(
@@ -851,6 +854,9 @@ void verifyJar(String jarName)
851854
sb.append('\n');
852855
}
853856
}
857+
for (var signed : entriesInSF.values()) {
858+
signed.remove(name);
859+
}
854860
} else if (showcerts && !verbose.equals("all")) {
855861
// Print no info for unsigned entries when -verbose:all,
856862
// to be consistent with old behavior.
@@ -1044,6 +1050,13 @@ void verifyJar(String jarName)
10441050
if (verbose != null) {
10451051
System.out.println(history);
10461052
}
1053+
var signed = entriesInSF.get(s);
1054+
if (!signed.isEmpty()) {
1055+
if (verbose != null) {
1056+
System.out.println(rb.getString("history.nonexistent.entries") + signed);
1057+
}
1058+
hasNonexistentEntries = true;
1059+
}
10471060
} else {
10481061
unparsableSignatures.putIfAbsent(s, String.format(
10491062
rb.getString("history.nobk"), s));
@@ -1287,6 +1300,9 @@ private void displayMessagesAndResult(boolean isSigning) {
12871300
}
12881301
}
12891302

1303+
if (hasNonexistentEntries) {
1304+
warnings.add(rb.getString("nonexistent.entries.found"));
1305+
}
12901306
if (extraAttrsDetected) {
12911307
warnings.add(rb.getString("extra.attributes.detected"));
12921308
}

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
@@ -165,6 +165,7 @@ public class Resources extends java.util.ListResourceBundle {
165165

166166
{"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"},
167167
{"history.without.ts", "- Signed by \"%1$s\"\n Digest algorithm: %2$s\n Signature algorithm: %3$s, %4$s"},
168+
{"history.nonexistent.entries", " Warning: nonexistent signed entries: "},
168169
{"history.unparsable", "- Unparsable signature-related file %s"},
169170
{"history.nosf", "- Missing signature-related file META-INF/%s.SF"},
170171
{"history.nobk", "- Missing block file for signature-related file META-INF/%s.SF"},
@@ -175,6 +176,7 @@ public class Resources extends java.util.ListResourceBundle {
175176
{"key.bit.weak", "%d-bit key (weak)"},
176177
{"key.bit.disabled", "%d-bit key (disabled)"},
177178
{"unknown.size", "unknown size"},
179+
{"nonexistent.entries.found", "This jar contains signed entries for files that do not exist. See the -verbose output for more details."},
178180
{"extra.attributes.detected", "POSIX file permission and/or symlink attributes detected. These attributes are ignored when signing and are not protected by the signature."},
179181

180182
{"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 ed25519");
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
@@ -320,6 +320,57 @@ public static void updateManifest(String src, String dest, Manifest man)
320320
updateJar(src, dest, Map.of(JarFile.MANIFEST_NAME, bout.toByteArray()));
321321
}
322322

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

0 commit comments

Comments
 (0)