8278851: Correct signer logic for jars signed with multiple digestalgs

Reviewed-by: coffeys, weijun
This commit is contained in:
Sean Mullan 2022-01-14 15:22:31 +00:00
parent 35734ad080
commit 61b8944327
3 changed files with 244 additions and 50 deletions
src/java.base/share/classes
java/util/jar
sun/security/util
test/jdk/jdk/security/jarsigner

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2022, 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
@ -96,6 +96,10 @@ class JarVerifier {
/** collect -DIGEST-MANIFEST values for deny list */
private List<Object> manifestDigests;
/* A cache mapping code signers to the algorithms used to digest jar
entries, and whether or not the algorithms are permitted. */
private Map<CodeSigner[], Map<String, Boolean>> signersToAlgs;
public JarVerifier(String name, byte[] rawBytes) {
manifestName = name;
manifestRawBytes = rawBytes;
@ -105,6 +109,7 @@ class JarVerifier {
pendingBlocks = new ArrayList<>();
baos = new ByteArrayOutputStream();
manifestDigests = new ArrayList<>();
signersToAlgs = new HashMap<>();
}
/**
@ -244,7 +249,8 @@ class JarVerifier {
if (!parsingBlockOrSF) {
JarEntry je = mev.getEntry();
if ((je != null) && (je.signers == null)) {
je.signers = mev.verify(verifiedSigners, sigFileSigners);
je.signers = mev.verify(verifiedSigners, sigFileSigners,
signersToAlgs);
je.certs = mapSignersToCertArray(je.signers);
}
} else {

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2022, 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
@ -192,7 +192,8 @@ public class ManifestEntryVerifier {
*
*/
public CodeSigner[] verify(Hashtable<String, CodeSigner[]> verifiedSigners,
Hashtable<String, CodeSigner[]> sigFileSigners)
Hashtable<String, CodeSigner[]> sigFileSigners,
Map<CodeSigner[], Map<String, Boolean>> signersToAlgs)
throws JarException
{
if (skip) {
@ -207,38 +208,60 @@ public class ManifestEntryVerifier {
return signers;
}
JarConstraintsParameters params =
getParams(verifiedSigners, sigFileSigners);
CodeSigner[] entrySigners = sigFileSigners.get(name);
Map<String, Boolean> algsPermittedStatus =
algsPermittedStatusForSigners(signersToAlgs, entrySigners);
// Flag that indicates if only disabled algorithms are used and jar
// entry should be treated as unsigned.
boolean disabledAlgs = true;
JarConstraintsParameters params = null;
for (int i=0; i < digests.size(); i++) {
MessageDigest digest = digests.get(i);
if (params != null) {
try {
params.setExtendedExceptionMsg(JarFile.MANIFEST_NAME,
name + " entry");
DisabledAlgorithmConstraints.jarConstraints()
.permits(digest.getAlgorithm(), params, false);
} catch (GeneralSecurityException e) {
if (debug != null) {
debug.println("Digest algorithm is restricted: " + e);
String digestAlg = digest.getAlgorithm();
// Check if this algorithm is permitted, skip if false.
if (algsPermittedStatus != null) {
Boolean permitted = algsPermittedStatus.get(digestAlg);
if (permitted == null) {
if (params == null) {
params = new JarConstraintsParameters(entrySigners);
}
return null;
if (!checkConstraints(digestAlg, params)) {
algsPermittedStatus.put(digestAlg, Boolean.FALSE);
continue;
} else {
algsPermittedStatus.put(digestAlg, Boolean.TRUE);
}
} else if (!permitted) {
continue;
}
}
// A non-disabled algorithm was used.
disabledAlgs = false;
byte [] manHash = manifestHashes.get(i);
byte [] theHash = digest.digest();
if (debug != null) {
debug.println("Manifest Entry: " +
name + " digest=" + digest.getAlgorithm());
name + " digest=" + digestAlg);
debug.println(" manifest " + HexFormat.of().formatHex(manHash));
debug.println(" computed " + HexFormat.of().formatHex(theHash));
debug.println();
}
if (!MessageDigest.isEqual(theHash, manHash))
throw new SecurityException(digest.getAlgorithm()+
if (!MessageDigest.isEqual(theHash, manHash)) {
throw new SecurityException(digestAlg +
" digest error for "+name);
}
}
// If there were only disabled algorithms used, return null and jar
// entry will be treated as unsigned.
if (disabledAlgs) {
return null;
}
// take it out of sigFileSigners and put it in verifiedSigners...
@ -249,40 +272,36 @@ public class ManifestEntryVerifier {
return signers;
}
/**
* Get constraints parameters for JAR. The constraints should be
* checked against all code signers. Returns the parameters,
* or null if the signers for this entry have already been checked
* or there are no signers for this entry.
*/
private JarConstraintsParameters getParams(
Map<String, CodeSigner[]> verifiedSigners,
Map<String, CodeSigner[]> sigFileSigners) {
// Gets the algorithms permitted status for the signers of this entry.
private static Map<String, Boolean> algsPermittedStatusForSigners(
Map<CodeSigner[], Map<String, Boolean>> signersToAlgs,
CodeSigner[] signers) {
if (signers != null) {
Map<String, Boolean> algs = signersToAlgs.get(signers);
// create new HashMap if absent
if (algs == null) {
algs = new HashMap<>();
signersToAlgs.put(signers, algs);
}
return algs;
}
return null;
}
// verifiedSigners is usually preloaded with the Manifest's signers.
// If verifiedSigners contains the Manifest, then it will have all of
// the signers of the JAR. But if it doesn't then we need to fallback
// and check verifiedSigners to see if the signers of this entry have
// been checked already.
if (verifiedSigners.containsKey(manifestFileName)) {
if (verifiedSigners.size() > 1) {
// this means we already checked it previously
return null;
} else {
return new JarConstraintsParameters(
verifiedSigners.get(manifestFileName));
}
} else {
// Checks the algorithm constraints against the signers of this entry.
private boolean checkConstraints(String algorithm,
JarConstraintsParameters params) {
try {
params.setExtendedExceptionMsg(JarFile.MANIFEST_NAME,
name + " entry");
DisabledAlgorithmConstraints.jarConstraints()
.permits(algorithm, params, false);
return true;
} catch (GeneralSecurityException e) {
if (debug != null) {
debug.println(manifestFileName + " not present in verifiedSigners");
}
CodeSigner[] signers = sigFileSigners.get(name);
if (signers == null || verifiedSigners.containsValue(signers)) {
return null;
} else {
return new JarConstraintsParameters(signers);
debug.println("Digest algorithm is restricted: " + e);
}
return false;
}
}
}

@ -0,0 +1,169 @@
/*
* Copyright (c) 2022, 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 8278851
* @summary Check that jar entry with at least one non-disabled digest
* algorithm in manifest is treated as signed
* @modules java.base/sun.security.tools.keytool
* @library /test/lib
* @build jdk.test.lib.util.JarUtils
* jdk.test.lib.security.SecurityUtils
* @run main/othervm JarWithOneNonDisabledDigestAlg
*/
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSigner;
import java.security.KeyStore;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
import jdk.security.jarsigner.JarSigner;
import jdk.test.lib.util.JarUtils;
import jdk.test.lib.security.SecurityUtils;
public class JarWithOneNonDisabledDigestAlg {
private static final String PASS = "changeit";
private static final String TESTFILE1 = "testfile1";
private static final String TESTFILE2 = "testfile2";
public static void main(String[] args) throws Exception {
SecurityUtils.removeFromDisabledAlgs("jdk.jar.disabledAlgorithms",
List.of("SHA1"));
Files.write(Path.of(TESTFILE1), TESTFILE1.getBytes());
JarUtils.createJarFile(Path.of("unsigned.jar"), Path.of("."),
Path.of(TESTFILE1));
genkeypair("-alias SHA1 -sigalg SHA1withRSA");
genkeypair("-alias SHA256 -sigalg SHA256withRSA");
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream fis = new FileInputStream("keystore")) {
ks.load(fis, PASS.toCharArray());
}
// Sign JAR twice with same signer but different digest algorithms
// so that each entry in manifest file contains two digest values.
signJarFile(ks, "SHA1", "MD5", "unsigned.jar", "signed.jar");
signJarFile(ks, "SHA1", "SHA1", "signed.jar", "signed2.jar");
checkThatJarIsSigned("signed2.jar", false);
// add another file to the JAR
Files.write(Path.of(TESTFILE2), "testFile2".getBytes());
JarUtils.updateJarFile(Path.of("signed2.jar"), Path.of("."),
Path.of(TESTFILE2));
// Sign again with different signer (SHA256) and SHA-1 digestalg.
// TESTFILE1 should have two signers and TESTFILE2 should have one
// signer.
signJarFile(ks, "SHA256", "SHA1", "signed2.jar", "multi-signed.jar");
checkThatJarIsSigned("multi-signed.jar", true);
}
private static KeyStore.PrivateKeyEntry getEntry(KeyStore ks, String alias)
throws Exception {
return (KeyStore.PrivateKeyEntry)
ks.getEntry(alias,
new KeyStore.PasswordProtection(PASS.toCharArray()));
}
private static void genkeypair(String cmd) throws Exception {
cmd = "-genkeypair -keystore keystore -storepass " + PASS +
" -keypass " + PASS + " -keyalg rsa -dname CN=Duke " + cmd;
sun.security.tools.keytool.Main.main(cmd.split(" "));
}
private static void signJarFile(KeyStore ks, String alias,
String digestAlg, String inputFile, String outputFile)
throws Exception {
JarSigner signer = new JarSigner.Builder(getEntry(ks, alias))
.digestAlgorithm(digestAlg)
.signerName(alias)
.build();
try (ZipFile in = new ZipFile(inputFile);
FileOutputStream out = new FileOutputStream(outputFile)) {
signer.sign(in, out);
}
}
private static void checkThatJarIsSigned(String jarFile, boolean multi)
throws Exception {
try (JarFile jf = new JarFile(jarFile, true)) {
Enumeration<JarEntry> entries = jf.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.isDirectory() || isSigningRelated(entry.getName())) {
continue;
}
InputStream is = jf.getInputStream(entry);
while (is.read() != -1);
CodeSigner[] signers = entry.getCodeSigners();
if (signers == null) {
throw new Exception("JarEntry " + entry.getName() +
" is not signed");
} else if (multi) {
if (entry.getName().equals(TESTFILE1) &&
signers.length != 2) {
throw new Exception("Unexpected number of signers " +
"for " + entry.getName() + ": " + signers.length);
} else if (entry.getName().equals(TESTFILE2) &&
signers.length != 1) {
throw new Exception("Unexpected number of signers " +
"for " + entry.getName() + ": " + signers.length);
}
}
}
}
}
private static boolean isSigningRelated(String name) {
name = name.toUpperCase(Locale.ENGLISH);
if (!name.startsWith("META-INF/")) {
return false;
}
name = name.substring(9);
if (name.indexOf('/') != -1) {
return false;
}
return name.endsWith(".SF")
|| name.endsWith(".DSA")
|| name.endsWith(".RSA")
|| name.endsWith(".EC")
|| name.equals("MANIFEST.MF");
}
}