jdk/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java
changeset 25859 3317bb8137f4
parent 24969 afa6934dd8e8
child 30033 b9c86c17164a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java	Sun Aug 17 15:54:13 2014 +0100
@@ -0,0 +1,656 @@
+/*
+ * Copyright (c) 1997, 2013, 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.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.*;
+import java.io.*;
+import java.util.*;
+import java.util.jar.*;
+
+import sun.security.pkcs.*;
+import java.util.Base64;
+
+import sun.security.jca.Providers;
+
+public class SignatureFileVerifier {
+
+    /* Are we debugging ? */
+    private static final Debug debug = Debug.getInstance("jar");
+
+    /* cache of CodeSigner objects */
+    private ArrayList<CodeSigner[]> 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 PKCS7 block;
+
+    /** the raw bytes of the .SF file */
+    private byte sfBytes[];
+
+    /** the name of the signature block file, uppercased and without
+     *  the extension (.DSA/.RSA/.EC)
+     */
+    private String name;
+
+    /** the ManifestDigester */
+    private ManifestDigester md;
+
+    /** cache of created MessageDigest objects */
+    private HashMap<String, MessageDigest> createdDigests;
+
+    /* workaround for parsing Netscape jars  */
+    private boolean workaround = false;
+
+    /* for generating certpath objects */
+    private CertificateFactory certificateFactory = null;
+
+    /**
+     * 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<CodeSigner[]> 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 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
+     */
+    public static boolean isBlockOrSF(String s) {
+        // we currently only support DSA and RSA PKCS7 blocks
+        if (s.endsWith(".SF") || s.endsWith(".DSA") ||
+                s.endsWith(".RSA") || s.endsWith(".EC")) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 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 s file name
+     * @return true if the input file name is signature related
+     */
+    public 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;
+        }
+        if (isBlockOrSF(name) || name.equals("MANIFEST.MF")) {
+            return true;
+        } else if (name.startsWith("SIG-")) {
+            // check filename extension
+            // see http://docs.oracle.com/javase/7/docs/technotes/guides/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<String, MessageDigest>();
+
+        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<String, CodeSigner[]> signers,
+            List<Object> manifestDigests)
+        throws IOException, SignatureException, NoSuchAlgorithmException,
+            JarException, 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);
+        } finally {
+            Providers.stopJarVerification(obj);
+        }
+
+    }
+
+    private void processImpl(Hashtable<String, CodeSigner[]> signers,
+            List<Object> manifestDigests)
+        throws IOException, SignatureException, NoSuchAlgorithmException,
+            JarException, 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;
+
+        Iterator<Map.Entry<String,Attributes>> 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<String,Attributes> 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, JarFile.MANIFEST_NAME);
+    }
+
+    /**
+     * See if the whole manifest was signed.
+     */
+    private boolean verifyManifestHash(Manifest sf,
+                                       ManifestDigester md,
+                                       List<Object> manifestDigests)
+         throws IOException
+    {
+        Attributes mattr = sf.getMainAttributes();
+        boolean manifestSigned = false;
+
+        // go through all the attributes and process *-Digest-Manifest entries
+        for (Map.Entry<Object,Object> 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);
+
+                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 " +
+                                          digest.getAlgorithm());
+                     debug.println( "  sigfile  " + toHex(expectedHash));
+                     debug.println( "  computed " + toHex(computedHash));
+                     debug.println();
+                    }
+
+                    if (MessageDigest.isEqual(computedHash,
+                                              expectedHash)) {
+                        manifestSigned = true;
+                    } else {
+                        //XXX: we will continue and verify each section
+                    }
+                }
+            }
+        }
+        return manifestSigned;
+    }
+
+    private boolean verifyManifestMainAttrs(Manifest sf,
+                                        ManifestDigester md)
+         throws IOException
+    {
+        Attributes mattr = sf.getMainAttributes();
+        boolean attrsVerified = true;
+
+        // go through all the attributes and process
+        // digest entries for the manifest main attributes
+        for (Map.Entry<Object,Object> 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());
+
+                MessageDigest digest = getDigest(algorithm);
+                if (digest != null) {
+                    ManifestDigester.Entry mde =
+                        md.get(ManifestDigester.MF_MAIN_ATTRS, false);
+                    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  " + toHex(expectedHash));
+                     debug.println( "  computed " + toHex(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;
+                    }
+                }
+            }
+        }
+
+        // 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 IOException
+    {
+        boolean oneDigestVerified = false;
+        ManifestDigester.Entry mde = md.get(name,block.isOldStyle());
+
+        if (mde == null) {
+            throw new SecurityException(
+                  "no manifiest section for signature file entry "+name);
+        }
+
+        if (sfAttr != null) {
+
+            //sun.misc.HexDumpEncoder hex = new sun.misc.HexDumpEncoder();
+            //hex.encodeBuffer(data, System.out);
+
+            // go through all the attributes and process *-Digest entries
+            for (Map.Entry<Object,Object> 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);
+
+                    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 " + toHex(expected));
+                          debug.println("  computed " + toHex(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 " + toHex(computed));
+                                       debug.println();
+                                   }
+                                   workaround = true;
+                                   oneDigestVerified = true;
+                                   ok = true;
+                               }
+                            }
+                        }
+                        if (!ok){
+                            throw new SecurityException("invalid " +
+                                       digest.getAlgorithm() +
+                                       " signature file digest for " + name);
+                        }
+                    }
+                }
+            }
+        }
+        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<CodeSigner> signers = null;
+
+        for (int i = 0; i < infos.length; i++) {
+
+            SignerInfo info = infos[i];
+            ArrayList<X509Certificate> chain = info.getCertificateChain(block);
+            CertPath certChain = certificateFactory.generateCertPath(chain);
+            if (signers == null) {
+                signers = new ArrayList<CodeSigner>();
+            }
+            // Append the new code signer
+            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[signers.size()]);
+        } else {
+            return null;
+        }
+    }
+
+    // for the toHex function
+    private static final char[] hexc =
+            {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
+    /**
+     * convert a byte array to a hex string for debugging purposes
+     * @param data the binary data to be converted to a hex string
+     * @return an ASCII hex string
+     */
+
+    static String toHex(byte[] data) {
+
+        StringBuilder sb = new StringBuilder(data.length*2);
+
+        for (int i=0; i<data.length; i++) {
+            sb.append(hexc[(data[i] >>4) & 0x0f]);
+            sb.append(hexc[data[i] & 0x0f]);
+        }
+        return sb.toString();
+    }
+
+    // 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;
+
+        boolean match;
+        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;
+
+        boolean match;
+
+        // 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<String, CodeSigner[]> 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);
+    }
+}