1 /*
2  * Copyright (C) 2020 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.ApkSignatureSchemeV3Verifier.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
20 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
21 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
22 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
23 import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
24 
25 import android.os.incremental.IncrementalManager;
26 import android.os.incremental.V4Signature;
27 import android.util.ArrayMap;
28 import android.util.Pair;
29 
30 import com.android.internal.security.VerityUtils;
31 
32 import java.io.ByteArrayInputStream;
33 import java.io.EOFException;
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.IOException;
37 import java.security.DigestException;
38 import java.security.InvalidAlgorithmParameterException;
39 import java.security.InvalidKeyException;
40 import java.security.KeyFactory;
41 import java.security.NoSuchAlgorithmException;
42 import java.security.PublicKey;
43 import java.security.Signature;
44 import java.security.SignatureException;
45 import java.security.cert.Certificate;
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.Arrays;
53 import java.util.Map;
54 
55 /**
56  * APK Signature Scheme v4 verifier.
57  *
58  * @hide for internal use only.
59  */
60 public class ApkSignatureSchemeV4Verifier {
61     static final int APK_SIGNATURE_SCHEME_DEFAULT = 0xffffffff;
62 
63     /**
64      * Extracts and verifies APK Signature Scheme v4 signature of the provided APK and returns the
65      * certificates associated with each signer.
66      */
extractCertificates(String apkFile)67     public static VerifiedSigner extractCertificates(String apkFile)
68             throws SignatureNotFoundException, SignatureException, SecurityException {
69         Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> pair = extractSignature(apkFile);
70         return verify(apkFile, pair.first, pair.second, APK_SIGNATURE_SCHEME_DEFAULT);
71     }
72 
73     /**
74      * Extracts APK Signature Scheme v4 signature of the provided APK.
75      */
extractSignature( String apkFile)76     public static Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> extractSignature(
77             String apkFile) throws SignatureNotFoundException, SignatureException {
78         try {
79             final File apk = new File(apkFile);
80             boolean needsConsistencyCheck;
81 
82             // 1. Try IncFS first. IncFS verifies the file according to the integrity metadata
83             // (including the root hash of Merkle tree) it keeps track of with signature check. No
84             // further consistentcy check is needed.
85             byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature(
86                     apk.getAbsolutePath());
87             V4Signature signature;
88             if (signatureBytes != null && signatureBytes.length > 0) {
89                 needsConsistencyCheck = false;
90                 signature = V4Signature.readFrom(signatureBytes);
91             } else if (android.security.Flags.extendVbChainToUpdatedApk()) {
92                 // 2. Try fs-verity next. fs-verity checks against the Merkle tree, but the
93                 // v4 signature file (including a raw root hash) is managed separately. We need to
94                 // ensure the signed data from the file is consistent with the actual file.
95                 needsConsistencyCheck = true;
96 
97                 final File idsig = new File(apk.getAbsolutePath() + V4Signature.EXT);
98                 try (var fis = new FileInputStream(idsig.getAbsolutePath())) {
99                     signature = V4Signature.readFrom(fis);
100                 } catch (IOException e) {
101                     throw new SignatureNotFoundException(
102                             "Failed to obtain signature bytes from .idsig");
103                 }
104             } else {
105                 throw new SignatureNotFoundException(
106                         "Failed to obtain signature bytes from IncFS.");
107             }
108             if (!signature.isVersionSupported()) {
109                 throw new SecurityException(
110                         "v4 signature version " + signature.version + " is not supported");
111             }
112             final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
113                     signature.hashingInfo);
114             final V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
115                     signature.signingInfos);
116 
117             if (needsConsistencyCheck) {
118                 final byte[] actualDigest = VerityUtils.getFsverityDigest(apk.getAbsolutePath());
119                 if (actualDigest == null) {
120                     throw new SecurityException("The APK does not have fs-verity");
121                 }
122                 final byte[] computedDigest =
123                         VerityUtils.generateFsVerityDigest(apk.length(), hashingInfo);
124                 if (!Arrays.equals(computedDigest, actualDigest)) {
125                     throw new SignatureException("Actual digest does not match the v4 signature");
126                 }
127             }
128 
129             return Pair.create(hashingInfo, signingInfos);
130         } catch (EOFException e) {
131             throw new SignatureException("V4 signature is invalid.", e);
132         } catch (IOException e) {
133             throw new SignatureNotFoundException("Failed to read V4 signature.", e);
134         } catch (DigestException | NoSuchAlgorithmException e) {
135             throw new SecurityException("Failed to calculate the digest", e);
136         }
137     }
138 
139     /**
140      * Verifies APK Signature Scheme v4 signature and returns the
141      * certificates associated with each signer.
142      */
verify(String apkFile, final V4Signature.HashingInfo hashingInfo, final V4Signature.SigningInfos signingInfos, final int v3BlockId)143     public static VerifiedSigner verify(String apkFile, final V4Signature.HashingInfo hashingInfo,
144             final V4Signature.SigningInfos signingInfos, final int v3BlockId)
145             throws SignatureNotFoundException, SecurityException {
146         final V4Signature.SigningInfo signingInfo = findSigningInfoForBlockId(signingInfos,
147                 v3BlockId);
148 
149         // Verify signed data and extract certificates and apk digest.
150         final byte[] signedData = V4Signature.getSignedData(new File(apkFile).length(), hashingInfo,
151                 signingInfo);
152         final Pair<Certificate, byte[]> result = verifySigner(signingInfo, signedData);
153 
154         // Populate digests enforced by IncFS driver and fs-verity.
155         Map<Integer, byte[]> contentDigests = new ArrayMap<>();
156         contentDigests.put(convertToContentDigestType(hashingInfo.hashAlgorithm),
157                 hashingInfo.rawRootHash);
158 
159         return new VerifiedSigner(new Certificate[]{result.first}, result.second, contentDigests);
160     }
161 
findSigningInfoForBlockId( final V4Signature.SigningInfos signingInfos, final int v3BlockId)162     private static V4Signature.SigningInfo findSigningInfoForBlockId(
163             final V4Signature.SigningInfos signingInfos, final int v3BlockId)
164             throws SignatureNotFoundException {
165         // Use default signingInfo for v3 block.
166         if (v3BlockId == APK_SIGNATURE_SCHEME_DEFAULT
167                 || v3BlockId == APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
168             return signingInfos.signingInfo;
169         }
170         for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
171             if (v3BlockId == signingInfoBlock.blockId) {
172                 try {
173                     return V4Signature.SigningInfo.fromByteArray(signingInfoBlock.signingInfo);
174                 } catch (IOException e) {
175                     throw new SecurityException(
176                             "Failed to read V4 signature block: " + signingInfoBlock.blockId, e);
177                 }
178             }
179         }
180         throw new SecurityException(
181                 "Failed to find V4 signature block corresponding to V3 blockId: " + v3BlockId);
182     }
183 
verifySigner(V4Signature.SigningInfo signingInfo, final byte[] signedData)184     private static Pair<Certificate, byte[]> verifySigner(V4Signature.SigningInfo signingInfo,
185             final byte[] signedData) throws SecurityException {
186         if (!isSupportedSignatureAlgorithm(signingInfo.signatureAlgorithmId)) {
187             throw new SecurityException("No supported signatures found");
188         }
189 
190         final int signatureAlgorithmId = signingInfo.signatureAlgorithmId;
191         final byte[] signatureBytes = signingInfo.signature;
192         final byte[] publicKeyBytes = signingInfo.publicKey;
193         final byte[] encodedCert = signingInfo.certificate;
194 
195         String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(signatureAlgorithmId);
196         Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
197                 getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithmId);
198         String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
199         AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
200         boolean sigVerified;
201         try {
202             PublicKey publicKey =
203                     KeyFactory.getInstance(keyAlgorithm)
204                             .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
205             Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
206             sig.initVerify(publicKey);
207             if (jcaSignatureAlgorithmParams != null) {
208                 sig.setParameter(jcaSignatureAlgorithmParams);
209             }
210             sig.update(signedData);
211             sigVerified = sig.verify(signatureBytes);
212         } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
213                 | InvalidAlgorithmParameterException | SignatureException e) {
214             throw new SecurityException(
215                     "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
216         }
217         if (!sigVerified) {
218             throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
219         }
220 
221         // Signature over signedData has verified.
222         CertificateFactory certFactory;
223         try {
224             certFactory = CertificateFactory.getInstance("X.509");
225         } catch (CertificateException e) {
226             throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
227         }
228 
229         X509Certificate certificate;
230         try {
231             certificate = (X509Certificate)
232                     certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
233         } catch (CertificateException e) {
234             throw new SecurityException("Failed to decode certificate", e);
235         }
236         certificate = new VerbatimX509Certificate(certificate, encodedCert);
237 
238         byte[] certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
239         if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
240             throw new SecurityException(
241                     "Public key mismatch between certificate and signature record");
242         }
243 
244         return Pair.create(certificate, signingInfo.apkDigest);
245     }
246 
convertToContentDigestType(int hashAlgorithm)247     private static int convertToContentDigestType(int hashAlgorithm) throws SecurityException {
248         if (hashAlgorithm == V4Signature.HASHING_ALGORITHM_SHA256) {
249             return CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
250         }
251         throw new SecurityException("Unsupported hashAlgorithm: " + hashAlgorithm);
252     }
253 
254     /**
255      * Verified APK Signature Scheme v4 signer, including V2/V3 digest.
256      *
257      * @hide for internal use only.
258      */
259     public static class VerifiedSigner {
260         public final Certificate[] certs;
261         public final byte[] apkDigest;
262 
263         // Algorithm -> digest map of signed digests in the signature.
264         // These are continuously enforced by the IncFS driver and fs-verity.
265         public final Map<Integer, byte[]> contentDigests;
266 
VerifiedSigner(Certificate[] certs, byte[] apkDigest, Map<Integer, byte[]> contentDigests)267         public VerifiedSigner(Certificate[] certs, byte[] apkDigest,
268                 Map<Integer, byte[]> contentDigests) {
269             this.certs = certs;
270             this.apkDigest = apkDigest;
271             this.contentDigests = contentDigests;
272         }
273 
274     }
275 }
276