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