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