/* * 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 * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * 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. */ package sun.security.util; import java.io.IOException; import java.security.*; import java.util.*; import java.util.jar.*; import sun.security.jca.Providers; /** * This class is used to verify each entry in a jar file with its * manifest value. */ public class ManifestEntryVerifier { private static final Debug debug = Debug.getInstance("jar"); /** * Holder class to lazily load Sun provider. NOTE: if * Providers.getSunProvider returned a cached provider, we could avoid the * need for caching the provider with this holder class; we should try to * revisit this in JDK 8. */ private static class SunProviderHolder { private static final Provider instance = Providers.getSunProvider(); } /** the created digest objects */ HashMap createdDigests; /** the digests in use for a given entry*/ ArrayList digests; /** the manifest hashes for the digests in use */ ArrayList manifestHashes; private String name = null; private final String manifestFileName; // never null private final Manifest man; private boolean skip = true; private JarEntry entry; private CodeSigner[] signers = null; /** * Create a new ManifestEntryVerifier object. */ public ManifestEntryVerifier(Manifest man, String manifestFileName) { createdDigests = new HashMap<>(11); digests = new ArrayList<>(); manifestHashes = new ArrayList<>(); this.manifestFileName = manifestFileName; this.man = man; } /** * Find the hashes in the * manifest for this entry, save them, and set the MessageDigest * objects to calculate the hashes on the fly. If name is * null it signifies that update/verify should ignore this entry. */ public void setEntry(String name, JarEntry entry) throws IOException { digests.clear(); manifestHashes.clear(); this.name = name; this.entry = entry; skip = true; signers = null; if (man == null || name == null) { return; } /* get the headers from the manifest for this entry */ /* if there aren't any, we can't verify any digests for this entry */ skip = false; Attributes attr = man.getAttributes(name); if (attr == null) { // ugh. we should be able to remove this at some point. // there are broken jars floating around with ./name and /name // in the manifest, and "name" in the zip/jar file. attr = man.getAttributes("./"+name); if (attr == null) { attr = man.getAttributes("/"+name); if (attr == null) return; } } for (Map.Entry se : attr.entrySet()) { String key = se.getKey().toString(); if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST")) { // 7 is length of "-Digest" String algorithm = key.substring(0, key.length()-7); MessageDigest digest = createdDigests.get(algorithm); if (digest == null) { try { digest = MessageDigest.getInstance (algorithm, SunProviderHolder.instance); createdDigests.put(algorithm, digest); } catch (NoSuchAlgorithmException nsae) { // ignore } } if (digest != null) { digest.reset(); digests.add(digest); manifestHashes.add( Base64.getMimeDecoder().decode((String)se.getValue())); } } } } /** * update the digests for the digests we are interested in */ public void update(byte buffer) { if (skip) return; for (int i=0; i < digests.size(); i++) { digests.get(i).update(buffer); } } /** * update the digests for the digests we are interested in */ public void update(byte[] buffer, int off, int len) { if (skip) return; for (int i=0; i < digests.size(); i++) { digests.get(i).update(buffer, off, len); } } /** * get the JarEntry for this object */ public JarEntry getEntry() { return entry; } /** * go through all the digests, calculating the final digest * and comparing it to the one in the manifest. If this is * the first time we have verified this object, remove its * code signers from sigFileSigners and place in verifiedSigners. * */ public CodeSigner[] verify(Hashtable verifiedSigners, Hashtable sigFileSigners, Map> signersToAlgs) throws JarException { if (skip) { return null; } if (digests.isEmpty()) { throw new SecurityException("digest missing for " + name); } if (signers != null) { return signers; } CodeSigner[] entrySigners = sigFileSigners.get(name); Map 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); 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); } 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=" + 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(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... signers = sigFileSigners.remove(name); if (signers != null) { verifiedSigners.put(name, signers); } return signers; } // Gets the algorithms permitted status for the signers of this entry. private static Map algsPermittedStatusForSigners( Map> signersToAlgs, CodeSigner[] signers) { if (signers != null) { Map algs = signersToAlgs.get(signers); // create new HashMap if absent if (algs == null) { algs = new HashMap<>(); signersToAlgs.put(signers, algs); } return algs; } return null; } // 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("Digest algorithm is restricted: " + e); } return false; } } }