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