• 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 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 a APK Signature Scheme v2 signature of this APK does not verify.
102      * @throws IOException if an I/O error occurs while reading the APK file.
103      */
verify(String apkFile)104     public static X509Certificate[][] verify(String apkFile)
105             throws SignatureNotFoundException, SecurityException, IOException {
106         VerifiedSigner vSigner = verify(apkFile, true);
107         return vSigner.certs;
108     }
109 
110     /**
111      * Returns the certificates associated with each signer for the given APK without verification.
112      * This method is dangerous and should not be used, unless the caller is absolutely certain the
113      * APK is trusted.  Specifically, verification is only done for the APK Signature Scheme v2
114      * Block while gathering signer information.  The APK contents are not verified.
115      *
116      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
117      * @throws IOException if an I/O error occurs while reading the APK file.
118      */
unsafeGetCertsWithoutVerification(String apkFile)119     public static X509Certificate[][] unsafeGetCertsWithoutVerification(String apkFile)
120             throws SignatureNotFoundException, SecurityException, IOException {
121         VerifiedSigner vSigner = verify(apkFile, false);
122         return vSigner.certs;
123     }
124 
125     /**
126      * Same as above returns the full signer object, containing additional info e.g. digest.
127      */
verify(String apkFile, boolean verifyIntegrity)128     public static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
129             throws SignatureNotFoundException, SecurityException, IOException {
130         try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
131             return verify(apk, verifyIntegrity);
132         }
133     }
134 
135     /**
136      * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
137      * associated with each signer.
138      *
139      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
140      * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
141      *         verify.
142      * @throws IOException if an I/O error occurs while reading the APK file.
143      */
verify(RandomAccessFile apk, boolean verifyIntegrity)144     private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
145             throws SignatureNotFoundException, SecurityException, IOException {
146         SignatureInfo signatureInfo = findSignature(apk);
147         return verify(apk, signatureInfo, verifyIntegrity);
148     }
149 
150     /**
151      * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
152      * additional information relevant for verifying the block against the file.
153      *
154      * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
155      * @throws IOException if an I/O error occurs while reading the APK file.
156      */
findSignature(RandomAccessFile apk)157     public static SignatureInfo findSignature(RandomAccessFile apk)
158             throws IOException, SignatureNotFoundException {
159         return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
160     }
161 
162     /**
163      * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2
164      * Block.
165      *
166      * @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
167      *        against the APK file.
168      */
verify( RandomAccessFile apk, SignatureInfo signatureInfo, boolean doVerifyIntegrity)169     private static VerifiedSigner verify(
170             RandomAccessFile apk,
171             SignatureInfo signatureInfo,
172             boolean doVerifyIntegrity) throws SecurityException, IOException {
173         int signerCount = 0;
174         Map<Integer, byte[]> contentDigests = new ArrayMap<>();
175         List<X509Certificate[]> signerCerts = new ArrayList<>();
176         CertificateFactory certFactory;
177         try {
178             certFactory = CertificateFactory.getInstance("X.509");
179         } catch (CertificateException e) {
180             throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
181         }
182         ByteBuffer signers;
183         try {
184             signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
185         } catch (IOException e) {
186             throw new SecurityException("Failed to read list of signers", e);
187         }
188         while (signers.hasRemaining()) {
189             signerCount++;
190             if (signerCount > MAX_V2_SIGNERS) {
191                 throw new SecurityException(
192                         "APK Signature Scheme v2 only supports a maximum of " + MAX_V2_SIGNERS
193                                 + " signers");
194             }
195             try {
196                 ByteBuffer signer = getLengthPrefixedSlice(signers);
197                 X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
198                 signerCerts.add(certs);
199             } catch (IOException | BufferUnderflowException | SecurityException e) {
200                 throw new SecurityException(
201                         "Failed to parse/verify signer #" + signerCount + " block",
202                         e);
203             }
204         }
205 
206         if (signerCount < 1) {
207             throw new SecurityException("No signers found");
208         }
209 
210         if (contentDigests.isEmpty()) {
211             throw new SecurityException("No content digests found");
212         }
213 
214         if (doVerifyIntegrity) {
215             ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
216         }
217 
218         byte[] verityRootHash = null;
219         if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
220             byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
221             verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
222                     verityDigest, apk.getChannel().size(), signatureInfo);
223         }
224 
225         return new VerifiedSigner(
226                 signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
227                 verityRootHash, contentDigests);
228     }
229 
verifySigner( ByteBuffer signerBlock, Map<Integer, byte[]> contentDigests, CertificateFactory certFactory)230     private static X509Certificate[] verifySigner(
231             ByteBuffer signerBlock,
232             Map<Integer, byte[]> contentDigests,
233             CertificateFactory certFactory) throws SecurityException, IOException {
234         ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
235         ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
236         byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
237 
238         int signatureCount = 0;
239         int bestSigAlgorithm = -1;
240         byte[] bestSigAlgorithmSignatureBytes = null;
241         List<Integer> signaturesSigAlgorithms = new ArrayList<>();
242         while (signatures.hasRemaining()) {
243             signatureCount++;
244             try {
245                 ByteBuffer signature = getLengthPrefixedSlice(signatures);
246                 if (signature.remaining() < 8) {
247                     throw new SecurityException("Signature record too short");
248                 }
249                 int sigAlgorithm = signature.getInt();
250                 signaturesSigAlgorithms.add(sigAlgorithm);
251                 if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
252                     continue;
253                 }
254                 if ((bestSigAlgorithm == -1)
255                         || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
256                     bestSigAlgorithm = sigAlgorithm;
257                     bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
258                 }
259             } catch (IOException | BufferUnderflowException e) {
260                 throw new SecurityException(
261                         "Failed to parse signature record #" + signatureCount,
262                         e);
263             }
264         }
265         if (bestSigAlgorithm == -1) {
266             if (signatureCount == 0) {
267                 throw new SecurityException("No signatures found");
268             } else {
269                 throw new SecurityException("No supported signatures found");
270             }
271         }
272 
273         String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
274         Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
275                 getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
276         String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
277         AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
278         boolean sigVerified;
279         try {
280             PublicKey publicKey =
281                     KeyFactory.getInstance(keyAlgorithm)
282                             .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
283             Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
284             sig.initVerify(publicKey);
285             if (jcaSignatureAlgorithmParams != null) {
286                 sig.setParameter(jcaSignatureAlgorithmParams);
287             }
288             sig.update(signedData);
289             sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
290         } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
291                 | InvalidAlgorithmParameterException | SignatureException e) {
292             throw new SecurityException(
293                     "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
294         }
295         if (!sigVerified) {
296             throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
297         }
298 
299         // Signature over signedData has verified.
300 
301         byte[] contentDigest = null;
302         signedData.clear();
303         ByteBuffer digests = getLengthPrefixedSlice(signedData);
304         List<Integer> digestsSigAlgorithms = new ArrayList<>();
305         int digestCount = 0;
306         while (digests.hasRemaining()) {
307             digestCount++;
308             try {
309                 ByteBuffer digest = getLengthPrefixedSlice(digests);
310                 if (digest.remaining() < 8) {
311                     throw new IOException("Record too short");
312                 }
313                 int sigAlgorithm = digest.getInt();
314                 digestsSigAlgorithms.add(sigAlgorithm);
315                 if (sigAlgorithm == bestSigAlgorithm) {
316                     contentDigest = readLengthPrefixedByteArray(digest);
317                 }
318             } catch (IOException | BufferUnderflowException e) {
319                 throw new IOException("Failed to parse digest record #" + digestCount, e);
320             }
321         }
322 
323         if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
324             throw new SecurityException(
325                     "Signature algorithms don't match between digests and signatures records");
326         }
327         int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
328         byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
329         if ((previousSignerDigest != null)
330                 && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
331             throw new SecurityException(
332                     getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
333                     + " contents digest does not match the digest specified by a preceding signer");
334         }
335 
336         ByteBuffer certificates = getLengthPrefixedSlice(signedData);
337         List<X509Certificate> certs = new ArrayList<>();
338         int certificateCount = 0;
339         while (certificates.hasRemaining()) {
340             certificateCount++;
341             byte[] encodedCert = readLengthPrefixedByteArray(certificates);
342             X509Certificate certificate;
343             try {
344                 certificate = (X509Certificate)
345                         certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
346             } catch (CertificateException e) {
347                 throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
348             }
349             certificate = new VerbatimX509Certificate(certificate, encodedCert);
350             certs.add(certificate);
351         }
352 
353         if (certs.isEmpty()) {
354             throw new SecurityException("No certificates listed");
355         }
356         X509Certificate mainCertificate = certs.get(0);
357         byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
358         if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
359             throw new SecurityException(
360                     "Public key mismatch between certificate and signature record");
361         }
362 
363         ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
364         verifyAdditionalAttributes(additionalAttrs);
365 
366         return certs.toArray(new X509Certificate[certs.size()]);
367     }
368 
369     // Attribute to check whether a newer APK Signature Scheme signature was stripped
370     private static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
371 
verifyAdditionalAttributes(ByteBuffer attrs)372     private static void verifyAdditionalAttributes(ByteBuffer attrs)
373             throws SecurityException, IOException {
374         while (attrs.hasRemaining()) {
375             ByteBuffer attr = getLengthPrefixedSlice(attrs);
376             if (attr.remaining() < 4) {
377                 throw new IOException("Remaining buffer too short to contain additional attribute "
378                         + "ID. Remaining: " + attr.remaining());
379             }
380             int id = attr.getInt();
381             switch (id) {
382                 case STRIPPING_PROTECTION_ATTR_ID:
383                     if (attr.remaining() < 4) {
384                         throw new IOException("V2 Signature Scheme Stripping Protection Attribute "
385                                 + " value too small.  Expected 4 bytes, but found "
386                                 + attr.remaining());
387                     }
388                     int vers = attr.getInt();
389                     if (vers == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
390                         throw new SecurityException("V2 signature indicates APK is signed using APK"
391                                 + " Signature Scheme v3, but none was found. Signature stripped?");
392                     }
393                     break;
394                 default:
395                     // not the droid we're looking for, move along, move along.
396                     break;
397             }
398         }
399         return;
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