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 android.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.security.cert.X509Certificate;
     29 import java.util.ArrayList;
     30 import java.util.HashMap;
     31 import java.util.Hashtable;
     32 import java.util.Iterator;
     33 import java.util.List;
     34 import java.util.Locale;
     35 import java.util.Map;
     36 import java.util.Set;
     37 import java.util.StringTokenizer;
     38 import java.util.jar.Attributes;
     39 import java.util.jar.JarFile;
     40 import android.util.ArraySet;
     41 import android.util.apk.ApkSignatureSchemeV2Verifier;
     42 import libcore.io.Base64;
     43 import sun.security.jca.Providers;
     44 import sun.security.pkcs.PKCS7;
     45 import sun.security.pkcs.SignerInfo;
     46 
     47 /**
     48  * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
     49  * the verification of signed JARs. {@code JarFile} and {@code JarInputStream}
     50  * objects are expected to have a {@code JarVerifier} instance member which
     51  * can be used to carry out the tasks associated with verifying a signed JAR.
     52  * These tasks would typically include:
     53  * <ul>
     54  * <li>verification of all signed signature files
     55  * <li>confirmation that all signed data was signed only by the party or parties
     56  * specified in the signature block data
     57  * <li>verification that the contents of all signature files (i.e. {@code .SF}
     58  * files) agree with the JAR entries information found in the JAR manifest.
     59  * </ul>
     60  */
     61 class StrictJarVerifier {
     62     /**
     63      * List of accepted digest algorithms. This list is in order from most
     64      * preferred to least preferred.
     65      */
     66     private static final String[] DIGEST_ALGORITHMS = new String[] {
     67         "SHA-512",
     68         "SHA-384",
     69         "SHA-256",
     70         "SHA1",
     71     };
     72 
     73     private final String jarName;
     74     private final StrictJarManifest manifest;
     75     private final HashMap<String, byte[]> metaEntries;
     76     private final int mainAttributesEnd;
     77     private final boolean signatureSchemeRollbackProtectionsEnforced;
     78 
     79     private final Hashtable<String, HashMap<String, Attributes>> signatures =
     80             new Hashtable<String, HashMap<String, Attributes>>(5);
     81 
     82     private final Hashtable<String, Certificate[]> certificates =
     83             new Hashtable<String, Certificate[]>(5);
     84 
     85     private final Hashtable<String, Certificate[][]> verifiedEntries =
     86             new Hashtable<String, Certificate[][]>();
     87 
     88     /**
     89      * Stores and a hash and a message digest and verifies that massage digest
     90      * matches the hash.
     91      */
     92     static class VerifierEntry extends OutputStream {
     93 
     94         private final String name;
     95 
     96         private final MessageDigest digest;
     97 
     98         private final byte[] hash;
     99 
    100         private final Certificate[][] certChains;
    101 
    102         private final Hashtable<String, Certificate[][]> verifiedEntries;
    103 
    104         VerifierEntry(String name, MessageDigest digest, byte[] hash,
    105                 Certificate[][] certChains, Hashtable<String, Certificate[][]> verifedEntries) {
    106             this.name = name;
    107             this.digest = digest;
    108             this.hash = hash;
    109             this.certChains = certChains;
    110             this.verifiedEntries = verifedEntries;
    111         }
    112 
    113         /**
    114          * Updates a digest with one byte.
    115          */
    116         @Override
    117         public void write(int value) {
    118             digest.update((byte) value);
    119         }
    120 
    121         /**
    122          * Updates a digest with byte array.
    123          */
    124         @Override
    125         public void write(byte[] buf, int off, int nbytes) {
    126             digest.update(buf, off, nbytes);
    127         }
    128 
    129         /**
    130          * Verifies that the digests stored in the manifest match the decrypted
    131          * digests from the .SF file. This indicates the validity of the
    132          * signing, not the integrity of the file, as its digest must be
    133          * calculated and verified when its contents are read.
    134          *
    135          * @throws SecurityException
    136          *             if the digest value stored in the manifest does <i>not</i>
    137          *             agree with the decrypted digest as recovered from the
    138          *             <code>.SF</code> file.
    139          */
    140         void verify() {
    141             byte[] d = digest.digest();
    142             if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
    143                 throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
    144             }
    145             verifiedEntries.put(name, certChains);
    146         }
    147     }
    148 
    149     private static SecurityException invalidDigest(String signatureFile, String name,
    150             String jarName) {
    151         throw new SecurityException(signatureFile + " has invalid digest for " + name +
    152                 " in " + jarName);
    153     }
    154 
    155     private static SecurityException failedVerification(String jarName, String signatureFile) {
    156         throw new SecurityException(jarName + " failed verification of " + signatureFile);
    157     }
    158 
    159     private static SecurityException failedVerification(String jarName, String signatureFile,
    160                                                       Throwable e) {
    161         throw new SecurityException(jarName + " failed verification of " + signatureFile, e);
    162     }
    163 
    164 
    165     /**
    166      * Constructs and returns a new instance of {@code JarVerifier}.
    167      *
    168      * @param name
    169      *            the name of the JAR file being verified.
    170      *
    171      * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
    172      *        stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
    173      *        {@code false} to ignore any such protections.
    174      */
    175     StrictJarVerifier(String name, StrictJarManifest manifest,
    176         HashMap<String, byte[]> metaEntries, boolean signatureSchemeRollbackProtectionsEnforced) {
    177         jarName = name;
    178         this.manifest = manifest;
    179         this.metaEntries = metaEntries;
    180         this.mainAttributesEnd = manifest.getMainAttributesEnd();
    181         this.signatureSchemeRollbackProtectionsEnforced =
    182                 signatureSchemeRollbackProtectionsEnforced;
    183     }
    184 
    185     /**
    186      * Invoked for each new JAR entry read operation from the input
    187      * stream. This method constructs and returns a new {@link VerifierEntry}
    188      * which contains the certificates used to sign the entry and its hash value
    189      * as specified in the JAR MANIFEST format.
    190      *
    191      * @param name
    192      *            the name of an entry in a JAR file which is <b>not</b> in the
    193      *            {@code META-INF} directory.
    194      * @return a new instance of {@link VerifierEntry} which can be used by
    195      *         callers as an {@link OutputStream}.
    196      */
    197     VerifierEntry initEntry(String name) {
    198         // If no manifest is present by the time an entry is found,
    199         // verification cannot occur. If no signature files have
    200         // been found, do not verify.
    201         if (manifest == null || signatures.isEmpty()) {
    202             return null;
    203         }
    204 
    205         Attributes attributes = manifest.getAttributes(name);
    206         // entry has no digest
    207         if (attributes == null) {
    208             return null;
    209         }
    210 
    211         ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
    212         Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
    213         while (it.hasNext()) {
    214             Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
    215             HashMap<String, Attributes> hm = entry.getValue();
    216             if (hm.get(name) != null) {
    217                 // Found an entry for entry name in .SF file
    218                 String signatureFile = entry.getKey();
    219                 Certificate[] certChain = certificates.get(signatureFile);
    220                 if (certChain != null) {
    221                     certChains.add(certChain);
    222                 }
    223             }
    224         }
    225 
    226         // entry is not signed
    227         if (certChains.isEmpty()) {
    228             return null;
    229         }
    230         Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);
    231 
    232         for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
    233             final String algorithm = DIGEST_ALGORITHMS[i];
    234             final String hash = attributes.getValue(algorithm + "-Digest");
    235             if (hash == null) {
    236                 continue;
    237             }
    238             byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
    239 
    240             try {
    241                 return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
    242                         certChainsArray, verifiedEntries);
    243             } catch (NoSuchAlgorithmException ignored) {
    244             }
    245         }
    246         return null;
    247     }
    248 
    249     /**
    250      * Add a new meta entry to the internal collection of data held on each JAR
    251      * entry in the {@code META-INF} directory including the manifest
    252      * file itself. Files associated with the signing of a JAR would also be
    253      * added to this collection.
    254      *
    255      * @param name
    256      *            the name of the file located in the {@code META-INF}
    257      *            directory.
    258      * @param buf
    259      *            the file bytes for the file called {@code name}.
    260      * @see #removeMetaEntries()
    261      */
    262     void addMetaEntry(String name, byte[] buf) {
    263         metaEntries.put(name.toUpperCase(Locale.US), buf);
    264     }
    265 
    266     /**
    267      * If the associated JAR file is signed, check on the validity of all of the
    268      * known signatures.
    269      *
    270      * @return {@code true} if the associated JAR is signed and an internal
    271      *         check verifies the validity of the signature(s). {@code false} if
    272      *         the associated JAR file has no entries at all in its {@code
    273      *         META-INF} directory. This situation is indicative of an invalid
    274      *         JAR file.
    275      *         <p>
    276      *         Will also return {@code true} if the JAR file is <i>not</i>
    277      *         signed.
    278      * @throws SecurityException
    279      *             if the JAR file is signed and it is determined that a
    280      *             signature block file contains an invalid signature for the
    281      *             corresponding signature file.
    282      */
    283     synchronized boolean readCertificates() {
    284         if (metaEntries.isEmpty()) {
    285             return false;
    286         }
    287 
    288         Iterator<String> it = metaEntries.keySet().iterator();
    289         while (it.hasNext()) {
    290             String key = it.next();
    291             if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
    292                 verifyCertificate(key);
    293                 it.remove();
    294             }
    295         }
    296         return true;
    297     }
    298 
    299    /**
    300      * Verifies that the signature computed from {@code sfBytes} matches
    301      * that specified in {@code blockBytes} (which is a PKCS7 block). Returns
    302      * certificates listed in the PKCS7 block. Throws a {@code GeneralSecurityException}
    303      * if something goes wrong during verification.
    304      */
    305     static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)
    306         throws GeneralSecurityException {
    307 
    308         Object obj = null;
    309         try {
    310 
    311             obj = Providers.startJarVerification();
    312             PKCS7 block = new PKCS7(blockBytes);
    313             SignerInfo[] verifiedSignerInfos = block.verify(sfBytes);
    314             if ((verifiedSignerInfos == null) || (verifiedSignerInfos.length == 0)) {
    315                 throw new GeneralSecurityException(
    316                         "Failed to verify signature: no verified SignerInfos");
    317             }
    318             // Ignore any SignerInfo other than the first one, to be compatible with older Android
    319             // platforms which have been doing this for years. See
    320             // libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java
    321             // verifySignature method of older platforms.
    322             SignerInfo verifiedSignerInfo = verifiedSignerInfos[0];
    323             List<X509Certificate> verifiedSignerCertChain =
    324                     verifiedSignerInfo.getCertificateChain(block);
    325             if (verifiedSignerCertChain == null) {
    326                 // Should never happen
    327                 throw new GeneralSecurityException(
    328                     "Failed to find verified SignerInfo certificate chain");
    329             } else if (verifiedSignerCertChain.isEmpty()) {
    330                 // Should never happen
    331                 throw new GeneralSecurityException(
    332                     "Verified SignerInfo certificate chain is emtpy");
    333             }
    334             return verifiedSignerCertChain.toArray(
    335                     new X509Certificate[verifiedSignerCertChain.size()]);
    336         } catch (IOException e) {
    337             throw new GeneralSecurityException("IO exception verifying jar cert", e);
    338         } finally {
    339             Providers.stopJarVerification(obj);
    340         }
    341     }
    342 
    343     /**
    344      * @param certFile
    345      */
    346     private void verifyCertificate(String certFile) {
    347         // Found Digital Sig, .SF should already have been read
    348         String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
    349         byte[] sfBytes = metaEntries.get(signatureFile);
    350         if (sfBytes == null) {
    351             return;
    352         }
    353 
    354         byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
    355         // Manifest entry is required for any verifications.
    356         if (manifestBytes == null) {
    357             return;
    358         }
    359 
    360         byte[] sBlockBytes = metaEntries.get(certFile);
    361         try {
    362             Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes);
    363             if (signerCertChain != null) {
    364                 certificates.put(signatureFile, signerCertChain);
    365             }
    366         } catch (GeneralSecurityException e) {
    367           throw failedVerification(jarName, signatureFile, e);
    368         }
    369 
    370         // Verify manifest hash in .sf file
    371         Attributes attributes = new Attributes();
    372         HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
    373         try {
    374             StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes);
    375             im.readEntries(entries, null);
    376         } catch (IOException e) {
    377             return;
    378         }
    379 
    380         // If requested, check whether APK Signature Scheme v2 signature was stripped.
    381         if (signatureSchemeRollbackProtectionsEnforced) {
    382             String apkSignatureSchemeIdList =
    383                     attributes.getValue(
    384                             ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
    385             if (apkSignatureSchemeIdList != null) {
    386                 // This field contains a comma-separated list of APK signature scheme IDs which
    387                 // were used to sign this APK. If an ID is known to us, it means signatures of that
    388                 // scheme were stripped from the APK because otherwise we wouldn't have fallen back
    389                 // to verifying the APK using the JAR signature scheme.
    390                 boolean v2SignatureGenerated = false;
    391                 StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
    392                 while (tokenizer.hasMoreTokens()) {
    393                     String idText = tokenizer.nextToken().trim();
    394                     if (idText.isEmpty()) {
    395                         continue;
    396                     }
    397                     int id;
    398                     try {
    399                         id = Integer.parseInt(idText);
    400                     } catch (Exception ignored) {
    401                         continue;
    402                     }
    403                     if (id == ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
    404                         // This APK was supposed to be signed with APK Signature Scheme v2 but no
    405                         // such signature was found.
    406                         v2SignatureGenerated = true;
    407                         break;
    408                     }
    409                 }
    410 
    411                 if (v2SignatureGenerated) {
    412                     throw new SecurityException(signatureFile + " indicates " + jarName
    413                             + " is signed using APK Signature Scheme v2, but no such signature was"
    414                             + " found. Signature stripped?");
    415                 }
    416             }
    417         }
    418 
    419         // Do we actually have any signatures to look at?
    420         if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
    421             return;
    422         }
    423 
    424         boolean createdBySigntool = false;
    425         String createdBy = attributes.getValue("Created-By");
    426         if (createdBy != null) {
    427             createdBySigntool = createdBy.indexOf("signtool") != -1;
    428         }
    429 
    430         // Use .SF to verify the mainAttributes of the manifest
    431         // If there is no -Digest-Manifest-Main-Attributes entry in .SF
    432         // file, such as those created before java 1.5, then we ignore
    433         // such verification.
    434         if (mainAttributesEnd > 0 && !createdBySigntool) {
    435             String digestAttribute = "-Digest-Manifest-Main-Attributes";
    436             if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
    437                 throw failedVerification(jarName, signatureFile);
    438             }
    439         }
    440 
    441         // Use .SF to verify the whole manifest.
    442         String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
    443         if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
    444             Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
    445             while (it.hasNext()) {
    446                 Map.Entry<String, Attributes> entry = it.next();
    447                 StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
    448                 if (chunk == null) {
    449                     return;
    450                 }
    451                 if (!verify(entry.getValue(), "-Digest", manifestBytes,
    452                         chunk.start, chunk.end, createdBySigntool, false)) {
    453                     throw invalidDigest(signatureFile, entry.getKey(), jarName);
    454                 }
    455             }
    456         }
    457         metaEntries.put(signatureFile, null);
    458         signatures.put(signatureFile, entries);
    459     }
    460 
    461     /**
    462      * Returns a <code>boolean</code> indication of whether or not the
    463      * associated jar file is signed.
    464      *
    465      * @return {@code true} if the JAR is signed, {@code false}
    466      *         otherwise.
    467      */
    468     boolean isSignedJar() {
    469         return certificates.size() > 0;
    470     }
    471 
    472     private boolean verify(Attributes attributes, String entry, byte[] data,
    473             int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
    474         for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
    475             String algorithm = DIGEST_ALGORITHMS[i];
    476             String hash = attributes.getValue(algorithm + entry);
    477             if (hash == null) {
    478                 continue;
    479             }
    480 
    481             MessageDigest md;
    482             try {
    483                 md = MessageDigest.getInstance(algorithm);
    484             } catch (NoSuchAlgorithmException e) {
    485                 continue;
    486             }
    487             if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
    488                 md.update(data, start, end - 1 - start);
    489             } else {
    490                 md.update(data, start, end - start);
    491             }
    492             byte[] b = md.digest();
    493             byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
    494             return MessageDigest.isEqual(b, Base64.decode(hashBytes));
    495         }
    496         return ignorable;
    497     }
    498 
    499     /**
    500      * Returns all of the {@link java.security.cert.Certificate} chains that
    501      * were used to verify the signature on the JAR entry called
    502      * {@code name}. Callers must not modify the returned arrays.
    503      *
    504      * @param name
    505      *            the name of a JAR entry.
    506      * @return an array of {@link java.security.cert.Certificate} chains.
    507      */
    508     Certificate[][] getCertificateChains(String name) {
    509         return verifiedEntries.get(name);
    510     }
    511 
    512     /**
    513      * Remove all entries from the internal collection of data held about each
    514      * JAR entry in the {@code META-INF} directory.
    515      */
    516     void removeMetaEntries() {
    517         metaEntries.clear();
    518     }
    519 }
    520