FileDocCategorySizeDatePackage
JarVerifier.javaAPI DocAndroid 1.5 API19090Wed May 06 22:41:02 BST 2009java.util.jar

JarVerifier

public class JarVerifier extends Object
Non-public class used by {@link JarFile} and {@link JarInputStream} to manage the verification of signed JARs. {@code JarFile} and {@code JarInputStream} objects are expected to have a {@code JarVerifier} instance member which can be used to carry out the tasks associated with verifying a signed JAR. These tasks would typically include:
  • verification of all signed signature files
  • confirmation that all signed data was signed only by the party or parties specified in the signature block data
  • verification that the contents of all signature files (i.e. {@code .SF} files) agree with the JAR entries information found in the JAR manifest.

Fields Summary
private final String
jarName
private Manifest
man
private HashMap
metaEntries
private final Hashtable
signatures
private final Hashtable
certificates
private final Hashtable
verifiedEntries
byte[]
mainAttributesChunk
private static long
measureCount
private static long
averageTime
Constructors Summary
JarVerifier(String name)
Constructs and returns a new instance of {@code JarVerifier}.

param
name the name of the JAR file being verified.

        jarName = name;
    
Methods Summary
voidaddMetaEntry(java.lang.String name, byte[] buf)
Add a new meta entry to the internal collection of data held on each JAR entry in the {@code META-INF} directory including the manifest file itself. Files associated with the signing of a JAR would also be added to this collection.

param
name the name of the file located in the {@code META-INF} directory.
param
buf the file bytes for the file called {@code name}.
see
#removeMetaEntries()

        metaEntries.put(Util.toASCIIUpperCase(name), buf);
    
java.security.cert.Certificate[]getCertificates(java.lang.String name)
Returns all of the {@link java.security.cert.Certificate} instances that were used to verify the signature on the JAR entry called {@code name}.

param
name the name of a JAR entry.
return
an array of {@link java.security.cert.Certificate}.

        Certificate[] verifiedCerts = verifiedEntries.get(name);
        if (verifiedCerts == null) {
            return null;
        }
        return verifiedCerts.clone();
    
public static java.util.VectorgetSignerCertificates(java.lang.String signatureFileName, java.util.Map certificates)
Returns a {@code Vector} of all of the {@link java.security.cert.Certificate}s that are associated with the signing of the named signature file.

param
signatureFileName the name of a signature file.
param
certificates a {@code Map} of all of the certificate chains discovered so far while attempting to verify the JAR that contains the signature file {@code signatureFileName}. This object is previously set in the course of one or more calls to {@link #verifyJarSignatureFile(String, String, String, Map, Map)} where it was passed as the last argument.
return
all of the {@code Certificate} entries for the signer of the JAR whose actions led to the creation of the named signature file.

        Vector<Certificate> result = new Vector<Certificate>();
        Certificate[] certChain = certificates.get(signatureFileName);
        if (certChain != null) {
            for (Certificate element : certChain) {
                result.add(element);
            }
        }
        return result;
    
java.util.jar.JarVerifier$VerifierEntryinitEntry(java.lang.String name)
Invoked for each new JAR entry read operation from the input stream. This method constructs and returns a new {@link VerifierEntry} which contains the certificates used to sign the entry and its hash value as specified in the JAR MANIFEST format.

param
name the name of an entry in a JAR file which is not in the {@code META-INF} directory.
return
a new instance of {@link VerifierEntry} which can be used by callers as an {@link OutputStream}.
since
Android 1.0

        // If no manifest is present by the time an entry is found,
        // verification cannot occur. If no signature files have
        // been found, do not verify.
        if (man == null || signatures.size() == 0) {
            return null;
        }

        Attributes attributes = man.getAttributes(name);
        // entry has no digest
        if (attributes == null) {
            return null;
        }

        Vector<Certificate> certs = new Vector<Certificate>();
        Iterator<Map.Entry<String, HashMap<String, Attributes>>> it =
            signatures.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
            HashMap<String, Attributes> hm = entry.getValue();
            if (hm.get(name) != null) {
                // Found an entry for entry name in .SF file
                String signatureFile = entry.getKey();

                Vector<Certificate> newCerts = getSignerCertificates(
                        signatureFile, certificates);
                Iterator<Certificate> iter = newCerts.iterator();
                while (iter.hasNext()) {
                    certs.add(iter.next());
                }
            }
        }

        // entry is not signed
        if (certs.size() == 0) {
            return null;
        }
        Certificate[] certificatesArray = new Certificate[certs.size()];
        certs.toArray(certificatesArray);

        String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$
        if (algorithms == null) {
            algorithms = "SHA SHA1"; //$NON-NLS-1$
        }
        StringTokenizer tokens = new StringTokenizer(algorithms);
        while (tokens.hasMoreTokens()) {
            String algorithm = tokens.nextToken();
            String hash = attributes.getValue(algorithm + "-Digest"); //$NON-NLS-1$
            if (hash == null) {
                continue;
            }
            byte[] hashBytes;
            try {
                hashBytes = hash.getBytes("ISO8859_1"); //$NON-NLS-1$
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e.toString());
            }

            try {
                // BEGIN android-changed
                return new VerifierEntry(OpenSSLMessageDigestJDK.getInstance(algorithm),
                        hashBytes, certificatesArray);
                // END android-changed
            } catch (NoSuchAlgorithmException e) {
                // Ignored
            }
        }
        return null;
    
