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.StandardCharsets;
     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(StandardCharsets.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('.')) + ".SF";
    280         byte[] sfBytes = metaEntries.get(signatureFile);
    281         if (sfBytes == null) {
    282             return;
    283         }
    284 
    285         byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
    286         // Manifest entry is required for any verifications.
    287         if (manifest == null) {
    288             return;
    289         }
    290 
    291         byte[] sBlockBytes = metaEntries.get(certFile);
    292         try {
    293             Certificate[] signerCertChain = JarUtils.verifySignature(
    294                     new ByteArrayInputStream(sfBytes),
    295                     new ByteArrayInputStream(sBlockBytes));
    296             /*
    297              * Recursive call in loading security provider related class which
    298              * is in a signed JAR.
    299              */
    300             if (metaEntries == null) {
    301                 return;
    302             }
    303             if (signerCertChain != null) {
    304                 certificates.put(signatureFile, signerCertChain);
    305             }
    306         } catch (IOException e) {
    307             return;
    308         } catch (GeneralSecurityException e) {
    309             throw failedVerification(jarName, signatureFile);
    310         }
    311 
    312         // Verify manifest hash in .sf file
    313         Attributes attributes = new Attributes();
    314         HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
    315         try {
    316             ManifestReader im = new ManifestReader(sfBytes, attributes);
    317             im.readEntries(entries, null);
    318         } catch (IOException e) {
    319             return;
    320         }
    321 
    322         // Do we actually have any signatures to look at?
    323         if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
    324             return;
    325         }
    326 
    327         boolean createdBySigntool = false;
    328         String createdBy = attributes.getValue("Created-By");
    329         if (createdBy != null) {
    330             createdBySigntool = createdBy.indexOf("signtool") != -1;
    331         }
    332 
    333         // Use .SF to verify the mainAttributes of the manifest
    334         // If there is no -Digest-Manifest-Main-Attributes entry in .SF
    335         // file, such as those created before java 1.5, then we ignore
    336         // such verification.
    337         if (mainAttributesEnd > 0 && !createdBySigntool) {
    338             String digestAttribute = "-Digest-Manifest-Main-Attributes";
    339             if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) {
    340                 throw failedVerification(jarName, signatureFile);
    341             }
    342         }
    343 
    344         // Use .SF to verify the whole manifest.
    345         String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
    346         if (!verify(attributes, digestAttribute, manifest, 0, manifest.length, false, false)) {
    347             Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
    348             while (it.hasNext()) {
    349                 Map.Entry<String, Attributes> entry = it.next();
    350                 Manifest.Chunk chunk = man.getChunk(entry.getKey());
    351                 if (chunk == null) {
    352                     return;
    353                 }
    354                 if (!verify(entry.getValue(), "-Digest", manifest,
    355                         chunk.start, chunk.end, createdBySigntool, false)) {
    356                     throw invalidDigest(signatureFile, entry.getKey(), jarName);
    357                 }
    358             }
    359         }
    360         metaEntries.put(signatureFile, null);
    361         signatures.put(signatureFile, entries);
    362     }
    363 
    364     /**
    365      * Associate this verifier with the specified {@link Manifest} object.
    366      *
    367      * @param mf
    368      *            a {@code java.util.jar.Manifest} object.
    369      */
    370     void setManifest(Manifest mf) {
    371         man = mf;
    372     }
    373 
    374     /**
    375      * Returns a <code>boolean</code> indication of whether or not the
    376      * associated jar file is signed.
    377      *
    378      * @return {@code true} if the JAR is signed, {@code false}
    379      *         otherwise.
    380      */
    381     boolean isSignedJar() {
    382         return certificates.size() > 0;
    383     }
    384 
    385     private boolean verify(Attributes attributes, String entry, byte[] data,
    386             int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
    387         for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
    388             String algorithm = DIGEST_ALGORITHMS[i];
    389             String hash = attributes.getValue(algorithm + entry);
    390             if (hash == null) {
    391                 continue;
    392             }
    393 
    394             MessageDigest md;
    395             try {
    396                 md = MessageDigest.getInstance(algorithm);
    397             } catch (NoSuchAlgorithmException e) {
    398                 continue;
    399             }
    400             if (ignoreSecondEndline && data[end - 1] == '\n' && 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(StandardCharsets.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