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 android.util.apk;
18 
19 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
20 import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
21 import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
22 import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
23 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
24 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
25 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
26 import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
27 import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
28 
29 import android.util.ArrayMap;
30 import android.util.Pair;
31 
32 import java.io.ByteArrayInputStream;
33 import java.io.IOException;
34 import java.io.RandomAccessFile;
35 import java.nio.BufferUnderflowException;
36 import java.nio.ByteBuffer;
37 import java.security.DigestException;
38 import java.security.InvalidAlgorithmParameterException;
39 import java.security.InvalidKeyException;
40 import java.security.KeyFactory;
41 import java.security.MessageDigest;
42 import java.security.NoSuchAlgorithmException;
43 import java.security.PublicKey;
44 import java.security.Signature;
45 import java.security.SignatureException;
46 import java.security.cert.CertificateException;
47 import java.security.cert.CertificateFactory;
48 import java.security.cert.X509Certificate;
49 import java.security.spec.AlgorithmParameterSpec;
50 import java.security.spec.InvalidKeySpecException;
51 import java.security.spec.X509EncodedKeySpec;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.List;
55 import java.util.Map;
56 
57 /**
58  * APK Signature Scheme v2 verifier.
59  *
60  * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
61  * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
62  * uncompressed contents of ZIP entries.
63  *
64  * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
65  *
66  * @hide for internal use only.
67  */
68 public class ApkSignatureSchemeV2Verifier {
69 
70     /**
71      * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
72      */
73     public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2;
74 
75     private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
76 
77     /**
78      * The maximum number of signers supported by the v2 APK signature scheme.
79      */
80     private static final int MAX_V2_SIGNERS = 10;
81 
82     /**
83      * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
84      *
85      * <p><b>NOTE: This method does not verify the signature.</b>
86      */
hasSignature(String apkFile)87     public static boolean hasSignature(String apkFile) throws IOException {
88         try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
89             findSignature(apk);
90             return true;
91         } catch (SignatureNotFoundException e) {
92             return false;
93         }
94     }
95 
96     /**
97      * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
98      * associated with each signer.
99      *
100      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
101      * @throws SecurityException          if an APK Signature Scheme v2 signature of this APK does
102      *                                    not verify.
103      * @throws IOException                if an I/O error occurs while reading the APK file.
104      */
verify(String apkFile)105     public static X509Certificate[][] verify(String apkFile)
106             throws SignatureNotFoundException, SecurityException, IOException {
107         VerifiedSigner vSigner = verify(apkFile, true);
108         return vSigner.certs;
109     }
110 
111     /**
112      * Returns the certificates associated with each signer for the given APK without verification.
113      * This method is dangerous and should not be used, unless the caller is absolutely certain the
114      * APK is trusted.  Specifically, verification is only done for the APK Signature Scheme v2
115      * Block while gathering signer information.  The APK contents are not verified.
116      *
117      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
118      * @throws IOException if an I/O error occurs while reading the APK file.
119      */
unsafeGetCertsWithoutVerification(String apkFile)120     public static X509Certificate[][] unsafeGetCertsWithoutVerification(String apkFile)
121             throws SignatureNotFoundException, SecurityException, IOException {
122         VerifiedSigner vSigner = verify(apkFile, false);
123         return vSigner.certs;
124     }
125 
126     /**
127      * Same as above returns the full signer object, containing additional info e.g. digest.
128      */
verify(String apkFile, boolean verifyIntegrity)129     public static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
130             throws SignatureNotFoundException, SecurityException, IOException {
131         try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
132             return verify(apk, verifyIntegrity);
133         }
134     }
135 
136     /**
137      * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
138      * associated with each signer.
139      *
140      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
141      * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
142      *         verify.
143      * @throws IOException if an I/O error occurs while reading the APK file.
144      */
verify(RandomAccessFile apk, boolean verifyIntegrity)145     private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
146             throws SignatureNotFoundException, SecurityException, IOException {
147         SignatureInfo signatureInfo = findSignature(apk);
148         return verify(apk, signatureInfo, verifyIntegrity);
149     }
150 
151     /**
152      * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
153      * additional information relevant for verifying the block against the file.
154      *
155      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
156      * @throws IOException if an I/O error occurs while reading the APK file.
157      */
findSignature(RandomAccessFile apk)158     public static SignatureInfo findSignature(RandomAccessFile apk)
159             throws IOException, SignatureNotFoundException {
160         return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
161     }
162 
163     /**
164      * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2
165      * Block.
166      *
167      * @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
168      *        against the APK file.
169      */
verify( RandomAccessFile apk, SignatureInfo signatureInfo, boolean doVerifyIntegrity)170     private static VerifiedSigner verify(
171             RandomAccessFile apk,
172             SignatureInfo signatureInfo,
173             boolean doVerifyIntegrity) throws SecurityException, IOException {
174         int signerCount = 0;
175         Map<Integer, byte[]> contentDigests = new ArrayMap<>();
176         List<X509Certificate[]> signerCerts = new ArrayList<>();
177         CertificateFactory certFactory;
178         try {
179             certFactory = CertificateFactory.getInstance("X.509");
180         } catch (CertificateException e) {
181             throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
182         }
183         ByteBuffer signers;
184         try {
185             signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
186         } catch (IOException e) {
187             throw new SecurityException("Failed to read list of signers", e);
188         }
189         while (signers.hasRemaining()) {
190             signerCount++;
191             if (signerCount > MAX_V2_SIGNERS) {
192                 throw new SecurityException(
193                         "APK Signature Scheme v2 only supports a maximum of " + MAX_V2_SIGNERS
194                                 + " signers");
195             }
196             try {
197                 ByteBuffer signer = getLengthPrefixedSlice(signers);
198                 X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
199                 signerCerts.add(certs);
200             } catch (IOException | BufferUnderflowException | SecurityException e) {
201                 throw new SecurityException(
202                         "Failed to parse/verify signer #" + signerCount + " block",
203                         e);
204             }
205         }
206 
207         if (signerCount < 1) {
208             throw new SecurityException("No signers found");
209         }
210 
211         if (contentDigests.isEmpty()) {
212             throw new SecurityException("No content digests found");
213         }
214 
215         if (doVerifyIntegrity) {
216             ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
217         }
218 
219         byte[] verityRootHash = null;
220         if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
221             byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
222             verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
223                     verityDigest, apk.getChannel().size(), signatureInfo);
224         }
225 
226         return new VerifiedSigner(
227                 signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
228                 verityRootHash, contentDigests);
229     }
230 
verifySigner( ByteBuffer signerBlock, Map<Integer, byte[]> contentDigests, CertificateFactory certFactory)231     private static X509Certificate[] verifySigner(
232             ByteBuffer signerBlock,
233             Map<Integer, byte[]> contentDigests,
234             CertificateFactory certFactory) throws SecurityException, IOException {
235         ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
236         ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
237         byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
238 
239         int signatureCount = 0;
240         int bestSigAlgorithm = -1;
241         byte[] bestSigAlgorithmSignatureBytes = null;
242         List<Integer> signaturesSigAlgorithms = new ArrayList<>();
243         while (signatures.hasRemaining()) {
244             signatureCount++;
245             try {
246                 ByteBuffer signature = getLengthPrefixedSlice(signatures);
247                 if (signature.remaining() < 8) {
248                     throw new SecurityException("Signature record too short");
249                 }
250                 int sigAlgorithm = signature.getInt();
251                 signaturesSigAlgorithms.add(sigAlgorithm);
252                 if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
253                     continue;
254                 }
255                 if ((bestSigAlgorithm == -1)
256                         || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
257                     bestSigAlgorithm = sigAlgorithm;
258                     bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
259                 }
260             } catch (IOException | BufferUnderflowException e) {
261                 throw new SecurityException(
262                         "Failed to parse signature record #" + signatureCount,
263                         e);
264             }
265         }
266         if (bestSigAlgorithm == -1) {
267             if (signatureCount == 0) {
268                 throw new SecurityException("No signatures found");
269             } else {
270                 throw new SecurityException("No supported signatures found");
271             }
272         }
273 
274         String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
275         Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
276                 getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
277         String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
278         AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
279         boolean sigVerified;
280         try {
281             PublicKey publicKey =
282                     KeyFactory.getInstance(keyAlgorithm)
283                             .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
284             Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
285             sig.initVerify(publicKey);
286             if (jcaSignatureAlgorithmParams != null) {
287                 sig.setParameter(jcaSignatureAlgorithmParams);
288             }
289             sig.update(signedData);
290             sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
291         } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
292                 | InvalidAlgorithmParameterException | SignatureException e) {
293             throw new SecurityException(
294                     "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
295         }
296         if (!sigVerified) {
297             throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
298         }
299 
300         // Signature over signedData has verified.
301 
302         byte[] contentDigest = null;
303         signedData.clear();
304         ByteBuffer digests = getLengthPrefixedSlice(signedData);
305         List<Integer> digestsSigAlgorithms = new ArrayList<>();
306         int digestCount = 0;
307         while (digests.hasRemaining()) {
308             digestCount++;
309             try {
310                 ByteBuffer digest = getLengthPrefixedSlice(digests);
311                 if (digest.remaining() < 8) {
312                     throw new IOException("Record too short");
313                 }
314                 int sigAlgorithm = digest.getInt();
315                 digestsSigAlgorithms.add(sigAlgorithm);
316                 if (sigAlgorithm == bestSigAlgorithm) {
317                     contentDigest = readLengthPrefixedByteArray(digest);
318                 }
319             } catch (IOException | BufferUnderflowException e) {
320                 throw new IOException("Failed to parse digest record #" + digestCount, e);
321             }
322         }
323 
324         if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
325             throw new SecurityException(
326                     "Signature algorithms don't match between digests and signatures records");
327         }
328         int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
329         byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
330         if ((previousSignerDigest != null)
331                 && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
332             throw new SecurityException(
333                     getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
334                     + " contents digest does not match the digest specified by a preceding signer");
335         }
336 
337         ByteBuffer certificates = getLengthPrefixedSlice(signedData);
338         List<X509Certificate> certs = new ArrayList<>();
339         int certificateCount = 0;
340         while (certificates.hasRemaining()) {
341             certificateCount++;
342             byte[] encodedCert = readLengthPrefixedByteArray(certificates);
343             X509Certificate certificate;
344             try {
345                 certificate = (X509Certificate)
346                         certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
347             } catch (CertificateException e) {
348                 throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
349             }
350             certificate = new VerbatimX509Certificate(certificate, encodedCert);
351             certs.add(certificate);
352         }
353 
354         if (certs.isEmpty()) {
355             throw new SecurityException("No certificates listed");
356         }
357         X509Certificate mainCertificate = certs.get(0);
358         byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
359         if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
360             throw new SecurityException(
361                     "Public key mismatch between certificate and signature record");
362         }
363 
364         ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
365         verifyAdditionalAttributes(additionalAttrs);
366 
367         return certs.toArray(new X509Certificate[certs.size()]);
368     }
369 
370     // Attribute to check whether a newer APK Signature Scheme signature was stripped
371     private static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
372 
verifyAdditionalAttributes(ByteBuffer attrs)373     private static void verifyAdditionalAttributes(ByteBuffer attrs)
374             throws SecurityException, IOException {
375         while (attrs.hasRemaining()) {
376             ByteBuffer attr = getLengthPrefixedSlice(attrs);
377             if (attr.remaining() < 4) {
378                 throw new IOException("Remaining buffer too short to contain additional attribute "
379                         + "ID. Remaining: " + attr.remaining());
380             }
381             int id = attr.getInt();
382             switch (id) {
383                 case STRIPPING_PROTECTION_ATTR_ID:
384                     if (attr.remaining() < 4) {
385                         throw new IOException("V2 Signature Scheme Stripping Protection Attribute "
386                                 + " value too small.  Expected 4 bytes, but found "
387                                 + attr.remaining());
388                     }
389                     int vers = attr.getInt();
390                     if (vers == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
391                         throw new SecurityException("V2 signature indicates APK is signed using APK"
392                                 + " Signature Scheme v3, but none was found. Signature stripped?");
393                     }
394                     break;
395                 default:
396                     // not the droid we're looking for, move along, move along.
397                     break;
398             }
399         }
400     }
401 
getVerityRootHash(String apkPath)402     static byte[] getVerityRootHash(String apkPath)
403             throws IOException, SignatureNotFoundException, SecurityException {
404         try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
405             SignatureInfo signatureInfo = findSignature(apk);
406             VerifiedSigner vSigner = verify(apk, false);
407             return vSigner.verityRootHash;
408         }
409     }
410 
generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)411     static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
412             throws IOException, SignatureNotFoundException, SecurityException, DigestException,
413                    NoSuchAlgorithmException {
414         try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
415             SignatureInfo signatureInfo = findSignature(apk);
416             return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
417         }
418     }
419 
420     /**
421      * Verified APK Signature Scheme v2 signer.
422      *
423      * @hide for internal use only.
424      */
425     public static class VerifiedSigner {
426         public final X509Certificate[][] certs;
427 
428         public final byte[] verityRootHash;
429         // Algorithm -> digest map of signed digests in the signature.
430         // All these are verified if requested.
431         public final Map<Integer, byte[]> contentDigests;
432 
VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash, Map<Integer, byte[]> contentDigests)433         public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash,
434                 Map<Integer, byte[]> contentDigests) {
435             this.certs = certs;
436             this.verityRootHash = verityRootHash;
437             this.contentDigests = contentDigests;
438         }
439 
440     }
441 }
442