booleanisSignedJar()
Returns a {@code boolean} indication of whether or not the associated JAR file is signed.

return
{@code true} if the JAR is signed, {@code false} otherwise.

        return certificates.size() > 0;
    
synchronized booleanreadCertificates()
If the associated JAR file is signed, check on the validity of all of the known signatures.

return
{@code true} if the associated JAR is signed and an internal check verifies the validity of the signature(s). {@code false} if the associated JAR file has no entries at all in its {@code META-INF} directory. This situation is indicative of an invalid JAR file.

Will also return {@code true} if the JAR file is not signed.

throws
SecurityException if the JAR file is signed and it is determined that a signature block file contains an invalid signature for the corresponding signature file.
since
Android 1.0

        if (metaEntries == null) {
            return false;
        }
        Iterator<String> it = metaEntries.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            if (key.endsWith(".DSA") || key.endsWith(".RSA")) { //$NON-NLS-1$ //$NON-NLS-2$
                verifyCertificate(key);
                // Check for recursive class load
                if (metaEntries == null) {
                    return false;
                }
                it.remove();
            }
        }
        return true;
    
voidremoveMetaEntries()
Remove all entries from the internal collection of data held about each JAR entry in the {@code META-INF} directory.

see
#addMetaEntry(String, byte[])

        metaEntries = null;
    
voidsetManifest(java.util.jar.Manifest mf)
Associate this verifier with the specified {@link Manifest} object.

param
mf a {@code java.util.jar.Manifest} object.

        man = mf;
    
private booleanverify(java.util.jar.Attributes attributes, java.lang.String entry, byte[] data, boolean ignoreSecondEndline, boolean ignorable)

        String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$
        if (algorithms == null) {
            algorithms = "SHA SHA1"; //$NON-NLS-1$
        }
        StringTokenizer tokens = new StringTokenizer(algorithms);
        while (tokens.hasMoreTokens()) {
            String algorithm = tokens.nextToken();
            String hash = attributes.getValue(algorithm + entry);
            if (hash == null) {
                continue;
            }

            MessageDigest md;
            try {
                // BEGIN android-changed
                md = OpenSSLMessageDigestJDK.getInstance(algorithm);
                // END android-changed
            } catch (NoSuchAlgorithmException e) {
                continue;
            }
            if (ignoreSecondEndline && data[data.length - 1] == '\n"
                    && data[data.length - 2] == '\n") {
                md.update(data, 0, data.length - 1);
            } else {
                md.update(data, 0, data.length);
            }
            byte[] b = md.digest();
            byte[] hashBytes;
            try {
                hashBytes = hash.getBytes("ISO8859_1"); //$NON-NLS-1$
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e.toString());
            }
            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
        }
        return ignorable;
    
private voidverifyCertificate(java.lang.String certFile)

