/* * Copyright (c) 1997, 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. 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.ByteArrayInputStream; import java.io.IOException; import java.security.*; import java.security.cert.CertPath; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.*; import java.util.jar.Attributes; import java.util.jar.Manifest; import sun.security.action.GetIntegerAction; import sun.security.jca.Providers; import sun.security.pkcs.PKCS7; import sun.security.pkcs.SignerInfo; public class SignatureFileVerifier { /* Are we debugging ? */ private static final Debug debug = Debug.getInstance("jar"); private final ArrayList signerCache; private static final String ATTR_DIGEST = "-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS.toUpperCase(Locale.ENGLISH); /** the PKCS7 block for this .DSA/.RSA/.EC file */ private final PKCS7 block; /** the raw bytes of the .SF file */ private byte[] sfBytes; /** the name of the signature block file, uppercase and without * the extension (.DSA/.RSA/.EC) */ private final String name; /** the ManifestDigester */ private final ManifestDigester md; /** cache of created MessageDigest objects */ private HashMap createdDigests; /* workaround for parsing Netscape jars */ private boolean workaround = false; /* for generating certpath objects */ private final CertificateFactory certificateFactory; /** Algorithms that have been previously checked against disabled * constraints. */ private final Map permittedAlgs = new HashMap<>(); /** ConstraintsParameters for checking disabled algorithms */ private JarConstraintsParameters params; private static final String META_INF = "META-INF/"; // the maximum allowed size in bytes for the signature-related files public static final int MAX_SIG_FILE_SIZE = initializeMaxSigFileSize(); // The maximum size of array to allocate. Some VMs reserve some header words in an array. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * Create the named SignatureFileVerifier. * * @param name the name of the signature block file (.DSA/.RSA/.EC) * * @param rawBytes the raw bytes of the signature block file */ public SignatureFileVerifier(ArrayList signerCache, ManifestDigester md, String name, byte[] rawBytes) throws IOException, CertificateException { // new PKCS7() calls CertificateFactory.getInstance() // need to use local providers here, see Providers class Object obj = null; try { obj = Providers.startJarVerification(); block = new PKCS7(rawBytes); sfBytes = block.getContentInfo().getData(); certificateFactory = CertificateFactory.getInstance("X509"); } finally { Providers.stopJarVerification(obj); } this.name = name.substring(0, name.lastIndexOf('.')) .toUpperCase(Locale.ENGLISH); this.md = md; this.signerCache = signerCache; } /** * returns true if we need the .SF file */ public boolean needSignatureFileBytes() { return sfBytes == null; } /** * returns true if we need this .SF file. * * @param name the name of the .SF file without the extension * */ public boolean needSignatureFile(String name) { return this.name.equalsIgnoreCase(name); } /** * used to set the raw bytes of the .SF file when it * is external to the signature block file. */ public void setSignatureFile(byte[] sfBytes) { this.sfBytes = sfBytes; } /** * Utility method used by JarVerifier and JarSigner * to determine if a path is located directly in the * META-INF/ directory * * @param name the path name to check * @return true if the path resides in META-INF directly, ignoring case */ public static boolean isInMetaInf(String name) { return name.regionMatches(true, 0, META_INF, 0, META_INF.length()) && name.lastIndexOf('/') < META_INF.length(); } /** * Utility method used by JarVerifier and JarSigner * to determine the signature file names and PKCS7 block * files names that are supported * * @param s file name * @return true if the input file name is a supported * Signature File or PKCS7 block file name * @see #getBlockExtension(PrivateKey) */ public static boolean isBlockOrSF(String s) { // Note: keep this in sync with j.u.z.ZipFile.Source#isSignatureRelated // we currently only support DSA, RSA or EC PKCS7 blocks return s.endsWith(".SF") || s.endsWith(".DSA") || s.endsWith(".RSA") || s.endsWith(".EC"); } /** * Returns the signed JAR block file extension for a key. * * Returns "DSA" for unknown algorithms. This is safe because the * signature verification process does not require the extension to * match the key algorithm as long as it is "RSA", "DSA", or "EC." * * @param key the key used to sign the JAR file * @return the extension * @see #isBlockOrSF(String) */ public static String getBlockExtension(PrivateKey key) { return switch (key.getAlgorithm().toUpperCase(Locale.ENGLISH)) { case "RSA", "RSASSA-PSS" -> "RSA"; case "EC", "EDDSA", "ED25519", "ED448" -> "EC"; default -> "DSA"; }; } /** * Yet another utility method used by JarVerifier and JarSigner * to determine what files are signature related, which includes * the MANIFEST, SF files, known signature block files, and other * unknown signature related files (those starting with SIG- with * an optional [A-Z0-9]{1,3} extension right inside META-INF). * * @param name file name * @return true if the input file name is signature related */ public static boolean isSigningRelated(String name) { if (!isInMetaInf(name)) { return false; } name = name.toUpperCase(Locale.ENGLISH); if (isBlockOrSF(name) || name.equals("META-INF/MANIFEST.MF")) { return true; } else if (name.startsWith("SIG-", META_INF.length())) { // check filename extension // see https://docs.oracle.com/en/java/javase/19/docs/specs/jar/jar.html#digital-signatures // for what filename extensions are legal int extIndex = name.lastIndexOf('.'); if (extIndex != -1) { String ext = name.substring(extIndex + 1); // validate length first if (ext.length() > 3 || ext.length() < 1) { return false; } // then check chars, must be in [a-zA-Z0-9] per the jar spec for (int index = 0; index < ext.length(); index++) { char cc = ext.charAt(index); // chars are promoted to uppercase so skip lowercase checks if ((cc < 'A' || cc > 'Z') && (cc < '0' || cc > '9')) { return false; } } } return true; // no extension is OK } return false; } /** get digest from cache */ private MessageDigest getDigest(String algorithm) { if (createdDigests == null) createdDigests = new HashMap<>(); MessageDigest digest = createdDigests.get(algorithm); if (digest == null) { try { digest = MessageDigest.getInstance(algorithm); createdDigests.put(algorithm, digest); } catch (NoSuchAlgorithmException nsae) { // ignore } } return digest; } /** * process the signature block file. Goes through the .SF file * and adds code signers for each section where the .SF section * hash was verified against the Manifest section. * * */ public void process(Hashtable signers, List manifestDigests, String manifestName) throws IOException, SignatureException, NoSuchAlgorithmException, CertificateException { // calls Signature.getInstance() and MessageDigest.getInstance() // need to use local providers here, see Providers class Object obj = null; try { obj = Providers.startJarVerification(); processImpl(signers, manifestDigests, manifestName); } finally { Providers.stopJarVerification(obj); } } private void processImpl(Hashtable signers, List manifestDigests, String manifestName) throws IOException, SignatureException, NoSuchAlgorithmException, CertificateException { Manifest sf = new Manifest(); sf.read(new ByteArrayInputStream(sfBytes)); String version = sf.getMainAttributes().getValue(Attributes.Name.SIGNATURE_VERSION); if ((version == null) || !(version.equalsIgnoreCase("1.0"))) { // XXX: should this be an exception? // for now, we just ignore this signature file return; } SignerInfo[] infos = block.verify(sfBytes); if (infos == null) { throw new SecurityException("cannot verify signature block file " + name); } CodeSigner[] newSigners = getSigners(infos, block); // make sure we have something to do all this work for... if (newSigners == null) { return; } // check if any of the algorithms used to verify the SignerInfos // are disabled params = new JarConstraintsParameters(newSigners); Set notDisabledAlgorithms = SignerInfo.verifyAlgorithms(infos, params, name + " PKCS7"); // add the SignerInfo algorithms that are ok to the permittedAlgs map // so they are not checked again for (String algorithm : notDisabledAlgorithms) { permittedAlgs.put(algorithm, Boolean.TRUE); } Iterator> entries = sf.getEntries().entrySet().iterator(); // see if we can verify the whole manifest first boolean manifestSigned = verifyManifestHash(sf, md, manifestDigests); // verify manifest main attributes if (!manifestSigned && !verifyManifestMainAttrs(sf, md)) { throw new SecurityException ("Invalid signature file digest for Manifest main attributes"); } // go through each section in the signature file while(entries.hasNext()) { Map.Entry e = entries.next(); String name = e.getKey(); if (manifestSigned || (verifySection(e.getValue(), name, md))) { if (name.startsWith("./")) name = name.substring(2); if (name.startsWith("/")) name = name.substring(1); updateSigners(newSigners, signers, name); if (debug != null) { debug.println("processSignature signed name = "+name); } } else if (debug != null) { debug.println("processSignature unsigned name = "+name); } } // MANIFEST.MF is always regarded as signed updateSigners(newSigners, signers, manifestName); } /** * Check if algorithm is permitted using the permittedAlgs Map. * If the algorithm is not in the map, check against disabled algorithms and * store the result. If the algorithm is in the map use that result. * False is returned for weak algorithm, true for good algorithms. */ private boolean permittedCheck(String key, String algorithm) { Boolean permitted = permittedAlgs.get(algorithm); if (permitted == null) { try { params.setExtendedExceptionMsg(name + ".SF", key + " attribute"); DisabledAlgorithmConstraints .jarConstraints().permits(algorithm, params, false); } catch (GeneralSecurityException e) { permittedAlgs.put(algorithm, Boolean.FALSE); permittedAlgs.put(key.toUpperCase(Locale.ENGLISH), Boolean.FALSE); if (debug != null) { if (e.getMessage() != null) { debug.println(key + ": " + e.getMessage()); } else { debug.println("Debug info only. " + key + ": " + algorithm + " was disabled, no exception msg given."); e.printStackTrace(); } } return false; } permittedAlgs.put(algorithm, Boolean.TRUE); return true; } // Algorithm has already been checked, return the value from map. return permitted.booleanValue(); } /** * With a given header (*-DIGEST*), return a string that lists all the * algorithms associated with the header. * If there are none, return "Unknown Algorithm". */ String getWeakAlgorithms(String header) { String w = ""; try { for (String key : permittedAlgs.keySet()) { if (key.endsWith(header)) { w += key.substring(0, key.length() - header.length()) + " "; } } } catch (RuntimeException e) { w = "Unknown Algorithm(s). Error processing " + header + ". " + e.getMessage(); } // This means we have an error in finding weak algorithms, run in // debug mode to see permittedAlgs map's values. if (w.isEmpty()) { return "Unknown Algorithm(s)"; } return w; } /** * See if the whole manifest was signed. */ private boolean verifyManifestHash(Manifest sf, ManifestDigester md, List manifestDigests) throws SignatureException { Attributes mattr = sf.getMainAttributes(); boolean manifestSigned = false; // If only weak algorithms are used. boolean weakAlgs = true; // If a "*-DIGEST-MANIFEST" entry is found. boolean validEntry = false; // go through all the attributes and process *-Digest-Manifest entries for (Map.Entry se : mattr.entrySet()) { String key = se.getKey().toString(); if (key.toUpperCase(Locale.ENGLISH).endsWith("-DIGEST-MANIFEST")) { // 16 is length of "-Digest-Manifest" String algorithm = key.substring(0, key.length()-16); validEntry = true; // Check if this algorithm is permitted, skip if false. if (!permittedCheck(key, algorithm)) { continue; } // A non-weak algorithm was used, any weak algorithms found do // not need to be reported. weakAlgs = false; manifestDigests.add(key); manifestDigests.add(se.getValue()); MessageDigest digest = getDigest(algorithm); if (digest != null) { byte[] computedHash = md.manifestDigest(digest); byte[] expectedHash = Base64.getMimeDecoder().decode((String)se.getValue()); if (debug != null) { debug.println("Signature File: Manifest digest " + algorithm); debug.println( " sigfile " + HexFormat.of().formatHex(expectedHash)); debug.println( " computed " + HexFormat.of().formatHex(computedHash)); debug.println(); } if (MessageDigest.isEqual(computedHash, expectedHash)) { manifestSigned = true; } else { //XXX: we will continue and verify each section } } } } if (debug != null) { debug.println("PermittedAlgs mapping: "); for (String key : permittedAlgs.keySet()) { debug.println(key + " : " + permittedAlgs.get(key).toString()); } } // If there were only weak algorithms entries used, throw an exception. if (validEntry && weakAlgs) { throw new SignatureException("Manifest hash check failed " + "(DIGEST-MANIFEST). Disabled algorithm(s) used: " + getWeakAlgorithms("-DIGEST-MANIFEST")); } return manifestSigned; } private boolean verifyManifestMainAttrs(Manifest sf, ManifestDigester md) throws SignatureException { Attributes mattr = sf.getMainAttributes(); boolean attrsVerified = true; // If only weak algorithms are used. boolean weakAlgs = true; // If a ATTR_DIGEST entry is found. boolean validEntry = false; // go through all the attributes and process // digest entries for the manifest main attributes for (Map.Entry se : mattr.entrySet()) { String key = se.getKey().toString(); if (key.toUpperCase(Locale.ENGLISH).endsWith(ATTR_DIGEST)) { String algorithm = key.substring(0, key.length() - ATTR_DIGEST.length()); validEntry = true; // Check if this algorithm is permitted, skip if false. if (!permittedCheck(key, algorithm)) { continue; } // A non-weak algorithm was used, any weak algorithms found do // not need to be reported. weakAlgs = false; MessageDigest digest = getDigest(algorithm); if (digest != null) { ManifestDigester.Entry mde = md.getMainAttsEntry(false); if (mde == null) { throw new SignatureException("Manifest Main Attribute check " + "failed due to missing main attributes entry"); } byte[] computedHash = mde.digest(digest); byte[] expectedHash = Base64.getMimeDecoder().decode((String)se.getValue()); if (debug != null) { debug.println("Signature File: " + "Manifest Main Attributes digest " + digest.getAlgorithm()); debug.println( " sigfile " + HexFormat.of().formatHex(expectedHash)); debug.println( " computed " + HexFormat.of().formatHex(computedHash)); debug.println(); } if (MessageDigest.isEqual(computedHash, expectedHash)) { // good } else { // we will *not* continue and verify each section attrsVerified = false; if (debug != null) { debug.println("Verification of " + "Manifest main attributes failed"); debug.println(); } break; } } } } if (debug != null) { debug.println("PermittedAlgs mapping: "); for (String key : permittedAlgs.keySet()) { debug.println(key + " : " + permittedAlgs.get(key).toString()); } } // If there were only weak algorithms entries used, throw an exception. if (validEntry && weakAlgs) { throw new SignatureException("Manifest Main Attribute check " + "failed (" + ATTR_DIGEST + "). " + "Disabled algorithm(s) used: " + getWeakAlgorithms(ATTR_DIGEST)); } // this method returns 'true' if either: // . manifest main attributes were not signed, or // . manifest main attributes were signed and verified return attrsVerified; } /** * given the .SF digest header, and the data from the * section in the manifest, see if the hashes match. * if not, throw a SecurityException. * * @return true if all the -Digest headers verified * @exception SecurityException if the hash was not equal */ private boolean verifySection(Attributes sfAttr, String name, ManifestDigester md) throws SignatureException { boolean oneDigestVerified = false; ManifestDigester.Entry mde = md.get(name,block.isOldStyle()); // If only weak algorithms are used. boolean weakAlgs = true; // If a "*-DIGEST" entry is found. boolean validEntry = false; if (mde == null) { throw new SecurityException( "no manifest section for signature file entry "+name); } if (sfAttr != null) { //sun.security.util.HexDumpEncoder hex = new sun.security.util.HexDumpEncoder(); //hex.encodeBuffer(data, System.out); // go through all the attributes and process *-Digest entries for (Map.Entry se : sfAttr.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); validEntry = true; // Check if this algorithm is permitted, skip if false. if (!permittedCheck(key, algorithm)) { continue; } // A non-weak algorithm was used, any weak algorithms found do // not need to be reported. weakAlgs = false; MessageDigest digest = getDigest(algorithm); if (digest != null) { boolean ok = false; byte[] expected = Base64.getMimeDecoder().decode((String)se.getValue()); byte[] computed; if (workaround) { computed = mde.digestWorkaround(digest); } else { computed = mde.digest(digest); } if (debug != null) { debug.println("Signature Block File: " + name + " digest=" + digest.getAlgorithm()); debug.println(" expected " + HexFormat.of().formatHex(expected)); debug.println(" computed " + HexFormat.of().formatHex(computed)); debug.println(); } if (MessageDigest.isEqual(computed, expected)) { oneDigestVerified = true; ok = true; } else { // attempt to fallback to the workaround if (!workaround) { computed = mde.digestWorkaround(digest); if (MessageDigest.isEqual(computed, expected)) { if (debug != null) { debug.println(" re-computed " + HexFormat.of().formatHex(computed)); debug.println(); } workaround = true; oneDigestVerified = true; ok = true; } } } if (!ok){ throw new SecurityException("invalid " + digest.getAlgorithm() + " signature file digest for " + name); } } } } } if (debug != null) { debug.println("PermittedAlgs mapping: "); for (String key : permittedAlgs.keySet()) { debug.println(key + " : " + permittedAlgs.get(key).toString()); } } // If there were only weak algorithms entries used, throw an exception. if (validEntry && weakAlgs) { throw new SignatureException("Manifest Main Attribute check " + "failed (DIGEST). Disabled algorithm(s) used: " + getWeakAlgorithms("DIGEST")); } return oneDigestVerified; } /** * Given the PKCS7 block and SignerInfo[], create an array of * CodeSigner objects. We do this only *once* for a given * signature block file. */ private CodeSigner[] getSigners(SignerInfo[] infos, PKCS7 block) throws IOException, NoSuchAlgorithmException, SignatureException, CertificateException { ArrayList signers = null; for (int i = 0; i < infos.length; i++) { SignerInfo info = infos[i]; ArrayList chain = info.getCertificateChain(block); CertPath certChain = certificateFactory.generateCertPath(chain); if (signers == null) { signers = new ArrayList<>(); } // Append the new code signer. If timestamp is invalid, this // jar will be treated as unsigned. signers.add(new CodeSigner(certChain, info.getTimestamp())); if (debug != null) { debug.println("Signature Block Certificate: " + chain.get(0)); } } if (signers != null) { return signers.toArray(new CodeSigner[0]); } else { return null; } } // returns true if set contains signer static boolean contains(CodeSigner[] set, CodeSigner signer) { for (int i = 0; i < set.length; i++) { if (set[i].equals(signer)) return true; } return false; } // returns true if subset is a subset of set static boolean isSubSet(CodeSigner[] subset, CodeSigner[] set) { // check for the same object if (set == subset) return true; for (int i = 0; i < subset.length; i++) { if (!contains(set, subset[i])) return false; } return true; } /** * returns true if signer contains exactly the same code signers as * oldSigner and newSigner, false otherwise. oldSigner * is allowed to be null. */ static boolean matches(CodeSigner[] signers, CodeSigner[] oldSigners, CodeSigner[] newSigners) { // special case if ((oldSigners == null) && (signers == newSigners)) return true; // make sure all oldSigners are in signers if ((oldSigners != null) && !isSubSet(oldSigners, signers)) return false; // make sure all newSigners are in signers if (!isSubSet(newSigners, signers)) { return false; } // now make sure all the code signers in signers are // also in oldSigners or newSigners for (int i = 0; i < signers.length; i++) { boolean found = ((oldSigners != null) && contains(oldSigners, signers[i])) || contains(newSigners, signers[i]); if (!found) return false; } return true; } void updateSigners(CodeSigner[] newSigners, Hashtable signers, String name) { CodeSigner[] oldSigners = signers.get(name); // search through the cache for a match, go in reverse order // as we are more likely to find a match with the last one // added to the cache CodeSigner[] cachedSigners; for (int i = signerCache.size() - 1; i != -1; i--) { cachedSigners = signerCache.get(i); if (matches(cachedSigners, oldSigners, newSigners)) { signers.put(name, cachedSigners); return; } } if (oldSigners == null) { cachedSigners = newSigners; } else { cachedSigners = new CodeSigner[oldSigners.length + newSigners.length]; System.arraycopy(oldSigners, 0, cachedSigners, 0, oldSigners.length); System.arraycopy(newSigners, 0, cachedSigners, oldSigners.length, newSigners.length); } signerCache.add(cachedSigners); signers.put(name, cachedSigners); } private static int initializeMaxSigFileSize() { /* * System property "jdk.jar.maxSignatureFileSize" used to configure * the maximum allowed number of bytes for the signature-related files * in a JAR file. */ int tmp = GetIntegerAction.privilegedGetProperty( "jdk.jar.maxSignatureFileSize", 16000000); if (tmp < 0 || tmp > MAX_ARRAY_SIZE) { if (debug != null) { debug.println("The default signature file size of 16000000 bytes " + "will be used for the jdk.jar.maxSignatureFileSize " + "system property since the specified value " + "is out of range: " + tmp); } tmp = 16000000; } return tmp; } }