diff --git a/test/jdk/java/util/jar/JarFile/SignedJarPendingBlock.java b/test/jdk/java/util/jar/JarFile/SignedJarPendingBlock.java new file mode 100644 index 00000000000..a6f9955a507 --- /dev/null +++ b/test/jdk/java/util/jar/JarFile/SignedJarPendingBlock.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023, 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 + * @modules java.base/sun.security.tools.keytool + * @summary JARs with pending block files (where .RSA comes before .SF) should verify correctly + */ + +import jdk.security.jarsigner.JarSigner; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.util.Collections; +import java.util.jar.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class SignedJarPendingBlock { + + public static void main(String[] args) throws Exception { + Path jar = createJarFile(); + Path signed = signJarFile(jar); + Path pendingBlocks = moveBlockFirst(signed); + Path invalid = invalidate(pendingBlocks); + + // 1: Regular signed JAR with no pending blocks should verify + checkSigned(signed); + + // 2: Signed jar with pending blocks should verify + checkSigned(pendingBlocks); + + // 3: Invalid signed jar with pending blocks should throw SecurityException + try { + checkSigned(invalid); + throw new Exception("Expected invalid digest to be detected"); + } catch (SecurityException se) { + // Ignore + } + } + + private static void checkSigned(Path b) throws Exception { + try (JarFile jf = new JarFile(b.toFile(), true)) { + + JarEntry je = jf.getJarEntry("a.txt"); + try (InputStream in = jf.getInputStream(je)) { + in.transferTo(OutputStream.nullOutputStream()); + } + } + } + + /** + * Invalidate signed file by modifying the contents of "a.txt" + */ + private static Path invalidate(Path s) throws Exception{ + Path invalid = Path.of("pending-block-file-invalidated.jar"); + + try (ZipFile zip = new ZipFile(s.toFile()); + ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(invalid))) { + + for (ZipEntry ze : Collections.list(zip.entries())) { + String name = ze.getName(); + out.putNextEntry(new ZipEntry(name)); + + if (name.equals("a.txt")) { + // Change the contents of a.txt to trigger SignatureException + out.write("b".getBytes(StandardCharsets.UTF_8)); + } else { + try (InputStream in = zip.getInputStream(ze)) { + in.transferTo(out); + } + } + } + } + return invalid; + } + + private static Path moveBlockFirst(Path s) throws Exception { + Path b = Path.of("pending-block-file-blockfirst.jar"); + try (ZipFile in = new ZipFile(s.toFile()); + ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(b))) { + + copy("META-INF/MANIFEST.MF", in, out); + + // Switch the order of the RSA and SF files + copy("META-INF/SIGNER.RSA", in, out); + copy("META-INF/SIGNER.SF", in, out); + + copy("a.txt", in, out); + } + return b; + } + + /** + * Copy an entry from a ZipFile to a ZipOutputStream + */ + private static void copy(String name, ZipFile in, ZipOutputStream out) throws Exception { + out.putNextEntry(new ZipEntry(name)); + try (InputStream is = in.getInputStream(in.getEntry(name))) { + is.transferTo(out); + } + } + + private static Path signJarFile(Path j) throws Exception { + Path s = Path.of("pending-block-file-signed.jar"); + + Files.deleteIfExists(Path.of("ks")); + + sun.security.tools.keytool.Main.main( + ("-keystore ks -storepass changeit -keypass changeit -dname" + + " CN=SIGNER" +" -alias r -genkeypair -keyalg rsa").split(" ")); + + char[] pass = "changeit".toCharArray(); + + KeyStore ks = KeyStore.getInstance(new File("ks"), pass); + + KeyStore.PrivateKeyEntry pke = (KeyStore.PrivateKeyEntry) + ks.getEntry("r", new KeyStore.PasswordProtection(pass)); + + JarSigner signer = new JarSigner.Builder(pke) + .digestAlgorithm("SHA-256") + .signatureAlgorithm("SHA256withRSA") + .signerName("SIGNER") + .build(); + + try (ZipFile in = new ZipFile(j.toFile()); + OutputStream out = Files.newOutputStream(s)) { + signer.sign(in, out); + } + + return s; + } + + /** + * Create a jar file with single entry "a.txt" containing "a" + */ + private static Path createJarFile() throws Exception { + Path jar = Path.of("pending-block-file.jar"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar),manifest)) { + out.putNextEntry(new JarEntry("a.txt")); + out.write("a".getBytes(StandardCharsets.UTF_8)); + } + return jar; + } +}