|
21 | 21 | * questions. |
22 | 22 | */ |
23 | 23 |
|
| 24 | +import java.io.IOException; |
| 25 | +import java.io.InputStream; |
| 26 | +import java.io.OutputStream; |
| 27 | +import java.nio.file.Files; |
| 28 | +import java.nio.file.Path; |
| 29 | +import java.security.CodeSigner; |
| 30 | +import java.security.KeyStore; |
| 31 | +import java.security.cert.Certificate; |
| 32 | +import java.util.ArrayList; |
| 33 | +import java.util.Arrays; |
| 34 | +import java.util.Enumeration; |
| 35 | +import java.util.List; |
| 36 | +import java.util.concurrent.TimeUnit; |
| 37 | +import java.util.jar.JarEntry; |
| 38 | +import java.util.jar.JarFile; |
| 39 | +import java.util.jar.JarOutputStream; |
| 40 | +import java.util.zip.ZipFile; |
| 41 | + |
| 42 | +import jdk.security.jarsigner.JarSigner; |
| 43 | +import org.junit.jupiter.api.BeforeAll; |
| 44 | +import org.junit.jupiter.api.Test; |
| 45 | +import sun.security.tools.keytool.CertAndKeyGen; |
| 46 | +import sun.security.x509.X500Name; |
| 47 | +import static java.nio.charset.StandardCharsets.US_ASCII; |
| 48 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 49 | +import static org.junit.jupiter.api.Assertions.assertNull; |
| 50 | + |
24 | 51 | /* |
25 | 52 | * @test |
26 | 53 | * @bug 6337925 |
27 | 54 | * @summary Ensure that callers cannot modify the internal JarEntry cert and |
28 | 55 | * codesigner arrays. |
| 56 | + * @modules java.base/sun.security.tools.keytool |
| 57 | + * java.base/sun.security.x509 |
29 | 58 | * @run junit GetMethodsReturnClones |
30 | 59 | */ |
31 | | -import org.junit.jupiter.api.BeforeAll; |
32 | | -import org.junit.jupiter.api.Test; |
| 60 | +class GetMethodsReturnClones { |
33 | 61 |
|
34 | | -import java.io.IOException; |
35 | | -import java.io.InputStream; |
36 | | -import java.security.CodeSigner; |
37 | | -import java.security.cert.Certificate; |
38 | | -import java.util.*; |
39 | | -import java.util.jar.*; |
40 | 62 |
|
41 | | -import static org.junit.jupiter.api.Assertions.assertNotNull; |
42 | | - |
43 | | -public class GetMethodsReturnClones { |
| 63 | + private static final String ENTRY_NAME = "foobar.txt"; |
| 64 | + private static final String SIGNER_NAME = "DUMMY"; |
| 65 | + private static final String DIGEST_ALGORITHM = "SHA-256"; |
| 66 | + private static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; |
| 67 | + private static final String KEY_TYPE = "RSA"; |
44 | 68 |
|
45 | | - private static final String BASE = System.getProperty("test.src", ".") + |
46 | | - System.getProperty("file.separator"); |
47 | 69 | private static List<JarEntry> jarEntries; |
48 | 70 |
|
| 71 | + /* |
| 72 | + * Creates a signed JAR file and initializes the "jarEntries" |
| 73 | + */ |
49 | 74 | @BeforeAll() |
50 | | - static void setupEntries() throws IOException { |
| 75 | + static void setupJarEntries() throws Exception { |
| 76 | + Path unsigned = createJar(); |
| 77 | + Path signed = signJar(unsigned); |
| 78 | + System.err.println("created signed JAR file at " + signed.toAbsolutePath()); |
51 | 79 | List<JarEntry> entries = new ArrayList<>(); |
52 | | - try (JarFile jf = new JarFile(BASE + "test.jar", true)) { |
53 | | - byte[] buffer = new byte[8192]; |
| 80 | + try (JarFile jf = new JarFile(signed.toFile(), true)) { |
54 | 81 | Enumeration<JarEntry> e = jf.entries(); |
55 | 82 | while (e.hasMoreElements()) { |
56 | 83 | JarEntry je = e.nextElement(); |
57 | 84 | entries.add(je); |
58 | 85 | try (InputStream is = jf.getInputStream(je)) { |
59 | | - while (is.read(buffer, 0, buffer.length) != -1) { |
60 | | - // we just read. this will throw a SecurityException |
61 | | - // if a signature/digest check fails. |
62 | | - } |
| 86 | + // we just read. this will throw a SecurityException |
| 87 | + // if a signature/digest check fails. |
| 88 | + var _ = is.readAllBytes(); |
63 | 89 | } |
64 | 90 | } |
65 | 91 | } |
66 | 92 | jarEntries = entries; |
67 | 93 | } |
68 | 94 |
|
| 95 | + /* |
| 96 | + * For entries in the signed JAR file, this test verifies that if a non-null |
| 97 | + * array is returned by the JarEntry.getCertificates() method, then any subsequent |
| 98 | + * updates to that returned array do not propagate back to the original array. |
| 99 | + */ |
69 | 100 | @Test |
70 | | - void certsTest() { |
| 101 | + void testCertificatesArray() { |
71 | 102 | for (JarEntry je : jarEntries) { |
72 | 103 | Certificate[] certs = je.getCertificates(); |
73 | | - if (certs != null) { |
74 | | - certs[0] = null; |
75 | | - certs = je.getCertificates(); |
76 | | - assertNotNull(certs[0], "Modified internal certs array"); |
| 104 | + System.err.println("Certificates for " + je.getName() + " " + Arrays.toString(certs)); |
| 105 | + if (isSignatureRelated(je)) { |
| 106 | + // we don't expect this entry to be signed |
| 107 | + assertNull(certs, "JarEntry.getCertificates() returned non-null for " + je.getName()); |
| 108 | + continue; |
77 | 109 | } |
| 110 | + assertNotNull(certs, "JarEntry.getCertificates() returned null for " + je.getName()); |
| 111 | + assertNotNull(certs[0], "Certificate is null"); |
| 112 | + |
| 113 | + certs[0] = null; // intentionally update the returned array |
| 114 | + certs = je.getCertificates(); // now get the certs again |
| 115 | + assertNotNull(certs, "JarEntry.getCertificates() returned null for " + je.getName()); |
| 116 | + // verify that the newly returned array doesn't have the overwritten value |
| 117 | + assertNotNull(certs[0], "Internal certificates array was modified"); |
78 | 118 | } |
79 | 119 | } |
80 | 120 |
|
| 121 | + /* |
| 122 | + * For entries in the signed JAR file, this test verifies that if a non-null |
| 123 | + * array is returned by the JarEntry.getCodeSigners() method, then any subsequent |
| 124 | + * updates to that returned array do not propagate back to the original array. |
| 125 | + */ |
81 | 126 | @Test |
82 | | - void signersTest() { |
| 127 | + void testCodeSignersArray() { |
83 | 128 | for (JarEntry je : jarEntries) { |
84 | 129 | CodeSigner[] signers = je.getCodeSigners(); |
85 | | - if (signers != null) { |
86 | | - signers[0] = null; |
87 | | - signers = je.getCodeSigners(); |
88 | | - assertNotNull(signers[0], "Modified internal codesigners array"); |
| 130 | + System.err.println("CodeSigners for " + je.getName() + " " + Arrays.toString(signers)); |
| 131 | + if (isSignatureRelated(je)) { |
| 132 | + // we don't expect this entry to be signed |
| 133 | + assertNull(signers, "JarEntry.getCodeSigners() returned non-null for " + je.getName()); |
| 134 | + continue; |
89 | 135 | } |
| 136 | + assertNotNull(signers, "JarEntry.getCodeSigners() returned null for " + je.getName()); |
| 137 | + assertNotNull(signers[0], "CodeSigner is null"); |
| 138 | + |
| 139 | + signers[0] = null; // intentionally update the array |
| 140 | + signers = je.getCodeSigners(); // now get the codesigners again |
| 141 | + assertNotNull(signers, "JarEntry.getCodeSigners() returned null for " + je.getName()); |
| 142 | + // verify that the newly returned array doesn't have the overwritten value |
| 143 | + assertNotNull(signers[0], "CodeSigner is null"); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + private static Path createJar() throws IOException { |
| 148 | + final Path unsigned = Path.of("unsigned.jar"); |
| 149 | + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(unsigned))) { |
| 150 | + out.putNextEntry(new JarEntry(ENTRY_NAME)); |
| 151 | + out.write("hello world".getBytes(US_ASCII)); |
90 | 152 | } |
| 153 | + return unsigned; |
| 154 | + } |
| 155 | + |
| 156 | + private static Path signJar(final Path unsigned) throws Exception { |
| 157 | + final Path signed = Path.of("signed.jar"); |
| 158 | + final JarSigner signer = new JarSigner.Builder(privateKeyEntry()) |
| 159 | + .signerName(SIGNER_NAME) |
| 160 | + .digestAlgorithm(DIGEST_ALGORITHM) |
| 161 | + .signatureAlgorithm(SIGNATURE_ALGORITHM) |
| 162 | + .build(); |
| 163 | + try (ZipFile zip = new ZipFile(unsigned.toFile()); |
| 164 | + OutputStream out = Files.newOutputStream(signed)) { |
| 165 | + signer.sign(zip, out); |
| 166 | + } |
| 167 | + return signed; |
| 168 | + } |
| 169 | + |
| 170 | + private static KeyStore.PrivateKeyEntry privateKeyEntry() throws Exception { |
| 171 | + final CertAndKeyGen gen = new CertAndKeyGen(KEY_TYPE, SIGNATURE_ALGORITHM); |
| 172 | + gen.generate(4096); |
| 173 | + final long oneDayInSecs = TimeUnit.SECONDS.convert(1, TimeUnit.DAYS); |
| 174 | + Certificate cert = gen.getSelfCertificate(new X500Name("cn=duke"), oneDayInSecs); |
| 175 | + return new KeyStore.PrivateKeyEntry(gen.getPrivateKey(), new Certificate[] {cert}); |
| 176 | + } |
| 177 | + |
| 178 | + private static boolean isSignatureRelated(final JarEntry entry) { |
| 179 | + final String entryName = entry.getName(); |
| 180 | + return entryName.equals("META-INF/" + SIGNER_NAME + ".SF") |
| 181 | + || entryName.equals("META-INF/" + SIGNER_NAME + ".RSA"); |
91 | 182 | } |
92 | 183 | } |
0 commit comments