Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 80d7902

Browse files
author
Yuri Nesterenko
committed
8278851: Correct signer logic for jars signed with multiple digest algorithms
Reviewed-by: bae Backport-of: 61b8944327e3d12cf58dc3f6bc45ecbeba4ef611
1 parent f9b153a commit 80d7902

File tree

4 files changed

+253
-51
lines changed

4 files changed

+253
-51
lines changed

src/java.base/share/classes/java/util/jar/JarVerifier.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1997, 2022, 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
@@ -96,6 +96,10 @@ class JarVerifier {
9696
/** collect -DIGEST-MANIFEST values for blacklist */
9797
private List<Object> manifestDigests;
9898

99+
/* A cache mapping code signers to the algorithms used to digest jar
100+
entries, and whether or not the algorithms are permitted. */
101+
private Map<CodeSigner[], Map<String, Boolean>> signersToAlgs;
102+
99103
public JarVerifier(String name, byte rawBytes[]) {
100104
manifestName = name;
101105
manifestRawBytes = rawBytes;
@@ -105,6 +109,7 @@ public JarVerifier(String name, byte rawBytes[]) {
105109
pendingBlocks = new ArrayList<>();
106110
baos = new ByteArrayOutputStream();
107111
manifestDigests = new ArrayList<>();
112+
signersToAlgs = new HashMap<>();
108113
}
109114

110115
/**
@@ -244,7 +249,8 @@ private void processEntry(ManifestEntryVerifier mev)
244249
if (!parsingBlockOrSF) {
245250
JarEntry je = mev.getEntry();
246251
if ((je != null) && (je.signers == null)) {
247-
je.signers = mev.verify(verifiedSigners, sigFileSigners);
252+
je.signers = mev.verify(verifiedSigners, sigFileSigners,
253+
signersToAlgs);
248254
je.certs = mapSignersToCertArray(je.signers);
249255
}
250256
} else {

src/java.base/share/classes/sun/security/util/ManifestEntryVerifier.java

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1997, 2020, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1997, 2022, 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
@@ -192,7 +192,8 @@ public JarEntry getEntry()
192192
*
193193
*/
194194
public CodeSigner[] verify(Hashtable<String, CodeSigner[]> verifiedSigners,
195-
Hashtable<String, CodeSigner[]> sigFileSigners)
195+
Hashtable<String, CodeSigner[]> sigFileSigners,
196+
Map<CodeSigner[], Map<String, Boolean>> signersToAlgs)
196197
throws JarException
197198
{
198199
if (skip) {
@@ -207,38 +208,60 @@ public CodeSigner[] verify(Hashtable<String, CodeSigner[]> verifiedSigners,
207208
return signers;
208209
}
209210

210-
JarConstraintsParameters params =
211-
getParams(verifiedSigners, sigFileSigners);
211+
CodeSigner[] entrySigners = sigFileSigners.get(name);
212+
Map<String, Boolean> algsPermittedStatus =
213+
algsPermittedStatusForSigners(signersToAlgs, entrySigners);
212214

215+
// Flag that indicates if only disabled algorithms are used and jar
216+
// entry should be treated as unsigned.
217+
boolean disabledAlgs = true;
218+
JarConstraintsParameters params = null;
213219
for (int i=0; i < digests.size(); i++) {
214220
MessageDigest digest = digests.get(i);
215-
if (params != null) {
216-
try {
217-
params.setExtendedExceptionMsg(JarFile.MANIFEST_NAME,
218-
name + " entry");
219-
DisabledAlgorithmConstraints.jarConstraints()
220-
.permits(digest.getAlgorithm(), params);
221-
} catch (GeneralSecurityException e) {
222-
if (debug != null) {
223-
debug.println("Digest algorithm is restricted: " + e);
221+
String digestAlg = digest.getAlgorithm();
222+
223+
// Check if this algorithm is permitted, skip if false.
224+
if (algsPermittedStatus != null) {
225+
Boolean permitted = algsPermittedStatus.get(digestAlg);
226+
if (permitted == null) {
227+
if (params == null) {
228+
params = new JarConstraintsParameters(entrySigners);
224229
}
225-
return null;
230+
if (!checkConstraints(digestAlg, params)) {
231+
algsPermittedStatus.put(digestAlg, Boolean.FALSE);
232+
continue;
233+
} else {
234+
algsPermittedStatus.put(digestAlg, Boolean.TRUE);
235+
}
236+
} else if (!permitted) {
237+
continue;
226238
}
227239
}
240+
241+
// A non-disabled algorithm was used.
242+
disabledAlgs = false;
243+
228244
byte [] manHash = manifestHashes.get(i);
229245
byte [] theHash = digest.digest();
230246

231247
if (debug != null) {
232248
debug.println("Manifest Entry: " +
233-
name + " digest=" + digest.getAlgorithm());
249+
name + " digest=" + digestAlg);
234250
debug.println(" manifest " + toHex(manHash));
235251
debug.println(" computed " + toHex(theHash));
236252
debug.println();
237253
}
238254

239-
if (!MessageDigest.isEqual(theHash, manHash))
240-
throw new SecurityException(digest.getAlgorithm()+
255+
if (!MessageDigest.isEqual(theHash, manHash)) {
256+
throw new SecurityException(digestAlg +
241257
" digest error for "+name);
258+
}
259+
}
260+
261+
// If there were only disabled algorithms used, return null and jar
262+
// entry will be treated as unsigned.
263+
if (disabledAlgs) {
264+
return null;
242265
}
243266

244267
// take it out of sigFileSigners and put it in verifiedSigners...
@@ -249,39 +272,36 @@ public CodeSigner[] verify(Hashtable<String, CodeSigner[]> verifiedSigners,
249272
return signers;
250273
}
251274

252-
/**
253-
* Get constraints parameters for JAR. The constraints should be
254-
* checked against all code signers. Returns the parameters,
255-
* or null if the signers for this entry have already been checked
256-
* or there are no signers for this entry.
257-
*/
258-
private JarConstraintsParameters getParams(
259-
Map<String, CodeSigner[]> verifiedSigners,
260-
Map<String, CodeSigner[]> sigFileSigners) {
261-
262-
// verifiedSigners is usually preloaded with the Manifest's signers.
263-
// If verifiedSigners contains the Manifest, then it will have all of
264-
// the signers of the JAR. But if it doesn't then we need to fallback
265-
// and check verifiedSigners to see if the signers of this entry have
266-
// been checked already.
267-
if (verifiedSigners.containsKey(manifestFileName)) {
268-
if (verifiedSigners.size() > 1) {
269-
// this means we already checked it previously
270-
return null;
271-
} else {
272-
return new JarConstraintsParameters(
273-
verifiedSigners.get(manifestFileName));
275+
// Gets the algorithms permitted status for the signers of this entry.
276+
private static Map<String, Boolean> algsPermittedStatusForSigners(
277+
Map<CodeSigner[], Map<String, Boolean>> signersToAlgs,
278+
CodeSigner[] signers) {
279+
if (signers != null) {
280+
Map<String, Boolean> algs = signersToAlgs.get(signers);
281+
// create new HashMap if absent
282+
if (algs == null) {
283+
algs = new HashMap<>();
284+
signersToAlgs.put(signers, algs);
274285
}
275-
} else {
286+
return algs;
287+
}
288+
return null;
289+
}
290+
291+
// Checks the algorithm constraints against the signers of this entry.
292+
private boolean checkConstraints(String algorithm,
293+
JarConstraintsParameters params) {
294+
try {
295+
params.setExtendedExceptionMsg(JarFile.MANIFEST_NAME,
296+
name + " entry");
297+
DisabledAlgorithmConstraints.jarConstraints()
298+
.permits(algorithm, params);
299+
return true;
300+
} catch (GeneralSecurityException e) {
276301
if (debug != null) {
277-
debug.println(manifestFileName + " not present in verifiedSigners");
278-
}
279-
CodeSigner[] signers = sigFileSigners.get(name);
280-
if (signers == null || verifiedSigners.containsValue(signers)) {
281-
return null;
282-
} else {
283-
return new JarConstraintsParameters(signers);
302+
debug.println("Digest algorithm is restricted: " + e);
284303
}
304+
return false;
285305
}
286306
}
287307

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright (c) 2022, 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 8278851
27+
* @summary Check that jar entry with at least one non-disabled digest
28+
* algorithm in manifest is treated as signed
29+
* @modules java.base/sun.security.tools.keytool
30+
* @library /test/lib
31+
* @build jdk.test.lib.util.JarUtils
32+
* jdk.test.lib.security.SecurityUtils
33+
* @run main/othervm JarWithOneNonDisabledDigestAlg
34+
*/
35+
36+
import java.io.InputStream;
37+
import java.io.FileInputStream;
38+
import java.io.FileOutputStream;
39+
import java.nio.file.Files;
40+
import java.nio.file.Path;
41+
import java.security.CodeSigner;
42+
import java.security.KeyStore;
43+
import java.util.Enumeration;
44+
import java.util.List;
45+
import java.util.Locale;
46+
import java.util.jar.JarEntry;
47+
import java.util.jar.JarFile;
48+
import java.util.zip.ZipFile;
49+
import jdk.security.jarsigner.JarSigner;
50+
51+
import jdk.test.lib.util.JarUtils;
52+
import jdk.test.lib.security.SecurityUtils;
53+
54+
public class JarWithOneNonDisabledDigestAlg {
55+
56+
private static final String PASS = "changeit";
57+
private static final String TESTFILE1 = "testfile1";
58+
private static final String TESTFILE2 = "testfile2";
59+
60+
public static void main(String[] args) throws Exception {
61+
SecurityUtils.removeFromDisabledAlgs("jdk.jar.disabledAlgorithms",
62+
List.of("SHA1"));
63+
Files.write(Path.of(TESTFILE1), TESTFILE1.getBytes());
64+
JarUtils.createJarFile(Path.of("unsigned.jar"), Path.of("."),
65+
Path.of(TESTFILE1));
66+
67+
genkeypair("-alias SHA1 -sigalg SHA1withRSA");
68+
genkeypair("-alias SHA256 -sigalg SHA256withRSA");
69+
70+
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
71+
try (FileInputStream fis = new FileInputStream("keystore")) {
72+
ks.load(fis, PASS.toCharArray());
73+
}
74+
75+
// Sign JAR twice with same signer but different digest algorithms
76+
// so that each entry in manifest file contains two digest values.
77+
signJarFile(ks, "SHA1", "MD5", "unsigned.jar", "signed.jar");
78+
signJarFile(ks, "SHA1", "SHA1", "signed.jar", "signed2.jar");
79+
checkThatJarIsSigned("signed2.jar", false);
80+
81+
// add another file to the JAR
82+
Files.write(Path.of(TESTFILE2), "testFile2".getBytes());
83+
JarUtils.updateJarFile(Path.of("signed2.jar"), Path.of("."),
84+
Path.of(TESTFILE2));
85+
86+
// Sign again with different signer (SHA256) and SHA-1 digestalg.
87+
// TESTFILE1 should have two signers and TESTFILE2 should have one
88+
// signer.
89+
signJarFile(ks, "SHA256", "SHA1", "signed2.jar", "multi-signed.jar");
90+
91+
checkThatJarIsSigned("multi-signed.jar", true);
92+
}
93+
94+
private static KeyStore.PrivateKeyEntry getEntry(KeyStore ks, String alias)
95+
throws Exception {
96+
97+
return (KeyStore.PrivateKeyEntry)
98+
ks.getEntry(alias,
99+
new KeyStore.PasswordProtection(PASS.toCharArray()));
100+
}
101+
102+
private static void genkeypair(String cmd) throws Exception {
103+
cmd = "-genkeypair -keystore keystore -storepass " + PASS +
104+
" -keypass " + PASS + " -keyalg rsa -dname CN=Duke " + cmd;
105+
sun.security.tools.keytool.Main.main(cmd.split(" "));
106+
}
107+
108+
private static void signJarFile(KeyStore ks, String alias,
109+
String digestAlg, String inputFile, String outputFile)
110+
throws Exception {
111+
112+
JarSigner signer = new JarSigner.Builder(getEntry(ks, alias))
113+
.digestAlgorithm(digestAlg)
114+
.signerName(alias)
115+
.build();
116+
117+
try (ZipFile in = new ZipFile(inputFile);
118+
FileOutputStream out = new FileOutputStream(outputFile)) {
119+
signer.sign(in, out);
120+
}
121+
}
122+
123+
private static void checkThatJarIsSigned(String jarFile, boolean multi)
124+
throws Exception {
125+
126+
try (JarFile jf = new JarFile(jarFile, true)) {
127+
Enumeration<JarEntry> entries = jf.entries();
128+
while (entries.hasMoreElements()) {
129+
JarEntry entry = entries.nextElement();
130+
if (entry.isDirectory() || isSigningRelated(entry.getName())) {
131+
continue;
132+
}
133+
InputStream is = jf.getInputStream(entry);
134+
while (is.read() != -1);
135+
CodeSigner[] signers = entry.getCodeSigners();
136+
if (signers == null) {
137+
throw new Exception("JarEntry " + entry.getName() +
138+
" is not signed");
139+
} else if (multi) {
140+
if (entry.getName().equals(TESTFILE1) &&
141+
signers.length != 2) {
142+
throw new Exception("Unexpected number of signers " +
143+
"for " + entry.getName() + ": " + signers.length);
144+
} else if (entry.getName().equals(TESTFILE2) &&
145+
signers.length != 1) {
146+
throw new Exception("Unexpected number of signers " +
147+
"for " + entry.getName() + ": " + signers.length);
148+
}
149+
}
150+
}
151+
}
152+
}
153+
154+
private static boolean isSigningRelated(String name) {
155+
name = name.toUpperCase(Locale.ENGLISH);
156+
if (!name.startsWith("META-INF/")) {
157+
return false;
158+
}
159+
name = name.substring(9);
160+
if (name.indexOf('/') != -1) {
161+
return false;
162+
}
163+
return name.endsWith(".SF")
164+
|| name.endsWith(".DSA")
165+
|| name.endsWith(".RSA")
166+
|| name.endsWith(".EC")
167+
|| name.equals("MANIFEST.MF");
168+
}
169+
}

test/lib/jdk/test/lib/security/SecurityUtils.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,18 @@ public static void removeFromDisabledTlsAlgs(String... protocols) {
6161
List.<String>of(protocols));
6262
}
6363

64-
private static void removeFromDisabledAlgs(String prop, List<String> algs) {
64+
/**
65+
* Removes constraints that contain the specified constraint from the
66+
* specified security property. For example, List.of("SHA1") will remove
67+
* any constraint containing "SHA1".
68+
*/
69+
public static void removeFromDisabledAlgs(String prop,
70+
List<String> constraints) {
6571
String value = Security.getProperty(prop);
6672
value = Arrays.stream(value.split(","))
6773
.map(s -> s.trim())
68-
.filter(s -> !algs.contains(s))
74+
.filter(s -> constraints.stream()
75+
.allMatch(constraint -> !s.contains(constraint)))
6976
.collect(Collectors.joining(","));
7077
Security.setProperty(prop, value);
7178
}

0 commit comments

Comments
 (0)