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