From 6efb9cbad1b94e042439902da239a4053ae21fe1 Mon Sep 17 00:00:00 2001 From: Roman Marchenko Date: Mon, 24 Mar 2025 15:47:50 +0200 Subject: [PATCH 1/2] Backport 2a791467919c9df9869e6fe1e57df0a5caa90d8f --- .../testlibrary/jdk/testlibrary/JarUtils.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java b/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java index 8d1e9ef6e57..271fc93bfbc 100644 --- a/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java +++ b/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -216,7 +216,7 @@ public static void updateJar(String src, String dest, changes.remove(name); } else { System.out.println(String.format("- Copy %s", name)); - jos.putNextEntry(entry); + jos.putNextEntry(copyEntry(entry)); Utils.transferTo(srcJarFile.getInputStream(entry), jos); } } @@ -276,4 +276,17 @@ private static String toJarEntryName(Path file) { .toString() .replace(File.separatorChar, '/'); } + + private static JarEntry copyEntry(JarEntry e1) { + JarEntry e2 = new JarEntry(e1.getName()); + e2.setMethod(e1.getMethod()); + e2.setTime(e1.getTime()); + e2.setComment(e1.getComment()); + e2.setExtra(e1.getExtra()); + if (e1.getMethod() == JarEntry.STORED) { + e2.setSize(e1.getSize()); + e2.setCrc(e1.getCrc()); + } + return e2; + } } From 3c99ab3a1716012d3cc28524ea6f2fcb077c9584 Mon Sep 17 00:00:00 2001 From: Roman Marchenko Date: Mon, 10 Mar 2025 17:48:39 +0200 Subject: [PATCH 2/2] Backport bdfb41f977258831e4b0ceaef5d016d095ab6e7f --- .../sun/security/tools/jarsigner/Main.java | 17 ++++ .../security/tools/jarsigner/Resources.java | 2 + .../jdk/testlibrary/JarUtilsTest.java | 79 +++++++++++++++ .../testlibrary/jdk/testlibrary/JarUtils.java | 54 ++++++++++- .../tools/jarsigner/RemovedFiles.java | 95 +++++++++++++++++++ 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 jdk/test/lib-test/testlibrary/jdk/testlibrary/JarUtilsTest.java create mode 100644 jdk/test/sun/security/tools/jarsigner/RemovedFiles.java diff --git a/jdk/src/share/classes/sun/security/tools/jarsigner/Main.java b/jdk/src/share/classes/sun/security/tools/jarsigner/Main.java index 667b602dd55..0cfa2c38ea6 100644 --- a/jdk/src/share/classes/sun/security/tools/jarsigner/Main.java +++ b/jdk/src/share/classes/sun/security/tools/jarsigner/Main.java @@ -181,6 +181,7 @@ public static void main(String args[]) throws Exception { private boolean hasExpiringCert = false; private boolean hasExpiringTsaCert = false; private boolean noTimestamp = true; + private boolean hasNonexistentEntries = false; // Expiration date. The value could be null if signed by a trusted cert. private Date expireDate = null; @@ -635,6 +636,7 @@ void verifyJar(String jarName) Map sigMap = new HashMap<>(); Map sigNameMap = new HashMap<>(); Map unparsableSignatures = new HashMap<>(); + Map> entriesInSF = new HashMap<>(); try { jf = new JarFile(jarName, true); @@ -671,6 +673,7 @@ void verifyJar(String jarName) break; } } + entriesInSF.put(alias, sf.getEntries().keySet()); if (!found) { unparsableSignatures.putIfAbsent(alias, String.format( @@ -769,6 +772,9 @@ void verifyJar(String jarName) sb.append('\n'); } } + for (Set signed : entriesInSF.values()) { + signed.remove(name); + } } else if (showcerts && !verbose.equals("all")) { // Print no info for unsigned entries when -verbose:all, // to be consistent with old behavior. @@ -951,6 +957,13 @@ void verifyJar(String jarName) if (verbose != null) { System.out.println(history); } + Set signed = entriesInSF.get(s); + if (!signed.isEmpty()) { + if (verbose != null) { + System.out.println(rb.getString("history.nonexistent.entries") + signed); + } + hasNonexistentEntries = true; + } } else { unparsableSignatures.putIfAbsent(s, String.format( rb.getString("history.nobk"), s)); @@ -1180,6 +1193,7 @@ private void displayMessagesAndResult(boolean isSigning) { if (hasExpiringCert || (hasExpiringTsaCert && expireDate != null) || (noTimestamp && expireDate != null) || + hasNonexistentEntries || (hasExpiredTsaCert && signerNotExpired)) { if (hasExpiredTsaCert && signerNotExpired) { @@ -1217,6 +1231,9 @@ private void displayMessagesAndResult(boolean isSigning) { : "no.timestamp.verifying"), expireDate)); } } + if (hasNonexistentEntries) { + warnings.add(rb.getString("nonexistent.entries.found")); + } } System.out.println(result); diff --git a/jdk/src/share/classes/sun/security/tools/jarsigner/Resources.java b/jdk/src/share/classes/sun/security/tools/jarsigner/Resources.java index 48bfe3169d0..923897089a9 100644 --- a/jdk/src/share/classes/sun/security/tools/jarsigner/Resources.java +++ b/jdk/src/share/classes/sun/security/tools/jarsigner/Resources.java @@ -155,6 +155,7 @@ public class Resources extends java.util.ListResourceBundle { {"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"}, {"history.without.ts", "- Signed by \"%1$s\"\n Digest algorithm: %2$s\n Signature algorithm: %3$s, %4$s"}, + {"history.nonexistent.entries", " Warning: nonexistent signed entries: "}, {"history.unparsable", "- Unparsable signature-related file %s"}, {"history.nosf", "- Missing signature-related file META-INF/%s.SF"}, {"history.nobk", "- Missing block file for signature-related file META-INF/%s.SF"}, @@ -165,6 +166,7 @@ public class Resources extends java.util.ListResourceBundle { {"key.bit.weak", "%d-bit key (weak)"}, {"key.bit.disabled", "%d-bit key (disabled)"}, {"unknown.size", "unknown size"}, + {"nonexistent.entries.found", "This jar contains signed entries for files that do not exist. See the -verbose output for more details."}, {"jarsigner.", "jarsigner: "}, {"signature.filename.must.consist.of.the.following.characters.A.Z.0.9.or.", diff --git a/jdk/test/lib-test/testlibrary/jdk/testlibrary/JarUtilsTest.java b/jdk/test/lib-test/testlibrary/jdk/testlibrary/JarUtilsTest.java new file mode 100644 index 00000000000..241d767c9f6 --- /dev/null +++ b/jdk/test/lib-test/testlibrary/jdk/testlibrary/JarUtilsTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* @test + * @bug 8309841 + * @summary Unit Test for a common Test API in jdk.testlibrary.JarUtils + * @library /lib/testlibrary + */ + +import jdk.testlibrary.Asserts; +import jdk.testlibrary.JarUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +public class JarUtilsTest { + public static void main(String[] args) throws Exception { + Files.createDirectory(Paths.get("bx")); + JarUtils.createJarFile(Paths.get("a.jar"), + Paths.get("."), + Files.write(Paths.get("a"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("b1"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("b2"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("bx/x"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("c"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("e1"), "".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("e2"), "".getBytes(StandardCharsets.UTF_8))); + checkContent("a", "b1", "b2", "bx/x", "c", "e1", "e2"); + + JarUtils.deleteEntries(Paths.get("a.jar"), "a"); + checkContent("b1", "b2", "bx/x", "c", "e1", "e2"); + + // Note: b* covers everything starting with b, even bx/x + JarUtils.deleteEntries(Paths.get("a.jar"), "b*"); + checkContent("c", "e1", "e2"); + + // d* does not match + JarUtils.deleteEntries(Paths.get("a.jar"), "d*"); + checkContent("c", "e1", "e2"); + + // multiple patterns + JarUtils.deleteEntries(Paths.get("a.jar"), "d*", "e*"); + checkContent("c"); + } + + static void checkContent(String... expected) throws IOException { + try (JarFile jf = new JarFile("a.jar")) { + Asserts.assertEquals(new HashSet<>(Arrays.asList(expected)), + jf.stream().map(JarEntry::getName).collect(Collectors.toSet())); + } + } +} diff --git a/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java b/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java index 271fc93bfbc..8062d56d252 100644 --- a/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java +++ b/jdk/test/lib/testlibrary/jdk/testlibrary/JarUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,6 +34,7 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; @@ -247,6 +248,57 @@ public static void updateManifest(String src, String dest, Manifest man) updateJar(src, dest, map); } + /** + * Remove entries from a ZIP file. + * + * Each entry can be a name or a name ending with "*". + * + * @return number of removed entries + * @throws IOException if there is any I/O error + */ + public static int deleteEntries(Path jarfile, String... patterns) + throws IOException { + Path tmpfile = Files.createTempFile("jar", "jar"); + int count = 0; + + try (OutputStream out = Files.newOutputStream(tmpfile); + JarOutputStream jos = new JarOutputStream(out)) { + try (JarFile jf = new JarFile(jarfile.toString())) { + Enumeration jentries = jf.entries(); + top: while (jentries.hasMoreElements()) { + JarEntry jentry = jentries.nextElement(); + String name = jentry.getName(); + for (String pattern : patterns) { + if (pattern.endsWith("*")) { + if (name.startsWith(pattern.substring( + 0, pattern.length() - 1))) { + // Go directly to next entry. This + // one is not written into `jos` and + // therefore removed. + count++; + continue top; + } + } else { + if (name.equals(pattern)) { + // Same as above + count++; + continue top; + } + } + } + // No pattern matched, file retained + jos.putNextEntry(copyEntry(jentry)); + Utils.transferTo(jf.getInputStream(jentry), jos); + } + } + } + + // replace the original JAR file + Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING); + + return count; + } + private static void updateEntry(JarOutputStream jos, String name, Object content) throws IOException { if (content instanceof Boolean) { diff --git a/jdk/test/sun/security/tools/jarsigner/RemovedFiles.java b/jdk/test/sun/security/tools/jarsigner/RemovedFiles.java new file mode 100644 index 00000000000..3e7815c62df --- /dev/null +++ b/jdk/test/sun/security/tools/jarsigner/RemovedFiles.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8309841 + * @summary Jarsigner should print a warning if an entry is removed + * @library /lib/testlibrary + */ + +import jdk.testlibrary.SecurityTools; +import jdk.testlibrary.JarUtils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +public class RemovedFiles { + + private static final String NONEXISTENT_ENTRIES_FOUND + = "This jar contains signed entries for files that do not exist. See the -verbose output for more details."; + + public static void main(String[] args) throws Exception { + JarUtils.createJarFile( + Paths.get("a.jar"), + Paths.get("."), + Files.write(Paths.get("a"), "a".getBytes(StandardCharsets.UTF_8)), + Files.write(Paths.get("b"), "b".getBytes(StandardCharsets.UTF_8))); + SecurityTools.keytool("-genkeypair -storepass changeit -keystore ks -alias x -dname CN=x -keyalg RSA"); + SecurityTools.jarsigner("-storepass changeit -keystore ks a.jar x"); + + // All is fine at the beginning. + SecurityTools.jarsigner("-verify a.jar") + .shouldNotContain(NONEXISTENT_ENTRIES_FOUND); + + // Remove an entry after signing. There will be a warning. + JarUtils.deleteEntries(Paths.get("a.jar"), "a"); + SecurityTools.jarsigner("-verify a.jar") + .shouldContain(NONEXISTENT_ENTRIES_FOUND); + SecurityTools.jarsigner("-verify -verbose a.jar") + .shouldContain(NONEXISTENT_ENTRIES_FOUND) + .shouldContain("Warning: nonexistent signed entries: [a]"); + + // Remove one more entry. + JarUtils.deleteEntries(Paths.get("a.jar"), "b"); + SecurityTools.jarsigner("-verify a.jar") + .shouldContain(NONEXISTENT_ENTRIES_FOUND); + SecurityTools.jarsigner("-verify -verbose a.jar") + .shouldContain(NONEXISTENT_ENTRIES_FOUND) + .shouldContain("Warning: nonexistent signed entries: [a, b]"); + + // Re-sign will not clear the warning. + SecurityTools.jarsigner("-storepass changeit -keystore ks a.jar x"); + SecurityTools.jarsigner("-verify a.jar") + .shouldContain(NONEXISTENT_ENTRIES_FOUND); + + // Unfortunately, if there is a non-file entry in manifest, there will be + // a false alarm. See https://bugs.openjdk.org/browse/JDK-8334261. + Manifest man = new Manifest(); + man.getMainAttributes().putValue("Manifest-Version", "1.0"); + man.getEntries().computeIfAbsent("Hello", key -> new Attributes()) + .putValue("Foo", "Bar"); + JarUtils.createJarFile(Paths.get("b.jar"), + man, + Paths.get("."), + Paths.get("a")); + SecurityTools.jarsigner("-storepass changeit -keystore ks b.jar x"); + SecurityTools.jarsigner("-verbose -verify b.jar") + .shouldContain("Warning: nonexistent signed entries: [Hello]") + .shouldContain(NONEXISTENT_ENTRIES_FOUND); + + } +}