• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.apksig.internal.apk.v1;
18 
19 import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels;
20 import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm;
21 import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm;
22 import static com.android.apksig.internal.x509.Certificate.findCertificate;
23 import static com.android.apksig.internal.x509.Certificate.parseCertificates;
24 
25 import com.android.apksig.ApkVerifier.Issue;
26 import com.android.apksig.ApkVerifier.IssueWithParams;
27 import com.android.apksig.apk.ApkFormatException;
28 import com.android.apksig.apk.ApkUtils;
29 import com.android.apksig.internal.asn1.Asn1BerParser;
30 import com.android.apksig.internal.asn1.Asn1Class;
31 import com.android.apksig.internal.asn1.Asn1DecodingException;
32 import com.android.apksig.internal.asn1.Asn1Field;
33 import com.android.apksig.internal.asn1.Asn1OpaqueObject;
34 import com.android.apksig.internal.asn1.Asn1Type;
35 import com.android.apksig.internal.jar.ManifestParser;
36 import com.android.apksig.internal.oid.OidConstants;
37 import com.android.apksig.internal.pkcs7.Attribute;
38 import com.android.apksig.internal.pkcs7.ContentInfo;
39 import com.android.apksig.internal.pkcs7.Pkcs7Constants;
40 import com.android.apksig.internal.pkcs7.Pkcs7DecodingException;
41 import com.android.apksig.internal.pkcs7.SignedData;
42 import com.android.apksig.internal.pkcs7.SignerInfo;
43 import com.android.apksig.internal.util.AndroidSdkVersion;
44 import com.android.apksig.internal.util.ByteBufferUtils;
45 import com.android.apksig.internal.util.InclusiveIntRange;
46 import com.android.apksig.internal.util.Pair;
47 import com.android.apksig.internal.zip.CentralDirectoryRecord;
48 import com.android.apksig.internal.zip.LocalFileRecord;
49 import com.android.apksig.internal.zip.ZipUtils;
50 import com.android.apksig.util.DataSinks;
51 import com.android.apksig.util.DataSource;
52 import com.android.apksig.zip.ZipFormatException;
53 
54 import java.io.IOException;
55 import java.nio.ByteBuffer;
56 import java.security.InvalidKeyException;
57 import java.security.MessageDigest;
58 import java.security.NoSuchAlgorithmException;
59 import java.security.Principal;
60 import java.security.Signature;
61 import java.security.SignatureException;
62 import java.security.cert.CertificateException;
63 import java.security.cert.X509Certificate;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.Base64;
67 import java.util.Base64.Decoder;
68 import java.util.Collection;
69 import java.util.Collections;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.List;
73 import java.util.Locale;
74 import java.util.Map;
75 import java.util.Set;
76 import java.util.StringTokenizer;
77 import java.util.jar.Attributes;
78 
79 /**
80  * APK verifier which uses JAR signing (aka v1 signing scheme).
81  *
82  * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
83  */
84 public abstract class V1SchemeVerifier {
V1SchemeVerifier()85     private V1SchemeVerifier() {}
86 
87     /**
88      * Verifies the provided APK's JAR signatures and returns the result of verification. APK is
89      * considered verified only if {@link Result#verified} is {@code true}. If verification fails,
90      * the result will contain errors -- see {@link Result#getErrors()}.
91      *
92      * <p>Verification succeeds iff the APK's JAR signatures are expected to verify on all Android
93      * platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. If the APK's signature
94      * is expected to not verify on any of the specified platform versions, this method returns a
95      * result with one or more errors and whose {@code Result.verified == false}, or this method
96      * throws an exception.
97      *
98      * @throws ApkFormatException if the APK is malformed
99      * @throws IOException if an I/O error occurs when reading the APK
100      * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a
101      *         required cryptographic algorithm implementation is missing
102      */
verify( DataSource apk, ApkUtils.ZipSections apkSections, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion)103     public static Result verify(
104             DataSource apk,
105             ApkUtils.ZipSections apkSections,
106             Map<Integer, String> supportedApkSigSchemeNames,
107             Set<Integer> foundApkSigSchemeIds,
108             int minSdkVersion,
109             int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException {
110         if (minSdkVersion > maxSdkVersion) {
111             throw new IllegalArgumentException(
112                     "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion
113                             + ")");
114         }
115 
116         Result result = new Result();
117 
118         // Parse the ZIP Central Directory and check that there are no entries with duplicate names.
119         List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections);
120         Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result);
121         if (result.containsErrors()) {
122             return result;
123         }
124 
125         // Verify JAR signature(s).
126         Signers.verify(
127                 apk,
128                 apkSections.getZipCentralDirectoryOffset(),
129                 cdRecords,
130                 cdEntryNames,
131                 supportedApkSigSchemeNames,
132                 foundApkSigSchemeIds,
133                 minSdkVersion,
134                 maxSdkVersion,
135                 result);
136 
137         return result;
138     }
139 
140     /**
141      * Returns the set of entry names and reports any duplicate entry names in the {@code result}
142      * as errors.
143      */
checkForDuplicateEntries( List<CentralDirectoryRecord> cdRecords, Result result)144     private static Set<String> checkForDuplicateEntries(
145             List<CentralDirectoryRecord> cdRecords, Result result) {
146         Set<String> cdEntryNames = new HashSet<>(cdRecords.size());
147         Set<String> duplicateCdEntryNames = null;
148         for (CentralDirectoryRecord cdRecord : cdRecords) {
149             String entryName = cdRecord.getName();
150             if (!cdEntryNames.add(entryName)) {
151                 // This is an error. Report this once per duplicate name.
152                 if (duplicateCdEntryNames == null) {
153                     duplicateCdEntryNames = new HashSet<>();
154                 }
155                 if (duplicateCdEntryNames.add(entryName)) {
156                     result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName);
157                 }
158             }
159         }
160         return cdEntryNames;
161     }
162 
163     /**
164     * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section
165     * representation and a mapping between entry name and its manifest section representation.
166     *
167     * @param manifestBytes raw representation of Manifest.MF
168     * @param cdEntryNames expected set of entry names
169     * @param result object to keep track of errors that happened during the parsing
170     * @return a pair of main entry manifest section representation and a mapping between entry name
171     *     and its manifest section representation
172     */
parseManifest( byte[] manifestBytes, Set<String> cdEntryNames, Result result)173     public static Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> parseManifest(
174             byte[] manifestBytes, Set<String> cdEntryNames, Result result) {
175         ManifestParser manifest = new ManifestParser(manifestBytes);
176         ManifestParser.Section manifestMainSection = manifest.readSection();
177         List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections();
178         Map<String, ManifestParser.Section> entryNameToManifestSection =
179                 new HashMap<>(manifestIndividualSections.size());
180         int manifestSectionNumber = 0;
181         for (ManifestParser.Section manifestSection : manifestIndividualSections) {
182             manifestSectionNumber++;
183             String entryName = manifestSection.getName();
184             if (entryName == null) {
185                 result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber);
186                 continue;
187             }
188             if (entryNameToManifestSection.put(entryName, manifestSection) != null) {
189                 result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName);
190                 continue;
191             }
192             if (!cdEntryNames.contains(entryName)) {
193                 result.addError(
194                         Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName);
195                 continue;
196             }
197         }
198         return Pair.of(manifestMainSection, entryNameToManifestSection);
199     }
200 
201     /**
202      * All JAR signers of an APK.
203      */
204     private static class Signers {
205 
206         /**
207          * Verifies JAR signatures of the provided APK and populates the provided result container
208          * with errors, warnings, and information about signers. The APK is considered verified if
209          * the {@link Result#verified} is {@code true}.
210          */
verify( DataSource apk, long cdStartOffset, List<CentralDirectoryRecord> cdRecords, Set<String> cdEntryNames, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion, Result result)211         private static void verify(
212                 DataSource apk,
213                 long cdStartOffset,
214                 List<CentralDirectoryRecord> cdRecords,
215                 Set<String> cdEntryNames,
216                 Map<Integer, String> supportedApkSigSchemeNames,
217                 Set<Integer> foundApkSigSchemeIds,
218                 int minSdkVersion,
219                 int maxSdkVersion,
220                 Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
221 
222             // Find JAR manifest and signature block files.
223             CentralDirectoryRecord manifestEntry = null;
224             Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1);
225             List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1);
226             for (CentralDirectoryRecord cdRecord : cdRecords) {
227                 String entryName = cdRecord.getName();
228                 if (!entryName.startsWith("META-INF/")) {
229                     continue;
230                 }
231                 if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(
232                         entryName))) {
233                     manifestEntry = cdRecord;
234                     continue;
235                 }
236                 if (entryName.endsWith(".SF")) {
237                     sigFileEntries.put(entryName, cdRecord);
238                     continue;
239                 }
240                 if ((entryName.endsWith(".RSA"))
241                         || (entryName.endsWith(".DSA"))
242                         || (entryName.endsWith(".EC"))) {
243                     sigBlockEntries.add(cdRecord);
244                     continue;
245                 }
246             }
247             if (manifestEntry == null) {
248                 result.addError(Issue.JAR_SIG_NO_MANIFEST);
249                 return;
250             }
251 
252             // Parse the JAR manifest and check that all JAR entries it references exist in the APK.
253             byte[] manifestBytes;
254             try {
255                 manifestBytes =
256                         LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset);
257             } catch (ZipFormatException e) {
258                 throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e);
259             }
260 
261             Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> manifestSections =
262                     parseManifest(manifestBytes, cdEntryNames, result);
263 
264             if (result.containsErrors()) {
265                 return;
266             }
267 
268             ManifestParser.Section manifestMainSection = manifestSections.getFirst();
269             Map<String, ManifestParser.Section> entryNameToManifestSection =
270                     manifestSections.getSecond();
271 
272             // STATE OF AFFAIRS:
273             // * All JAR entries listed in JAR manifest are present in the APK.
274 
275             // Identify signers
276             List<Signer> signers = new ArrayList<>(sigBlockEntries.size());
277             for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) {
278                 String sigBlockEntryName = sigBlockEntry.getName();
279                 int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.');
280                 if (extensionDelimiterIndex == -1) {
281                     throw new RuntimeException(
282                             "Signature block file name does not contain extension: "
283                                     + sigBlockEntryName);
284                 }
285                 String sigFileEntryName =
286                         sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF";
287                 CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName);
288                 if (sigFileEntry == null) {
289                     result.addWarning(
290                             Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName);
291                     continue;
292                 }
293                 String signerName = sigBlockEntryName.substring("META-INF/".length());
294                 Result.SignerInfo signerInfo =
295                         new Result.SignerInfo(
296                                 signerName, sigBlockEntryName, sigFileEntry.getName());
297                 Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo);
298                 signers.add(signer);
299             }
300             if (signers.isEmpty()) {
301                 result.addError(Issue.JAR_SIG_NO_SIGNATURES);
302                 return;
303             }
304 
305             // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding
306             // signature file .SF. Any error encountered for any signer terminates verification, to
307             // mimic Android's behavior.
308             for (Signer signer : signers) {
309                 signer.verifySigBlockAgainstSigFile(
310                         apk, cdStartOffset, minSdkVersion, maxSdkVersion);
311                 if (signer.getResult().containsErrors()) {
312                     result.signers.add(signer.getResult());
313                 }
314             }
315             if (result.containsErrors()) {
316                 return;
317             }
318             // STATE OF AFFAIRS:
319             // * All JAR entries listed in JAR manifest are present in the APK.
320             // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
321 
322             // Verify each signer's signature file (.SF) against the JAR manifest.
323             List<Signer> remainingSigners = new ArrayList<>(signers.size());
324             for (Signer signer : signers) {
325                 signer.verifySigFileAgainstManifest(
326                         manifestBytes,
327                         manifestMainSection,
328                         entryNameToManifestSection,
329                         supportedApkSigSchemeNames,
330                         foundApkSigSchemeIds,
331                         minSdkVersion,
332                         maxSdkVersion);
333                 if (signer.isIgnored()) {
334                     result.ignoredSigners.add(signer.getResult());
335                 } else {
336                     if (signer.getResult().containsErrors()) {
337                         result.signers.add(signer.getResult());
338                     } else {
339                         remainingSigners.add(signer);
340                     }
341                 }
342             }
343             if (result.containsErrors()) {
344                 return;
345             }
346             signers = remainingSigners;
347             if (signers.isEmpty()) {
348                 result.addError(Issue.JAR_SIG_NO_SIGNATURES);
349                 return;
350             }
351             // STATE OF AFFAIRS:
352             // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
353             // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
354             // * All JAR entries listed in JAR manifest are present in the APK.
355 
356             // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's
357             // JAR entry is considered signed by signers associated with an .SF file iff the entry
358             // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest
359             // match theentry's uncompressed data. Android requires that all such JAR entries are
360             // signed by the same set of signers. This set may be smaller than the set of signers
361             // we've identified so far.
362             Set<Signer> apkSigners =
363                     verifyJarEntriesAgainstManifestAndSigners(
364                             apk,
365                             cdStartOffset,
366                             cdRecords,
367                             entryNameToManifestSection,
368                             signers,
369                             minSdkVersion,
370                             maxSdkVersion,
371                             result);
372             if (result.containsErrors()) {
373                 return;
374             }
375             // STATE OF AFFAIRS:
376             // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC).
377             // * Contents of all JAR manifest sections listed in .SF files verify against .SF files.
378             // * All JAR entries listed in JAR manifest are present in the APK.
379             // * All JAR entries present in the APK and supposed to be covered by JAR signature
380             //   (i.e., reside outside of META-INF/) are covered by signatures from the same set
381             //   of signers.
382 
383             // Report any JAR entries which aren't covered by signature.
384             Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2);
385             signatureEntryNames.add(manifestEntry.getName());
386             for (Signer signer : apkSigners) {
387                 signatureEntryNames.add(signer.getSignatureBlockEntryName());
388                 signatureEntryNames.add(signer.getSignatureFileEntryName());
389             }
390             for (CentralDirectoryRecord cdRecord : cdRecords) {
391                 String entryName = cdRecord.getName();
392                 if ((entryName.startsWith("META-INF/"))
393                         && (!entryName.endsWith("/"))
394                         && (!signatureEntryNames.contains(entryName))) {
395                     result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName);
396                 }
397             }
398 
399             // Reflect the sets of used signers and ignored signers in the result.
400             for (Signer signer : signers) {
401                 if (apkSigners.contains(signer)) {
402                     result.signers.add(signer.getResult());
403                 } else {
404                     result.ignoredSigners.add(signer.getResult());
405                 }
406             }
407 
408             result.verified = true;
409         }
410     }
411 
412     static class Signer {
413         private final String mName;
414         private final Result.SignerInfo mResult;
415         private final CentralDirectoryRecord mSignatureFileEntry;
416         private final CentralDirectoryRecord mSignatureBlockEntry;
417         private boolean mIgnored;
418 
419         private byte[] mSigFileBytes;
420         private Set<String> mSigFileEntryNames;
421 
Signer( String name, CentralDirectoryRecord sigBlockEntry, CentralDirectoryRecord sigFileEntry, Result.SignerInfo result)422         private Signer(
423                 String name,
424                 CentralDirectoryRecord sigBlockEntry,
425                 CentralDirectoryRecord sigFileEntry,
426                 Result.SignerInfo result) {
427             mName = name;
428             mResult = result;
429             mSignatureBlockEntry = sigBlockEntry;
430             mSignatureFileEntry = sigFileEntry;
431         }
432 
getName()433         public String getName() {
434             return mName;
435         }
436 
getSignatureFileEntryName()437         public String getSignatureFileEntryName() {
438             return mSignatureFileEntry.getName();
439         }
440 
getSignatureBlockEntryName()441         public String getSignatureBlockEntryName() {
442             return mSignatureBlockEntry.getName();
443         }
444 
setIgnored()445         void setIgnored() {
446             mIgnored = true;
447         }
448 
isIgnored()449         public boolean isIgnored() {
450             return mIgnored;
451         }
452 
getSigFileEntryNames()453         public Set<String> getSigFileEntryNames() {
454             return mSigFileEntryNames;
455         }
456 
getResult()457         public Result.SignerInfo getResult() {
458             return mResult;
459         }
460 
verifySigBlockAgainstSigFile( DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)461         public void verifySigBlockAgainstSigFile(
462                 DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)
463                         throws IOException, ApkFormatException, NoSuchAlgorithmException {
464             // Obtain the signature block from the APK
465             byte[] sigBlockBytes;
466             try {
467                 sigBlockBytes =
468                         LocalFileRecord.getUncompressedData(
469                                 apk, mSignatureBlockEntry, cdStartOffset);
470             } catch (ZipFormatException e) {
471                 throw new ApkFormatException(
472                         "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e);
473             }
474             // Obtain the signature file from the APK
475             try {
476                 mSigFileBytes =
477                         LocalFileRecord.getUncompressedData(
478                                 apk, mSignatureFileEntry, cdStartOffset);
479             } catch (ZipFormatException e) {
480                 throw new ApkFormatException(
481                         "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e);
482             }
483 
484             // Extract PKCS #7 SignedData from the signature block
485             SignedData signedData;
486             try {
487                 ContentInfo contentInfo =
488                         Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class);
489                 if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) {
490                     throw new Asn1DecodingException(
491                           "Unsupported ContentInfo.contentType: " + contentInfo.contentType);
492                 }
493                 signedData =
494                         Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class);
495             } catch (Asn1DecodingException e) {
496                 e.printStackTrace();
497                 mResult.addError(
498                         Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
499                 return;
500             }
501 
502             if (signedData.signerInfos.isEmpty()) {
503                 mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName());
504                 return;
505             }
506 
507             // Find the first SignedData.SignerInfos element which verifies against the signature
508             // file
509             SignerInfo firstVerifiedSignerInfo = null;
510             X509Certificate firstVerifiedSignerInfoSigningCertificate = null;
511             // Prior to Android N, Android attempts to verify only the first SignerInfo. From N
512             // onwards, Android attempts to verify all SignerInfos and then picks the first verified
513             // SignerInfo.
514             List<SignerInfo> unverifiedSignerInfosToTry;
515             if (minSdkVersion < AndroidSdkVersion.N) {
516                 unverifiedSignerInfosToTry =
517                         Collections.singletonList(signedData.signerInfos.get(0));
518             } else {
519                 unverifiedSignerInfosToTry = signedData.signerInfos;
520             }
521             List<X509Certificate> signedDataCertificates = null;
522             for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) {
523                 // Parse SignedData.certificates -- they are needed to verify SignerInfo
524                 if (signedDataCertificates == null) {
525                     try {
526                         signedDataCertificates = parseCertificates(signedData.certificates);
527                     } catch (CertificateException e) {
528                         mResult.addError(
529                                 Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
530                         return;
531                     }
532                 }
533 
534                 // Verify SignerInfo
535                 X509Certificate signingCertificate;
536                 try {
537                     signingCertificate =
538                             verifySignerInfoAgainstSigFile(
539                                     signedData,
540                                     signedDataCertificates,
541                                     unverifiedSignerInfo,
542                                     mSigFileBytes,
543                                     minSdkVersion,
544                                     maxSdkVersion);
545                     if (mResult.containsErrors()) {
546                         return;
547                     }
548                     if (signingCertificate != null) {
549                         // SignerInfo verified
550                         if (firstVerifiedSignerInfo == null) {
551                             firstVerifiedSignerInfo = unverifiedSignerInfo;
552                             firstVerifiedSignerInfoSigningCertificate = signingCertificate;
553                         }
554                     }
555                 } catch (Pkcs7DecodingException e) {
556                     mResult.addError(
557                             Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e);
558                     return;
559                 } catch (InvalidKeyException | SignatureException e) {
560                     mResult.addError(
561                             Issue.JAR_SIG_VERIFY_EXCEPTION,
562                             mSignatureBlockEntry.getName(),
563                             mSignatureFileEntry.getName(),
564                             e);
565                     return;
566                 }
567             }
568             if (firstVerifiedSignerInfo == null) {
569                 // No SignerInfo verified
570                 mResult.addError(
571                         Issue.JAR_SIG_DID_NOT_VERIFY,
572                         mSignatureBlockEntry.getName(),
573                         mSignatureFileEntry.getName());
574                 return;
575             }
576             // Verified
577             List<X509Certificate> signingCertChain =
578                     getCertificateChain(
579                             signedDataCertificates, firstVerifiedSignerInfoSigningCertificate);
580             mResult.certChain.clear();
581             mResult.certChain.addAll(signingCertChain);
582         }
583 
584         /**
585          * Returns the signing certificate if the provided {@link SignerInfo} verifies against the
586          * contents of the provided signature file, or {@code null} if it does not verify.
587          */
verifySignerInfoAgainstSigFile( SignedData signedData, Collection<X509Certificate> signedDataCertificates, SignerInfo signerInfo, byte[] signatureFile, int minSdkVersion, int maxSdkVersion)588         private X509Certificate verifySignerInfoAgainstSigFile(
589                 SignedData signedData,
590                 Collection<X509Certificate> signedDataCertificates,
591                 SignerInfo signerInfo,
592                 byte[] signatureFile,
593                 int minSdkVersion,
594                 int maxSdkVersion)
595                         throws Pkcs7DecodingException, NoSuchAlgorithmException,
596                                 InvalidKeyException, SignatureException {
597             String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm;
598             String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm;
599             InclusiveIntRange desiredApiLevels =
600                     InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion);
601             List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported =
602                     getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid);
603             List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported =
604                     desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported);
605             if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) {
606                 String digestAlgorithmUserFriendly =
607                         OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
608                                 digestAlgorithmOid);
609                 if (digestAlgorithmUserFriendly == null) {
610                     digestAlgorithmUserFriendly = digestAlgorithmOid;
611                 }
612                 String signatureAlgorithmUserFriendly =
613                         OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid(
614                                 signatureAlgorithmOid);
615                 if (signatureAlgorithmUserFriendly == null) {
616                     signatureAlgorithmUserFriendly = signatureAlgorithmOid;
617                 }
618                 StringBuilder apiLevelsUserFriendly = new StringBuilder();
619                 for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) {
620                     if (apiLevelsUserFriendly.length() > 0) {
621                         apiLevelsUserFriendly.append(", ");
622                     }
623                     if (range.getMin() == range.getMax()) {
624                         apiLevelsUserFriendly.append(String.valueOf(range.getMin()));
625                     } else if (range.getMax() == Integer.MAX_VALUE) {
626                         apiLevelsUserFriendly.append(range.getMin() + "+");
627                     } else {
628                         apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax());
629                     }
630                 }
631                 mResult.addError(
632                         Issue.JAR_SIG_UNSUPPORTED_SIG_ALG,
633                         mSignatureBlockEntry.getName(),
634                         digestAlgorithmOid,
635                         signatureAlgorithmOid,
636                         apiLevelsUserFriendly.toString(),
637                         digestAlgorithmUserFriendly,
638                         signatureAlgorithmUserFriendly);
639                 return null;
640             }
641 
642             // From the bag of certs, obtain the certificate referenced by the SignerInfo,
643             // and verify the cryptographic signature in the SignerInfo against the certificate.
644 
645             // Locate the signing certificate referenced by the SignerInfo
646             X509Certificate signingCertificate =
647                     findCertificate(signedDataCertificates, signerInfo.sid);
648             if (signingCertificate == null) {
649                 throw new SignatureException(
650                         "Signing certificate referenced in SignerInfo not found in"
651                                 + " SignedData");
652             }
653 
654             // Check whether the signing certificate is acceptable. Android performs these
655             // checks explicitly, instead of delegating this to
656             // Signature.initVerify(Certificate).
657             if (signingCertificate.hasUnsupportedCriticalExtension()) {
658                 throw new SignatureException(
659                         "Signing certificate has unsupported critical extensions");
660             }
661             boolean[] keyUsageExtension = signingCertificate.getKeyUsage();
662             if (keyUsageExtension != null) {
663                 boolean digitalSignature =
664                         (keyUsageExtension.length >= 1) && (keyUsageExtension[0]);
665                 boolean nonRepudiation =
666                         (keyUsageExtension.length >= 2) && (keyUsageExtension[1]);
667                 if ((!digitalSignature) && (!nonRepudiation)) {
668                     throw new SignatureException(
669                             "Signing certificate not authorized for use in digital signatures"
670                                     + ": keyUsage extension missing digitalSignature and"
671                                     + " nonRepudiation");
672                 }
673             }
674 
675             // Verify the cryptographic signature in SignerInfo against the certificate's
676             // public key
677             String jcaSignatureAlgorithm =
678                     getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid);
679             Signature s = Signature.getInstance(jcaSignatureAlgorithm);
680             s.initVerify(signingCertificate.getPublicKey());
681             if (signerInfo.signedAttrs != null) {
682                 // Signed attributes present -- verify signature against the ASN.1 DER encoded form
683                 // of signed attributes. This verifies integrity of the signature file because
684                 // signed attributes must contain the digest of the signature file.
685                 if (minSdkVersion < AndroidSdkVersion.KITKAT) {
686                     // Prior to Android KitKat, APKs with signed attributes are unsafe:
687                     // * The APK's contents are not protected by the JAR signature because the
688                     //   digest in signed attributes is not verified. This means an attacker can
689                     //   arbitrarily modify the APK without invalidating its signature.
690                     // * Luckily, the signature over signed attributes was verified incorrectly
691                     //   (over the verbatim IMPLICIT [0] form rather than over re-encoded
692                     //   UNIVERSAL SET form) which means that JAR signatures which would verify on
693                     //   pre-KitKat Android and yet do not protect the APK from modification could
694                     //   be generated only by broken tools or on purpose by the entity signing the
695                     //   APK.
696                     //
697                     // We thus reject such unsafe APKs, even if they verify on platforms before
698                     // KitKat.
699                     throw new SignatureException(
700                             "APKs with Signed Attributes broken on platforms with API Level < "
701                                     + AndroidSdkVersion.KITKAT);
702                 }
703                 try {
704                     List<Attribute> signedAttributes =
705                             Asn1BerParser.parseImplicitSetOf(
706                                     signerInfo.signedAttrs.getEncoded(), Attribute.class);
707                     SignedAttributes signedAttrs = new SignedAttributes(signedAttributes);
708                     if (maxSdkVersion >= AndroidSdkVersion.N) {
709                         // Content Type attribute is checked only on Android N and newer
710                         String contentType =
711                                 signedAttrs.getSingleObjectIdentifierValue(
712                                         Pkcs7Constants.OID_CONTENT_TYPE);
713                         if (contentType == null) {
714                             throw new SignatureException("No Content Type in signed attributes");
715                         }
716                         if (!contentType.equals(signedData.encapContentInfo.contentType)) {
717                             // Did not verify: Content type signed attribute does not match
718                             // SignedData.encapContentInfo.eContentType. This fails verification of
719                             // this SignerInfo but should not prevent verification of other
720                             // SignerInfos. Hence, no exception is thrown.
721                             return null;
722                         }
723                     }
724                     byte[] expectedSignatureFileDigest =
725                             signedAttrs.getSingleOctetStringValue(
726                                     Pkcs7Constants.OID_MESSAGE_DIGEST);
727                     if (expectedSignatureFileDigest == null) {
728                         throw new SignatureException("No content digest in signed attributes");
729                     }
730                     byte[] actualSignatureFileDigest =
731                             MessageDigest.getInstance(
732                                     getJcaDigestAlgorithm(digestAlgorithmOid))
733                                     .digest(signatureFile);
734                     if (!Arrays.equals(
735                             expectedSignatureFileDigest, actualSignatureFileDigest)) {
736                         // Skip verification: signature file digest in signed attributes does not
737                         // match the signature file. This fails verification of
738                         // this SignerInfo but should not prevent verification of other
739                         // SignerInfos. Hence, no exception is thrown.
740                         return null;
741                     }
742                 } catch (Asn1DecodingException e) {
743                     throw new SignatureException("Failed to parse signed attributes", e);
744                 }
745                 // PKCS #7 requires that signature is over signed attributes re-encoded as
746                 // ASN.1 DER. However, Android does not re-encode except for changing the
747                 // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the
748                 // same for maximum compatibility.
749                 ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded();
750                 s.update((byte) 0x31); // UNIVERSAL SET
751                 signedAttrsOriginalEncoding.position(1);
752                 s.update(signedAttrsOriginalEncoding);
753             } else {
754                 // No signed attributes present -- verify signature against the contents of the
755                 // signature file
756                 s.update(signatureFile);
757             }
758             byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice());
759             if (!s.verify(sigBytes)) {
760                 // Cryptographic signature did not verify. This fails verification of this
761                 // SignerInfo but should not prevent verification of other SignerInfos. Hence, no
762                 // exception is thrown.
763                 return null;
764             }
765             // Cryptographic signature verified
766             return signingCertificate;
767         }
768 
769 
770 
getCertificateChain( List<X509Certificate> certs, X509Certificate leaf)771         public static List<X509Certificate> getCertificateChain(
772                 List<X509Certificate> certs, X509Certificate leaf) {
773             List<X509Certificate> unusedCerts = new ArrayList<>(certs);
774             List<X509Certificate> result = new ArrayList<>(1);
775             result.add(leaf);
776             unusedCerts.remove(leaf);
777             X509Certificate root = leaf;
778             while (!root.getSubjectDN().equals(root.getIssuerDN())) {
779                 Principal targetDn = root.getIssuerDN();
780                 boolean issuerFound = false;
781                 for (int i = 0; i < unusedCerts.size(); i++) {
782                     X509Certificate unusedCert = unusedCerts.get(i);
783                     if (targetDn.equals(unusedCert.getSubjectDN())) {
784                         issuerFound = true;
785                         unusedCerts.remove(i);
786                         result.add(unusedCert);
787                         root = unusedCert;
788                         break;
789                     }
790                 }
791                 if (!issuerFound) {
792                     break;
793                 }
794             }
795             return result;
796         }
797 
798 
799 
800 
verifySigFileAgainstManifest( byte[] manifestBytes, ManifestParser.Section manifestMainSection, Map<String, ManifestParser.Section> entryNameToManifestSection, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion)801         public void verifySigFileAgainstManifest(
802                 byte[] manifestBytes,
803                 ManifestParser.Section manifestMainSection,
804                 Map<String, ManifestParser.Section> entryNameToManifestSection,
805                 Map<Integer, String> supportedApkSigSchemeNames,
806                 Set<Integer> foundApkSigSchemeIds,
807                 int minSdkVersion,
808                 int maxSdkVersion) throws NoSuchAlgorithmException {
809             // Inspect the main section of the .SF file.
810             ManifestParser sf = new ManifestParser(mSigFileBytes);
811             ManifestParser.Section sfMainSection = sf.readSection();
812             if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) {
813                 mResult.addError(
814                         Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE,
815                         mSignatureFileEntry.getName());
816                 setIgnored();
817                 return;
818             }
819 
820             if (maxSdkVersion >= AndroidSdkVersion.N) {
821                 // Android N and newer rejects APKs whose .SF file says they were supposed to be
822                 // signed with APK Signature Scheme v2 (or newer) and yet no such signature was
823                 // found.
824                 checkForStrippedApkSignatures(
825                         sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds);
826                 if (mResult.containsErrors()) {
827                     return;
828                 }
829             }
830 
831             boolean createdBySigntool = false;
832             String createdBy = sfMainSection.getAttributeValue("Created-By");
833             if (createdBy != null) {
834                 createdBySigntool = createdBy.indexOf("signtool") != -1;
835             }
836             boolean manifestDigestVerified =
837                     verifyManifestDigest(
838                             sfMainSection,
839                             createdBySigntool,
840                             manifestBytes,
841                             minSdkVersion,
842                             maxSdkVersion);
843             if (!createdBySigntool) {
844                 verifyManifestMainSectionDigest(
845                         sfMainSection,
846                         manifestMainSection,
847                         manifestBytes,
848                         minSdkVersion,
849                         maxSdkVersion);
850             }
851             if (mResult.containsErrors()) {
852                 return;
853             }
854 
855             // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest
856             // verifies, per-entry sections should be ignored. However, most Android platform
857             // implementations require that such sections exist.
858             List<ManifestParser.Section> sfSections = sf.readAllSections();
859             Set<String> sfEntryNames = new HashSet<>(sfSections.size());
860             int sfSectionNumber = 0;
861             for (ManifestParser.Section sfSection : sfSections) {
862                 sfSectionNumber++;
863                 String entryName = sfSection.getName();
864                 if (entryName == null) {
865                     mResult.addError(
866                             Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION,
867                             mSignatureFileEntry.getName(),
868                             sfSectionNumber);
869                     setIgnored();
870                     return;
871                 }
872                 if (!sfEntryNames.add(entryName)) {
873                     mResult.addError(
874                             Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION,
875                             mSignatureFileEntry.getName(),
876                             entryName);
877                     setIgnored();
878                     return;
879                 }
880                 if (manifestDigestVerified) {
881                     // No need to verify this entry's corresponding JAR manifest entry because the
882                     // JAR manifest verifies in full.
883                     continue;
884                 }
885                 // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify
886                 // the digest of the JAR manifest section corresponding to this .SF section.
887                 ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
888                 if (manifestSection == null) {
889                     mResult.addError(
890                             Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
891                             entryName,
892                             mSignatureFileEntry.getName());
893                     setIgnored();
894                     continue;
895                 }
896                 verifyManifestIndividualSectionDigest(
897                         sfSection,
898                         createdBySigntool,
899                         manifestSection,
900                         manifestBytes,
901                         minSdkVersion,
902                         maxSdkVersion);
903             }
904             mSigFileEntryNames = sfEntryNames;
905         }
906 
907 
908         /**
909          * Returns {@code true} if the whole-file digest of the manifest against the main section of
910          * the .SF file.
911          */
verifyManifestDigest( ManifestParser.Section sfMainSection, boolean createdBySigntool, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)912         private boolean verifyManifestDigest(
913                 ManifestParser.Section sfMainSection,
914                 boolean createdBySigntool,
915                 byte[] manifestBytes,
916                 int minSdkVersion,
917                 int maxSdkVersion) throws NoSuchAlgorithmException {
918             Collection<NamedDigest> expectedDigests =
919                     getDigestsToVerify(
920                             sfMainSection,
921                             ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"),
922                             minSdkVersion,
923                             maxSdkVersion);
924             boolean digestFound = !expectedDigests.isEmpty();
925             if (!digestFound) {
926                 mResult.addWarning(
927                         Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE,
928                         mSignatureFileEntry.getName());
929                 return false;
930             }
931 
932             boolean verified = true;
933             for (NamedDigest expectedDigest : expectedDigests) {
934                 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
935                 byte[] actual = digest(jcaDigestAlgorithm, manifestBytes);
936                 byte[] expected = expectedDigest.digest;
937                 if (!Arrays.equals(expected, actual)) {
938                     mResult.addWarning(
939                             Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
940                             V1SchemeConstants.MANIFEST_ENTRY_NAME,
941                             jcaDigestAlgorithm,
942                             mSignatureFileEntry.getName(),
943                             Base64.getEncoder().encodeToString(actual),
944                             Base64.getEncoder().encodeToString(expected));
945                     verified = false;
946                 }
947             }
948             return verified;
949         }
950 
951         /**
952          * Verifies the digest of the manifest's main section against the main section of the .SF
953          * file.
954          */
verifyManifestMainSectionDigest( ManifestParser.Section sfMainSection, ManifestParser.Section manifestMainSection, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)955         private void verifyManifestMainSectionDigest(
956                 ManifestParser.Section sfMainSection,
957                 ManifestParser.Section manifestMainSection,
958                 byte[] manifestBytes,
959                 int minSdkVersion,
960                 int maxSdkVersion) throws NoSuchAlgorithmException {
961             Collection<NamedDigest> expectedDigests =
962                     getDigestsToVerify(
963                             sfMainSection,
964                             "-Digest-Manifest-Main-Attributes",
965                             minSdkVersion,
966                             maxSdkVersion);
967             if (expectedDigests.isEmpty()) {
968                 return;
969             }
970 
971             for (NamedDigest expectedDigest : expectedDigests) {
972                 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
973                 byte[] actual =
974                         digest(
975                                 jcaDigestAlgorithm,
976                                 manifestBytes,
977                                 manifestMainSection.getStartOffset(),
978                                 manifestMainSection.getSizeBytes());
979                 byte[] expected = expectedDigest.digest;
980                 if (!Arrays.equals(expected, actual)) {
981                     mResult.addError(
982                             Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY,
983                             jcaDigestAlgorithm,
984                             mSignatureFileEntry.getName(),
985                             Base64.getEncoder().encodeToString(actual),
986                             Base64.getEncoder().encodeToString(expected));
987                 }
988             }
989         }
990 
991         /**
992          * Verifies the digest of the manifest's individual section against the corresponding
993          * individual section of the .SF file.
994          */
verifyManifestIndividualSectionDigest( ManifestParser.Section sfIndividualSection, boolean createdBySigntool, ManifestParser.Section manifestIndividualSection, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)995         private void verifyManifestIndividualSectionDigest(
996                 ManifestParser.Section sfIndividualSection,
997                 boolean createdBySigntool,
998                 ManifestParser.Section manifestIndividualSection,
999                 byte[] manifestBytes,
1000                 int minSdkVersion,
1001                 int maxSdkVersion) throws NoSuchAlgorithmException {
1002             String entryName = sfIndividualSection.getName();
1003             Collection<NamedDigest> expectedDigests =
1004                     getDigestsToVerify(
1005                             sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion);
1006             if (expectedDigests.isEmpty()) {
1007                 mResult.addError(
1008                         Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE,
1009                         entryName,
1010                         mSignatureFileEntry.getName());
1011                 return;
1012             }
1013 
1014             int sectionStartIndex = manifestIndividualSection.getStartOffset();
1015             int sectionSizeBytes = manifestIndividualSection.getSizeBytes();
1016             if (createdBySigntool) {
1017                 int sectionEndIndex = sectionStartIndex + sectionSizeBytes;
1018                 if ((manifestBytes[sectionEndIndex - 1] == '\n')
1019                         && (manifestBytes[sectionEndIndex - 2] == '\n')) {
1020                     sectionSizeBytes--;
1021                 }
1022             }
1023             for (NamedDigest expectedDigest : expectedDigests) {
1024                 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm;
1025                 byte[] actual =
1026                         digest(
1027                                 jcaDigestAlgorithm,
1028                                 manifestBytes,
1029                                 sectionStartIndex,
1030                                 sectionSizeBytes);
1031                 byte[] expected = expectedDigest.digest;
1032                 if (!Arrays.equals(expected, actual)) {
1033                     mResult.addError(
1034                             Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY,
1035                             entryName,
1036                             jcaDigestAlgorithm,
1037                             mSignatureFileEntry.getName(),
1038                             Base64.getEncoder().encodeToString(actual),
1039                             Base64.getEncoder().encodeToString(expected));
1040                 }
1041             }
1042         }
1043 
checkForStrippedApkSignatures( ManifestParser.Section sfMainSection, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds)1044         private void checkForStrippedApkSignatures(
1045                 ManifestParser.Section sfMainSection,
1046                 Map<Integer, String> supportedApkSigSchemeNames,
1047                 Set<Integer> foundApkSigSchemeIds) {
1048             String signedWithApkSchemes =
1049                     sfMainSection.getAttributeValue(
1050                             V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
1051             // This field contains a comma-separated list of APK signature scheme IDs which were
1052             // used to sign this APK. Android rejects APKs where an ID is known to the platform but
1053             // the APK didn't verify using that scheme.
1054 
1055             if (signedWithApkSchemes == null) {
1056                 // APK signature (e.g., v2 scheme) stripping protections not enabled.
1057                 if (!foundApkSigSchemeIds.isEmpty()) {
1058                     // APK is signed with an APK signature scheme such as v2 scheme.
1059                     mResult.addWarning(
1060                             Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION,
1061                             mSignatureFileEntry.getName());
1062                 }
1063                 return;
1064             }
1065 
1066             if (supportedApkSigSchemeNames.isEmpty()) {
1067                 return;
1068             }
1069 
1070             Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
1071             Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
1072             StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ",");
1073             while (tokenizer.hasMoreTokens()) {
1074                 String idText = tokenizer.nextToken().trim();
1075                 if (idText.isEmpty()) {
1076                     continue;
1077                 }
1078                 int id;
1079                 try {
1080                     id = Integer.parseInt(idText);
1081                 } catch (Exception ignored) {
1082                     continue;
1083                 }
1084                 // This APK was supposed to be signed with the APK signature scheme having
1085                 // this ID.
1086                 if (supportedApkSigSchemeIds.contains(id)) {
1087                     supportedExpectedApkSigSchemeIds.add(id);
1088                 } else {
1089                     mResult.addWarning(
1090                             Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID,
1091                             mSignatureFileEntry.getName(),
1092                             id);
1093                 }
1094             }
1095 
1096             for (int id : supportedExpectedApkSigSchemeIds) {
1097                 if (!foundApkSigSchemeIds.contains(id)) {
1098                     String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
1099                     mResult.addError(
1100                             Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED,
1101                             mSignatureFileEntry.getName(),
1102                             id,
1103                             apkSigSchemeName);
1104                 }
1105             }
1106         }
1107     }
1108 
getDigestsToVerify( ManifestParser.Section section, String digestAttrSuffix, int minSdkVersion, int maxSdkVersion)1109     public static Collection<NamedDigest> getDigestsToVerify(
1110             ManifestParser.Section section,
1111             String digestAttrSuffix,
1112             int minSdkVersion,
1113             int maxSdkVersion) {
1114         Decoder base64Decoder = Base64.getDecoder();
1115         List<NamedDigest> result = new ArrayList<>(1);
1116         if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) {
1117             // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is
1118             // to rely on the ancient Digest-Algorithms attribute which contains
1119             // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The
1120             // first digest attribute (with supported digest algorithm) found using the list is
1121             // used.
1122             String algs = section.getAttributeValue("Digest-Algorithms");
1123             if (algs == null) {
1124                 algs = "SHA SHA1";
1125             }
1126             StringTokenizer tokens = new StringTokenizer(algs);
1127             while (tokens.hasMoreTokens()) {
1128                 String alg = tokens.nextToken();
1129                 String attrName = alg + digestAttrSuffix;
1130                 String digestBase64 = section.getAttributeValue(attrName);
1131                 if (digestBase64 == null) {
1132                     // Attribute not found
1133                     continue;
1134                 }
1135                 alg = getCanonicalJcaMessageDigestAlgorithm(alg);
1136                 if ((alg == null)
1137                         || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg)
1138                                 > minSdkVersion)) {
1139                     // Unsupported digest algorithm
1140                     continue;
1141                 }
1142                 // Supported digest algorithm
1143                 result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64)));
1144                 break;
1145             }
1146             // No supported digests found -- this will fail to verify on pre-JB MR2 Androids.
1147             if (result.isEmpty()) {
1148                 return result;
1149             }
1150         }
1151 
1152         if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) {
1153             // On JB MR2 and newer, Android platform picks the strongest algorithm out of:
1154             // SHA-512, SHA-384, SHA-256, SHA-1.
1155             for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) {
1156                 String attrName = getJarDigestAttributeName(alg, digestAttrSuffix);
1157                 String digestBase64 = section.getAttributeValue(attrName);
1158                 if (digestBase64 == null) {
1159                     // Attribute not found
1160                     continue;
1161                 }
1162                 byte[] digest = base64Decoder.decode(digestBase64);
1163                 byte[] digestInResult = getDigest(result, alg);
1164                 if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) {
1165                     result.add(new NamedDigest(alg, digest));
1166                 }
1167                 break;
1168             }
1169         }
1170 
1171         return result;
1172     }
1173 
1174     private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = {
1175             "SHA-512",
1176             "SHA-384",
1177             "SHA-256",
1178             "SHA-1",
1179     };
1180 
getCanonicalJcaMessageDigestAlgorithm(String algorithm)1181     private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) {
1182         return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US));
1183     }
1184 
getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( String jcaAlgorithmName)1185     public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(
1186             String jcaAlgorithmName) {
1187         Integer result =
1188                 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get(
1189                         jcaAlgorithmName.toUpperCase(Locale.US));
1190         return (result != null) ? result : Integer.MAX_VALUE;
1191     }
1192 
getJarDigestAttributeName( String jcaDigestAlgorithm, String attrNameSuffix)1193     private static String getJarDigestAttributeName(
1194             String jcaDigestAlgorithm, String attrNameSuffix) {
1195         if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) {
1196             return "SHA1" + attrNameSuffix;
1197         } else {
1198             return jcaDigestAlgorithm + attrNameSuffix;
1199         }
1200     }
1201 
1202     private static final Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL;
1203     static {
1204         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8);
1205         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5");
1206         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1");
1207         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1");
1208         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1");
1209         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256");
1210         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384");
1211         UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512");
1212     }
1213 
1214     private static final Map<String, Integer>
1215             MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST;
1216     static {
1217         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5);
1218         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0);
1219         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0);
1220         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0);
1221         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
1222                 "SHA-384", AndroidSdkVersion.GINGERBREAD);
1223         MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put(
1224                 "SHA-512", AndroidSdkVersion.GINGERBREAD);
1225     }
1226 
getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm)1227     private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) {
1228         for (NamedDigest digest : digests) {
1229             if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) {
1230                 return digest.digest;
1231             }
1232         }
1233         return null;
1234     }
1235 
parseZipCentralDirectory( DataSource apk, ApkUtils.ZipSections apkSections)1236     public static List<CentralDirectoryRecord> parseZipCentralDirectory(
1237             DataSource apk,
1238             ApkUtils.ZipSections apkSections)
1239                     throws IOException, ApkFormatException {
1240         return ZipUtils.parseZipCentralDirectory(apk, apkSections);
1241     }
1242 
1243     /**
1244      * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
1245      * manifest for the APK to verify on Android.
1246      */
isJarEntryDigestNeededInManifest(String entryName)1247     private static boolean isJarEntryDigestNeededInManifest(String entryName) {
1248         // NOTE: This logic is different from what's required by the JAR signing scheme. This is
1249         // because Android's APK verification logic differs from that spec. In particular, JAR
1250         // signing spec includes into JAR manifest all files in subdirectories of META-INF and
1251         // any files inside META-INF not related to signatures.
1252         if (entryName.startsWith("META-INF/")) {
1253             return false;
1254         }
1255         return !entryName.endsWith("/");
1256     }
1257 
verifyJarEntriesAgainstManifestAndSigners( DataSource apk, long cdOffsetInApk, Collection<CentralDirectoryRecord> cdRecords, Map<String, ManifestParser.Section> entryNameToManifestSection, List<Signer> signers, int minSdkVersion, int maxSdkVersion, Result result)1258     private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners(
1259             DataSource apk,
1260             long cdOffsetInApk,
1261             Collection<CentralDirectoryRecord> cdRecords,
1262             Map<String, ManifestParser.Section> entryNameToManifestSection,
1263             List<Signer> signers,
1264             int minSdkVersion,
1265             int maxSdkVersion,
1266             Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException {
1267         // Iterate over APK contents as sequentially as possible to improve performance.
1268         List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset =
1269                 new ArrayList<>(cdRecords);
1270         Collections.sort(
1271                 cdRecordsSortedByLocalFileHeaderOffset,
1272                 CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR);
1273         List<Signer> firstSignedEntrySigners = null;
1274         String firstSignedEntryName = null;
1275         for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) {
1276             String entryName = cdRecord.getName();
1277             if (!isJarEntryDigestNeededInManifest(entryName)) {
1278                 continue;
1279             }
1280 
1281             ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName);
1282             if (manifestSection == null) {
1283                 result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
1284                 continue;
1285             }
1286 
1287             List<Signer> entrySigners = new ArrayList<>(signers.size());
1288             for (Signer signer : signers) {
1289                 if (signer.getSigFileEntryNames().contains(entryName)) {
1290                     entrySigners.add(signer);
1291                 }
1292             }
1293             if (entrySigners.isEmpty()) {
1294                 result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName);
1295                 continue;
1296             }
1297             if (firstSignedEntrySigners == null) {
1298                 firstSignedEntrySigners = entrySigners;
1299                 firstSignedEntryName = entryName;
1300             } else if (!entrySigners.equals(firstSignedEntrySigners)) {
1301                 result.addError(
1302                         Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH,
1303                         firstSignedEntryName,
1304                         getSignerNames(firstSignedEntrySigners),
1305                         entryName,
1306                         getSignerNames(entrySigners));
1307                 continue;
1308             }
1309 
1310             List<NamedDigest> expectedDigests =
1311                     new ArrayList<>(
1312                             getDigestsToVerify(
1313                                     manifestSection, "-Digest", minSdkVersion, maxSdkVersion));
1314             if (expectedDigests.isEmpty()) {
1315                 result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName);
1316                 continue;
1317             }
1318 
1319             MessageDigest[] mds = new MessageDigest[expectedDigests.size()];
1320             for (int i = 0; i < expectedDigests.size(); i++) {
1321                 mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm);
1322             }
1323 
1324             try {
1325                 LocalFileRecord.outputUncompressedData(
1326                         apk,
1327                         cdRecord,
1328                         cdOffsetInApk,
1329                         DataSinks.asDataSink(mds));
1330             } catch (ZipFormatException e) {
1331                 throw new ApkFormatException("Malformed ZIP entry: " + entryName, e);
1332             } catch (IOException e) {
1333                 throw new IOException("Failed to read entry: " + entryName, e);
1334             }
1335 
1336             for (int i = 0; i < expectedDigests.size(); i++) {
1337                 NamedDigest expectedDigest = expectedDigests.get(i);
1338                 byte[] actualDigest = mds[i].digest();
1339                 if (!Arrays.equals(expectedDigest.digest, actualDigest)) {
1340                     result.addError(
1341                             Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY,
1342                             entryName,
1343                             expectedDigest.jcaDigestAlgorithm,
1344                             V1SchemeConstants.MANIFEST_ENTRY_NAME,
1345                             Base64.getEncoder().encodeToString(actualDigest),
1346                             Base64.getEncoder().encodeToString(expectedDigest.digest));
1347                 }
1348             }
1349         }
1350 
1351         if (firstSignedEntrySigners == null) {
1352             result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES);
1353             return Collections.emptySet();
1354         } else {
1355             return new HashSet<>(firstSignedEntrySigners);
1356         }
1357     }
1358 
getSignerNames(List<Signer> signers)1359     private static List<String> getSignerNames(List<Signer> signers) {
1360         if (signers.isEmpty()) {
1361             return Collections.emptyList();
1362         }
1363         List<String> result = new ArrayList<>(signers.size());
1364         for (Signer signer : signers) {
1365             result.add(signer.getName());
1366         }
1367         return result;
1368     }
1369 
getMessageDigest(String algorithm)1370     private static MessageDigest getMessageDigest(String algorithm)
1371             throws NoSuchAlgorithmException {
1372         return MessageDigest.getInstance(algorithm);
1373     }
1374 
digest(String algorithm, byte[] data, int offset, int length)1375     private static byte[] digest(String algorithm, byte[] data, int offset, int length)
1376             throws NoSuchAlgorithmException {
1377         MessageDigest md = getMessageDigest(algorithm);
1378         md.update(data, offset, length);
1379         return md.digest();
1380     }
1381 
digest(String algorithm, byte[] data)1382     private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException {
1383         return getMessageDigest(algorithm).digest(data);
1384     }
1385 
1386     public static class NamedDigest {
1387         public final String jcaDigestAlgorithm;
1388         public final byte[] digest;
1389 
NamedDigest(String jcaDigestAlgorithm, byte[] digest)1390         private NamedDigest(String jcaDigestAlgorithm, byte[] digest) {
1391             this.jcaDigestAlgorithm = jcaDigestAlgorithm;
1392             this.digest = digest;
1393         }
1394     }
1395 
1396     public static class Result {
1397 
1398         /** Whether the APK's JAR signature verifies. */
1399         public boolean verified;
1400 
1401         /** List of APK's signers. These signers are used by Android. */
1402         public final List<SignerInfo> signers = new ArrayList<>();
1403 
1404         /**
1405          * Signers encountered in the APK but not included in the set of the APK's signers. These
1406          * signers are ignored by Android.
1407          */
1408         public final List<SignerInfo> ignoredSigners = new ArrayList<>();
1409 
1410         private final List<IssueWithParams> mWarnings = new ArrayList<>();
1411         private final List<IssueWithParams> mErrors = new ArrayList<>();
1412 
containsErrors()1413         private boolean containsErrors() {
1414             if (!mErrors.isEmpty()) {
1415                 return true;
1416             }
1417             for (SignerInfo signer : signers) {
1418                 if (signer.containsErrors()) {
1419                     return true;
1420                 }
1421             }
1422             return false;
1423         }
1424 
addError(Issue msg, Object... parameters)1425         private void addError(Issue msg, Object... parameters) {
1426             mErrors.add(new IssueWithParams(msg, parameters));
1427         }
1428 
addWarning(Issue msg, Object... parameters)1429         private void addWarning(Issue msg, Object... parameters) {
1430             mWarnings.add(new IssueWithParams(msg, parameters));
1431         }
1432 
getErrors()1433         public List<IssueWithParams> getErrors() {
1434             return mErrors;
1435         }
1436 
getWarnings()1437         public List<IssueWithParams> getWarnings() {
1438             return mWarnings;
1439         }
1440 
1441         public static class SignerInfo {
1442             public final String name;
1443             public final String signatureFileName;
1444             public final String signatureBlockFileName;
1445             public final List<X509Certificate> certChain = new ArrayList<>();
1446 
1447             private final List<IssueWithParams> mWarnings = new ArrayList<>();
1448             private final List<IssueWithParams> mErrors = new ArrayList<>();
1449 
SignerInfo( String name, String signatureBlockFileName, String signatureFileName)1450             private SignerInfo(
1451                     String name, String signatureBlockFileName, String signatureFileName) {
1452                 this.name = name;
1453                 this.signatureBlockFileName = signatureBlockFileName;
1454                 this.signatureFileName = signatureFileName;
1455             }
1456 
containsErrors()1457             private boolean containsErrors() {
1458                 return !mErrors.isEmpty();
1459             }
1460 
addError(Issue msg, Object... parameters)1461             private void addError(Issue msg, Object... parameters) {
1462                 mErrors.add(new IssueWithParams(msg, parameters));
1463             }
1464 
addWarning(Issue msg, Object... parameters)1465             private void addWarning(Issue msg, Object... parameters) {
1466                 mWarnings.add(new IssueWithParams(msg, parameters));
1467             }
1468 
getErrors()1469             public List<IssueWithParams> getErrors() {
1470                 return mErrors;
1471             }
1472 
getWarnings()1473             public List<IssueWithParams> getWarnings() {
1474                 return mWarnings;
1475             }
1476         }
1477     }
1478 
1479     private static class SignedAttributes {
1480         private Map<String, List<Asn1OpaqueObject>> mAttrs;
1481 
SignedAttributes(Collection<Attribute> attrs)1482         public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException {
1483             Map<String, List<Asn1OpaqueObject>> result = new HashMap<>(attrs.size());
1484             for (Attribute attr : attrs) {
1485                 if (result.put(attr.attrType, attr.attrValues) != null) {
1486                     throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType);
1487                 }
1488             }
1489             mAttrs = result;
1490         }
1491 
getSingleValue(String attrOid)1492         private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException {
1493             List<Asn1OpaqueObject> values = mAttrs.get(attrOid);
1494             if ((values == null) || (values.isEmpty())) {
1495                 return null;
1496             }
1497             if (values.size() > 1) {
1498                 throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values");
1499             }
1500             return values.get(0);
1501         }
1502 
getSingleObjectIdentifierValue(String attrOid)1503         public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException {
1504             Asn1OpaqueObject value = getSingleValue(attrOid);
1505             if (value == null) {
1506                 return null;
1507             }
1508             try {
1509                 return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value;
1510             } catch (Asn1DecodingException e) {
1511                 throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
1512             }
1513         }
1514 
getSingleOctetStringValue(String attrOid)1515         public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException {
1516             Asn1OpaqueObject value = getSingleValue(attrOid);
1517             if (value == null) {
1518                 return null;
1519             }
1520             try {
1521                 return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value;
1522             } catch (Asn1DecodingException e) {
1523                 throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e);
1524             }
1525         }
1526     }
1527 
1528     @Asn1Class(type = Asn1Type.CHOICE)
1529     public static class OctetStringChoice {
1530         @Asn1Field(type = Asn1Type.OCTET_STRING)
1531         public byte[] value;
1532     }
1533 
1534     @Asn1Class(type = Asn1Type.CHOICE)
1535     public static class ObjectIdentifierChoice {
1536         @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER)
1537         public String value;
1538     }
1539 }
1540