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