Home | History | Annotate | Download | only in jar
      1 /*
      2  * Licensed to the Apache Software Foundation (ASF) under one or more
      3  * contributor license agreements.  See the NOTICE file distributed with
      4  * this work for additional information regarding copyright ownership.
      5  * The ASF licenses this file to You under the Apache License, Version 2.0
      6  * (the "License"); you may not use this file except in compliance with
      7  * the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package java.util.jar;
     19 
     20 import java.io.ByteArrayInputStream;
     21 import java.io.IOException;
     22 import java.io.OutputStream;
     23 import java.nio.charset.Charsets;
     24 import java.security.GeneralSecurityException;
     25 import java.security.MessageDigest;
     26 import java.security.NoSuchAlgorithmException;
     27 import java.security.cert.Certificate;
     28 import java.util.ArrayList;
     29 import java.util.HashMap;
     30 import java.util.Hashtable;
     31 import java.util.Iterator;
     32 import java.util.Locale;
     33 import java.util.Map;
     34 import java.util.StringTokenizer;
     35 import java.util.Vector;
     36 import libcore.io.Base64;
     37 import org.apache.harmony.security.utils.JarUtils;
     38 
     39 /**
     40  * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
     41  * the verification of signed JARs. {@code JarFile} and {@code JarInputStream}
     42  * objects are expected to have a {@code JarVerifier} instance member which
     43  * can be used to carry out the tasks associated with verifying a signed JAR.
     44  * These tasks would typically include:
     45  * <ul>
     46  * <li>verification of all signed signature files
     47  * <li>confirmation that all signed data was signed only by the party or parties
     48  * specified in the signature block data
     49  * <li>verification that the contents of all signature files (i.e. {@code .SF}
     50  * files) agree with the JAR entries information found in the JAR manifest.
     51  * </ul>
     52  */
     53 class JarVerifier {
     54 
     55     private final String jarName;
     56 
     57     private Manifest man;
     58 
     59     private HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(5);
     60 
     61     private final Hashtable<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>(
     62             5);
     63 
     64     private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>(
     65             5);
     66 
     67     private final Hashtable<String, Certificate[]> verifiedEntries = new Hashtable<String, Certificate[]>();
     68 
     69     int mainAttributesEnd;
     70 
     71     /**
     72      * Stores and a hash and a message digest and verifies that massage digest
     73      * matches the hash.
     74      */
     75     class VerifierEntry extends OutputStream {
     76 
     77         private String name;
     78 
     79         private MessageDigest digest;
     80 
     81         private byte[] hash;
     82 
     83         private Certificate[] certificates;
     84 
     85         VerifierEntry(String name, MessageDigest digest, byte[] hash,
     86                 Certificate[] certificates) {
     87             this.name = name;
     88             this.digest = digest;
     89             this.hash = hash;
     90             this.certificates = certificates;
     91         }
     92 
     93         /**
     94          * Updates a digest with one byte.
     95          */
     96         @Override
     97         public void write(int value) {
     98             digest.update((byte) value);
     99         }
    100 
    101         /**
    102          * Updates a digest with byte array.
    103          */
    104         @Override
    105         public void write(byte[] buf, int off, int nbytes) {
    106             digest.update(buf, off, nbytes);
    107         }
    108 
    109         /**
    110          * Verifies that the digests stored in the manifest match the decrypted
    111          * digests from the .SF file. This indicates the validity of the
    112          * signing, not the integrity of the file, as it's digest must be
    113          * calculated and verified when its contents are read.
    114          *
    115          * @throws SecurityException
    116          *             if the digest value stored in the manifest does <i>not</i>
    117          *             agree with the decrypted digest as recovered from the
    118          *             <code>.SF</code> file.
    119          */
    120         void verify() {
    121             byte[] d = digest.digest();
    122             if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
    123                 throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName);
    124             }
    125             verifiedEntries.put(name, certificates);
    126         }
    127 
    128     }
    129 
    130     private SecurityException invalidDigest(String signatureFile, String name, String jarName) {
    131         throw new SecurityException(signatureFile + " has invalid digest for " + name +
    132                 " in " + jarName);
    133     }
    134 
    135     private SecurityException failedVerification(String jarName, String signatureFile) {
    136         throw new SecurityException(jarName + " failed verification of " + signatureFile);
    137     }
    138 
    139     /**
    140      * Constructs and returns a new instance of {@code JarVerifier}.
    141      *
    142      * @param name
    143      *            the name of the JAR file being verified.
    144      */
    145     JarVerifier(String name) {
    146         jarName = name;
    147     }
    148 
    149     /**
    150      * Invoked for each new JAR entry read operation from the input
    151      * stream. This method constructs and returns a new {@link VerifierEntry}
    152      * which contains the certificates used to sign the entry and its hash value
    153      * as specified in the JAR MANIFEST format.
    154      *
    155      * @param name
    156      *            the name of an entry in a JAR file which is <b>not</b> in the
    157      *            {@code META-INF} directory.
    158      * @return a new instance of {@link VerifierEntry} which can be used by
    159      *         callers as an {@link OutputStream}.
    160      */
    161     VerifierEntry initEntry(String name) {
    162         // If no manifest is present by the time an entry is found,
    163         // verification cannot occur. If no signature files have
    164         // been found, do not verify.
    165         if (man == null || signatures.size() == 0) {
    166             return null;
    167         }
    168 
    169         Attributes attributes = man.getAttributes(name);
    170         // entry has no digest
    171         if (attributes == null) {
    172             return null;
    173         }
    174 
    175         ArrayList<Certificate> certs = new ArrayList<Certificate>();
    176         Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
    177         while (it.hasNext()) {
    178             Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
    179             HashMap<String, Attributes> hm = entry.getValue();
    180             if (hm.get(name) != null) {
    181                 // Found an entry for entry name in .SF file
    182                 String signatureFile = entry.getKey();
    183                 certs.addAll(getSignerCertificates(signatureFile, certificates));
    184             }
    185         }
    186 
    187         // entry is not signed
    188         if (certs.isEmpty()) {
    189             return null;
    190         }
    191         Certificate[] certificatesArray = certs.toArray(new Certificate[certs.size()]);
    192 
    193         String algorithms = attributes.getValue("Digest-Algorithms");
    194         if (algorithms == null) {
    195             algorithms = "SHA SHA1";
    196         }
    197         StringTokenizer tokens = new StringTokenizer(algorithms);
    198         while (tokens.hasMoreTokens()) {
    199             String algorithm = tokens.nextToken();
    200             String hash = attributes.getValue(algorithm + "-Digest");
    201             if (hash == null) {
    202                 continue;
    203             }
    204             byte[] hashBytes = hash.getBytes(Charsets.ISO_8859_1);
    205 
    206             try {
    207                 return new VerifierEntry(name, MessageDigest
    208                         .getInstance(algorithm), hashBytes, certificatesArray);
    209             } catch (NoSuchAlgorithmException e) {
    210                 // ignored
    211             }
    212         }
    213         return null;
    214     }
    215 
    216     /**
    217      * Add a new meta entry to the internal collection of data held on each JAR
    218      * entry in the {@code META-INF} directory including the manifest
    219      * file itself. Files associated with the signing of a JAR would also be
    220      * added to this collection.
    221      *
    222      * @param name
    223      *            the name of the file located in the {@code META-INF}
    224      *            directory.
    225      * @param buf
    226      *            the file bytes for the file called {@code name}.
    227      * @see #removeMetaEntries()
    228      */
    229     void addMetaEntry(String name, byte[] buf) {
    230         metaEntries.put(name.toUpperCase(Locale.US), buf);
    231     }
    232 
    233     /**
    234      * If the associated JAR file is signed, check on the validity of all of the
    235      * known signatures.
    236      *
    237      * @return {@code true} if the associated JAR is signed and an internal
    238      *         check verifies the validity of the signature(s). {@code false} if
    239      *         the associated JAR file has no entries at all in its {@code
    240      *         META-INF} directory. This situation is indicative of an invalid
    241      *         JAR file.
    242      *         <p>
    243      *         Will also return {@code true} if the JAR file is <i>not</i>
    244      *         signed.
    245      * @throws SecurityException
    246      *             if the JAR file is signed and it is determined that a
    247      *             signature block file contains an invalid signature for the
    248      *             corresponding signature file.
    249      */
    250     synchronized boolean readCertificates() {
    251         if (metaEntries == null) {
    252             return false;
    253         }
    254         Iterator<String> it = metaEntries.keySet().iterator();
    255         while (it.hasNext()) {
    256             String key = it.next();
    257             if (key.endsWith(".DSA") || key.endsWith(".RSA")) {
    258                 verifyCertificate(key);
    259                 // Check for recursive class load
    260                 if (metaEntries == null) {
    261                     return false;
    262                 }
    263                 it.remove();
    264             }
    265         }
    266         return true;
    267     }
    268 
    269     /**
    270      * @param certFile
    271      */
    272     private void verifyCertificate(String certFile) {
    273         // Found Digital Sig, .SF should already have been read
    274         String signatureFile = certFile.substring(0, certFile.lastIndexOf('.'))
    275                 + ".SF";
    276         byte[] sfBytes = metaEntries.get(signatureFile);
    277         if (sfBytes == null) {
    278             return;
    279         }
    280 
    281         byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
    282         // Manifest entry is required for any verifications.
    283         if (manifest == null) {
    284             return;
    285         }
    286 
    287         byte[] sBlockBytes = metaEntries.get(certFile);
    288         try {
    289             Certificate[] signerCertChain = JarUtils.verifySignature(
    290                     new ByteArrayInputStream(sfBytes),
    291                     new ByteArrayInputStream(sBlockBytes));
    292             /*
    293              * Recursive call in loading security provider related class which
    294              * is in a signed JAR.
    295              */
    296             if (metaEntries == null) {
    297                 return;
    298             }
    299             if (signerCertChain != null) {
    300                 certificates.put(signatureFile, signerCertChain);
    301             }
    302         } catch (IOException e) {
    303             return;
    304         } catch (GeneralSecurityException e) {
    305             throw failedVerification(jarName, signatureFile);
    306         }
    307 
    308         // Verify manifest hash in .sf file
    309         Attributes attributes = new Attributes();
    310         HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
    311         try {
    312             InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION);
    313             im.initEntries(entries, null);
    314         } catch (IOException e) {
    315             return;
    316         }
    317 
    318         boolean createdBySigntool = false;
    319         String createdBy = attributes.getValue("Created-By");
    320         if (createdBy != null) {
    321             createdBySigntool = createdBy.indexOf("signtool") != -1;
    322         }
    323 
    324         // Use .SF to verify the mainAttributes of the manifest
    325         // If there is no -Digest-Manifest-Main-Attributes entry in .SF
    326         // file, such as those created before java 1.5, then we ignore
    327         // such verification.
    328         if (mainAttributesEnd > 0 && !createdBySigntool) {
    329             String digestAttribute = "-Digest-Manifest-Main-Attributes";
    330             if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) {
    331                 throw failedVerification(jarName, signatureFile);
    332             }
    333         }
    334 
    335         // Use .SF to verify the whole manifest.
    336         String digestAttribute = createdBySigntool ? "-Digest"
    337                 : "-Digest-Manifest";
    338         if (!verify(attributes, digestAttribute, manifest, 0, manifest.length,
    339                 false, false)) {
    340             Iterator<Map.Entry<String, Attributes>> it = entries.entrySet()
    341                     .iterator();
    342             while (it.hasNext()) {
    343                 Map.Entry<String, Attributes> entry = it.next();
    344                 Manifest.Chunk chunk = man.getChunk(entry.getKey());
    345                 if (chunk == null) {
    346                     return;
    347                 }
    348                 if (!verify(entry.getValue(), "-Digest", manifest,
    349                         chunk.start, chunk.end, createdBySigntool, false)) {
    350                     throw invalidDigest(signatureFile, entry.getKey(), jarName);
    351                 }
    352             }
    353         }
    354         metaEntries.put(signatureFile, null);
    355         signatures.put(signatureFile, entries);
    356     }
    357 
    358     /**
    359      * Associate this verifier with the specified {@link Manifest} object.
    360      *
    361      * @param mf
    362      *            a {@code java.util.jar.Manifest} object.
    363      */
    364     void setManifest(Manifest mf) {
    365         man = mf;
    366     }
    367 
    368     /**
    369      * Returns a <code>boolean</code> indication of whether or not the
    370      * associated jar file is signed.
    371      *
    372      * @return {@code true} if the JAR is signed, {@code false}
    373      *         otherwise.
    374      */
    375     boolean isSignedJar() {
    376         return certificates.size() > 0;
    377     }
    378 
    379     private boolean verify(Attributes attributes, String entry, byte[] data,
    380             int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
    381         String algorithms = attributes.getValue("Digest-Algorithms");
    382         if (algorithms == null) {
    383             algorithms = "SHA SHA1";
    384         }
    385         StringTokenizer tokens = new StringTokenizer(algorithms);
    386         while (tokens.hasMoreTokens()) {
    387             String algorithm = tokens.nextToken();
    388             String hash = attributes.getValue(algorithm + entry);
    389             if (hash == null) {
    390                 continue;
    391             }
    392 
    393             MessageDigest md;
    394             try {
    395                 md = MessageDigest.getInstance(algorithm);
    396             } catch (NoSuchAlgorithmException e) {
    397                 continue;
    398             }
    399             if (ignoreSecondEndline && data[end - 1] == '\n'
    400                     && data[end - 2] == '\n') {
    401                 md.update(data, start, end - 1 - start);
    402             } else {
    403                 md.update(data, start, end - start);
    404             }
    405             byte[] b = md.digest();
    406             byte[] hashBytes = hash.getBytes(Charsets.ISO_8859_1);
    407             return MessageDigest.isEqual(b, Base64.decode(hashBytes));
    408         }
    409         return ignorable;
    410     }
    411 
    412     /**
    413      * Returns all of the {@link java.security.cert.Certificate} instances that
    414      * were used to verify the signature on the JAR entry called
    415      * {@code name}.
    416      *
    417      * @param name
    418      *            the name of a JAR entry.
    419      * @return an array of {@link java.security.cert.Certificate}.
    420      */
    421     Certificate[] getCertificates(String name) {
    422         Certificate[] verifiedCerts = verifiedEntries.get(name);
    423         if (verifiedCerts == null) {
    424             return null;
    425         }
    426         return verifiedCerts.clone();
    427     }
    428 
    429     /**
    430      * Remove all entries from the internal collection of data held about each
    431      * JAR entry in the {@code META-INF} directory.
    432      *
    433      * @see #addMetaEntry(String, byte[])
    434      */
    435     void removeMetaEntries() {
    436         metaEntries = null;
    437     }
    438 
    439     /**
    440      * Returns a {@code Vector} of all of the
    441      * {@link java.security.cert.Certificate}s that are associated with the
    442      * signing of the named signature file.
    443      *
    444      * @param signatureFileName
    445      *            the name of a signature file.
    446      * @param certificates
    447      *            a {@code Map} of all of the certificate chains discovered so
    448      *            far while attempting to verify the JAR that contains the
    449      *            signature file {@code signatureFileName}. This object is
    450      *            previously set in the course of one or more calls to
    451      *            {@link #verifyJarSignatureFile(String, String, String, Map, Map)}
    452      *            where it was passed as the last argument.
    453      * @return all of the {@code Certificate} entries for the signer of the JAR
    454      *         whose actions led to the creation of the named signature file.
    455      */
    456     public static Vector<Certificate> getSignerCertificates(
    457             String signatureFileName, Map<String, Certificate[]> certificates) {
    458         Vector<Certificate> result = new Vector<Certificate>();
    459         Certificate[] certChain = certificates.get(signatureFileName);
    460         if (certChain != null) {
    461             for (Certificate element : certChain) {
    462                 result.add(element);
    463             }
    464         }
    465         return result;
    466     }
    467 }
    468