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