param
certFile

        // Found Digital Sig, .SF should already have been read
        String signatureFile = certFile.substring(0, certFile.lastIndexOf('."))
                + ".SF"; //$NON-NLS-1$
        byte[] sfBytes = metaEntries.get(signatureFile);
        if (sfBytes == null) {
            return;
        }

        byte[] sBlockBytes = metaEntries.get(certFile);
        try {
            Certificate[] signerCertChain = JarUtils.verifySignature(
                    new ByteArrayInputStream(sfBytes),
                    new ByteArrayInputStream(sBlockBytes));
            /*
             * Recursive call in loading security provider related class which
             * is in a signed JAR. 
             */
            if (null == metaEntries) {
                return;
            }
            if (signerCertChain != null) {
                certificates.put(signatureFile, signerCertChain);
            }
        } catch (IOException e) {
            return;
        } catch (GeneralSecurityException e) {
            /* [MSG "archive.30", "{0} failed verification of {1}"] */
            throw new SecurityException(
                    Messages.getString("archive.30", jarName, signatureFile)); //$NON-NLS-1$
        }

        // Verify manifest hash in .sf file
        Attributes attributes = new Attributes();
        HashMap<String, Attributes> hm = new HashMap<String, Attributes>();
        try {
            new InitManifest(new ByteArrayInputStream(sfBytes), attributes, hm,
                    null, "Signature-Version"); //$NON-NLS-1$
        } catch (IOException e) {
            return;
        }

        boolean createdBySigntool = false;
        String createdByValue = attributes.getValue("Created-By"); //$NON-NLS-1$
        if (createdByValue != null) {
            createdBySigntool = createdByValue.indexOf("signtool") != -1; //$NON-NLS-1$
        }

        // Use .SF to verify the mainAttributes of the manifest
        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
        // file, such as those created before java 1.5, then we ignore
        // such verification.
        // FIXME: The meaning of createdBySigntool
        if (mainAttributesChunk != null && !createdBySigntool) {
            String digestAttribute = "-Digest-Manifest-Main-Attributes"; //$NON-NLS-1$
            if (!verify(attributes, digestAttribute, mainAttributesChunk,
                    false, true)) {
                /* [MSG "archive.30", "{0} failed verification of {1}"] */
                throw new SecurityException(
                        Messages.getString("archive.30", jarName, signatureFile)); //$NON-NLS-1$
            }
        }

        byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
        if (manifest == null) {
            return;
        }
        // Use .SF to verify the whole manifest
        String digestAttribute = createdBySigntool ? "-Digest" //$NON-NLS-1$
                : "-Digest-Manifest"; //$NON-NLS-1$
        if (!verify(attributes, digestAttribute, manifest, false, false)) {
            Iterator<Map.Entry<String, Attributes>> it = hm.entrySet()
                    .iterator();
            while (it.hasNext()) {
                Map.Entry<String, Attributes> entry = it.next();
                byte[] chunk = man.getChunk(entry.getKey());
                if (chunk == null) {
                    return;
                }
                if (!verify(entry.getValue(), "-Digest", chunk, //$NON-NLS-1$
                        createdBySigntool, false)) {
                    /* [MSG "archive.31", "{0} has invalid digest for {1} in {2}"] */
                    throw new SecurityException(
                        Messages.getString("archive.31", //$NON-NLS-1$
                            new Object[] { signatureFile, entry.getKey(), jarName }));
                }
            }
        }
        metaEntries.put(signatureFile, null);
        signatures.put(signatureFile, hm);
    
voidverifySignatures(java.util.jar.JarVerifier$VerifierEntry entry, java.util.zip.ZipEntry zipEntry)
Verifies that the digests stored in the manifest match the decrypted digests from the .SF file. This indicates the validity of the signing, not the integrity of the file, as it's digest must be calculated and verified when its contents are read.

param
entry the {@link VerifierEntry} associated with the specified {@code zipEntry}.
param
zipEntry an entry in the JAR file
throws
SecurityException if the digest value stored in the manifest does not agree with the decrypted digest as recovered from the {@code .SF} file.
see
#initEntry(String)

        byte[] digest = entry.digest.digest();
        if (!MessageDigest.isEqual(digest, Base64.decode(entry.hash))) {
            /* [MSG "archive.31", "{0} has invalid digest for {1} in {2}"] */
            throw new SecurityException(Messages.getString("archive.31", new Object[] { //$NON-NLS-1$
                    JarFile.MANIFEST_NAME, zipEntry.getName(), jarName }));
        }
        verifiedEntries.put(zipEntry.getName(), entry.certificates);