1 /*
2  * Copyright 2021 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 androidx.core.appdigest;
18 
19 import android.annotation.SuppressLint;
20 import android.content.Context;
21 import android.content.pm.ApkChecksum;
22 import android.content.pm.InstallSourceInfo;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.content.pm.Signature;
26 import android.os.Build;
27 import android.text.TextUtils;
28 import android.util.ArraySet;
29 import android.util.Log;
30 import android.util.SparseArray;
31 
32 import androidx.annotation.RequiresApi;
33 import androidx.concurrent.futures.ResolvableFuture;
34 
35 import com.google.common.util.concurrent.ListenableFuture;
36 
37 import org.bouncycastle.cert.X509CertificateHolder;
38 import org.bouncycastle.cms.CMSException;
39 import org.bouncycastle.cms.CMSProcessableByteArray;
40 import org.bouncycastle.cms.CMSSignedData;
41 import org.bouncycastle.cms.SignerInformation;
42 import org.bouncycastle.cms.SignerInformationStore;
43 import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
44 import org.bouncycastle.operator.OperatorCreationException;
45 import org.jspecify.annotations.NonNull;
46 import org.jspecify.annotations.Nullable;
47 
48 import java.io.ByteArrayInputStream;
49 import java.io.ByteArrayOutputStream;
50 import java.io.DataInputStream;
51 import java.io.DataOutputStream;
52 import java.io.EOFException;
53 import java.io.File;
54 import java.io.FileInputStream;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.OutputStream;
58 import java.nio.file.Files;
59 import java.security.InvalidParameterException;
60 import java.security.NoSuchAlgorithmException;
61 import java.security.SignatureException;
62 import java.security.cert.Certificate;
63 import java.security.cert.CertificateEncodingException;
64 import java.security.cert.CertificateException;
65 import java.security.cert.CertificateFactory;
66 import java.security.cert.X509Certificate;
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Set;
70 import java.util.concurrent.Executor;
71 
72 @SuppressWarnings("unchecked")
73 @RequiresApi(api = Build.VERSION_CODES.S)
74 class ChecksumsApiSImpl {
75     private static final String TAG = "ChecksumsApiSImpl";
76 
77     private static final String APK_FILE_EXTENSION = ".apk";
78     private static final String DIGESTS_FILE_EXTENSION = ".digests";
79     private static final String DIGESTS_SIGNATURE_FILE_EXTENSION = ".signature";
80 
ChecksumsApiSImpl()81     private ChecksumsApiSImpl() {}
82 
getInstallerPackageName(@onNull Context context, @NonNull String packageName)83     static @Nullable String getInstallerPackageName(@NonNull Context context,
84             @NonNull String packageName) throws PackageManager.NameNotFoundException {
85         InstallSourceInfo installSourceInfo = context.getPackageManager().getInstallSourceInfo(
86                 packageName);
87         return installSourceInfo != null ? installSourceInfo.getInitiatingPackageName() : null;
88     }
89 
getChecksums(@onNull Context context, @NonNull String packageName, boolean includeSplits, @Checksum.Type int required, @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)90     static @NonNull ListenableFuture<Checksum[]> getChecksums(@NonNull Context context,
91             @NonNull String packageName, boolean includeSplits, @Checksum.Type int required,
92             @NonNull List<Certificate> trustedInstallers, @NonNull Executor executor)
93             throws CertificateEncodingException, PackageManager.NameNotFoundException {
94         final ResolvableFuture<Checksum[]> result = ResolvableFuture.create();
95 
96         if (trustedInstallers == Checksums.TRUST_ALL) {
97             trustedInstallers = PackageManager.TRUST_ALL;
98         } else if (trustedInstallers == Checksums.TRUST_NONE) {
99             trustedInstallers = PackageManager.TRUST_NONE;
100         } else if (trustedInstallers.isEmpty()) {
101             throw new IllegalArgumentException(
102                     "trustedInstallers has to be one of TRUST_ALL/TRUST_NONE or a non-empty "
103                             + "list of certificates.");
104         }
105 
106         if (Build.VERSION.SDK_INT <= 34) {
107             // On certain U devices, this might throw NameNotFoundException even if package is
108             // present.
109             context.getPackageManager().getInstallSourceInfo(packageName);
110         }
111 
112         context.getPackageManager().requestChecksums(packageName, includeSplits, required,
113                 trustedInstallers, new PackageManager.OnChecksumsReadyListener() {
114                     @SuppressLint({"WrongConstant"})
115                     @Override
116                     public void onChecksumsReady(List<ApkChecksum> apkChecksums) {
117                         if (apkChecksums == null) {
118                             result.setException(
119                                     new IllegalStateException("Checksums missing."));
120                             return;
121                         }
122 
123                         try {
124                             Checksum[] checksums = new Checksum[apkChecksums.size()];
125                             for (int i = 0, size = apkChecksums.size(); i < size; ++i) {
126                                 ApkChecksum apkChecksum = apkChecksums.get(i);
127                                 checksums[i] = new Checksum(apkChecksum.getSplitName(),
128                                         apkChecksum.getType(), apkChecksum.getValue(),
129                                         apkChecksum.getInstallerPackageName(),
130                                         apkChecksum.getInstallerCertificate());
131                             }
132                             result.set(checksums);
133                         } catch (Throwable e) {
134                             result.setException(e);
135                         }
136                     }
137                 });
138 
139         return result;
140     }
141 
142     @SuppressLint("WrongConstant")
143     @SuppressWarnings("deprecation")
getInstallerChecksums(@onNull Context context, String split, File file, @Checksum.Type int required, @Nullable String installerPackageName, @Nullable List<Certificate> trustedInstallers, SparseArray<Checksum> checksums)144     static void getInstallerChecksums(@NonNull Context context,
145             String split, File file,
146             @Checksum.Type int required,
147             @Nullable String installerPackageName,
148             @Nullable List<Certificate> trustedInstallers,
149             SparseArray<Checksum> checksums) {
150         if (trustedInstallers == Checksums.TRUST_ALL) {
151             trustedInstallers = null;
152         } else if (trustedInstallers == Checksums.TRUST_NONE) {
153             return;
154         } else if (trustedInstallers == null || trustedInstallers.isEmpty()) {
155             throw new IllegalArgumentException(
156                     "trustedInstallers has to be one of TRUST_ALL/TRUST_NONE or a non-empty "
157                             + "list of certificates.");
158         }
159 
160         final File digestsFile = findDigestsForFile(file);
161         if (digestsFile == null) {
162             return;
163         }
164         final File signatureFile = findSignatureForDigests(digestsFile);
165 
166         try {
167             final android.content.pm.Checksum[] digests = readChecksums(digestsFile);
168             final Signature[] certs;
169             final Signature[] pastCerts;
170 
171             if (signatureFile != null) {
172                 final Certificate[] certificates = verifySignature(digests,
173                         Files.readAllBytes(signatureFile.toPath()));
174                 if (certificates == null || certificates.length == 0) {
175                     Log.e(TAG, "Error validating signature");
176                     return;
177                 }
178 
179                 certs = new Signature[certificates.length];
180                 for (int i = 0, size = certificates.length; i < size; i++) {
181                     certs[i] = new Signature(certificates[i].getEncoded());
182                 }
183 
184                 pastCerts = null;
185             } else {
186                 if (TextUtils.isEmpty(installerPackageName)) {
187                     Log.e(TAG, "Installer package is not specified.");
188                     return;
189                 }
190                 final PackageInfo installer =
191                         context.getPackageManager().getPackageInfo(installerPackageName,
192                                 PackageManager.GET_SIGNING_CERTIFICATES);
193                 if (installer == null) {
194                     Log.e(TAG, "Installer package not found.");
195                     return;
196                 }
197 
198                 // Obtaining array of certificates used for signing the installer package.
199                 certs = installer.signingInfo.getApkContentsSigners();
200                 pastCerts = installer.signingInfo.getSigningCertificateHistory();
201             }
202             if (certs == null || certs.length == 0 || certs[0] == null) {
203                 Log.e(TAG, "Can't obtain certificates.");
204                 return;
205             }
206 
207             // According to V2/V3 signing schema, the first certificate corresponds to the public
208             // key in the signing block.
209             byte[] trustedCertBytes = certs[0].toByteArray();
210 
211             final Set<Signature> trusted = convertToSet(trustedInstallers);
212 
213             if (trusted != null && !trusted.isEmpty()) {
214                 // Obtaining array of certificates used for signing the installer package.
215                 Signature trustedCert = isTrusted(certs, trusted);
216                 if (trustedCert == null) {
217                     trustedCert = isTrusted(pastCerts, trusted);
218                 }
219                 if (trustedCert == null) {
220                     return;
221                 }
222                 trustedCertBytes = trustedCert.toByteArray();
223             }
224 
225             // Append missing digests.
226             for (android.content.pm.Checksum digest : digests) {
227                 int type = digest.getType();
228                 if (checksums.indexOfKey(type) < 0) {
229                     checksums.put(type,
230                             new Checksum(split, type, digest.getValue(),
231                                     installerPackageName, trustedCertBytes));
232                 }
233             }
234         } catch (PackageManager.NameNotFoundException e) {
235             Log.e(TAG, "Installer package not found.", e);
236         } catch (IOException e) {
237             Log.e(TAG, "Error reading .digests or .signature", e);
238         } catch (NoSuchAlgorithmException | SignatureException | InvalidParameterException e) {
239             Log.e(TAG, "Error validating digests", e);
240         } catch (CertificateEncodingException e) {
241             Log.e(TAG, "Error encoding trustedInstallers", e);
242         }
243     }
244 
buildDigestsPathForApk(String codePath)245     private static String buildDigestsPathForApk(String codePath) {
246         if (!codePath.endsWith(APK_FILE_EXTENSION)) {
247             throw new IllegalStateException("Code path is not an apk " + codePath);
248         }
249         return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length())
250                 + DIGESTS_FILE_EXTENSION;
251     }
252 
buildSignaturePathForDigests(String digestsPath)253     private static String buildSignaturePathForDigests(String digestsPath) {
254         return digestsPath + DIGESTS_SIGNATURE_FILE_EXTENSION;
255     }
256 
findDigestsForFile(File targetFile)257     private static File findDigestsForFile(File targetFile) {
258         String digestsPath = buildDigestsPathForApk(targetFile.getAbsolutePath());
259         File digestsFile = new File(digestsPath);
260         return digestsFile.exists() ? digestsFile : null;
261     }
262 
findSignatureForDigests(File digestsFile)263     private static File findSignatureForDigests(File digestsFile) {
264         String signaturePath = buildSignaturePathForDigests(digestsFile.getAbsolutePath());
265         File signatureFile = new File(signaturePath);
266         return signatureFile.exists() ? signatureFile : null;
267     }
268 
readChecksums(File file)269     private static android.content.pm.Checksum[] readChecksums(File file) throws IOException {
270         try (InputStream is = new FileInputStream(file);
271              DataInputStream dis = new DataInputStream(is)) {
272             ArrayList<android.content.pm.Checksum> checksums = new ArrayList<>();
273             try {
274                 // 100 is an arbitrary very big number. We should stop at EOF.
275                 for (int i = 0; i < 100; ++i) {
276                     checksums.add(readFromStream(dis));
277                 }
278             } catch (EOFException e) {
279                 // expected
280             }
281             return checksums.toArray(new android.content.pm.Checksum[checksums.size()]);
282         }
283     }
284 
readFromStream(@onNull DataInputStream dis)285     private static android.content.pm.@NonNull Checksum readFromStream(@NonNull DataInputStream dis)
286             throws IOException {
287         final int type = dis.readInt();
288 
289         final byte[] valueBytes = new byte[dis.readInt()];
290         dis.read(valueBytes);
291         return new android.content.pm.Checksum(type, valueBytes);
292     }
293 
writeChecksums(OutputStream os, android.content.pm.Checksum[] checksums)294     private static void writeChecksums(OutputStream os, android.content.pm.Checksum[] checksums)
295             throws IOException {
296         try (DataOutputStream dos = new DataOutputStream(os)) {
297             for (android.content.pm.Checksum checksum : checksums) {
298                 writeToStream(dos, checksum);
299             }
300         }
301     }
302 
writeToStream(@onNull DataOutputStream dos, android.content.pm.@NonNull Checksum checksum)303     private static void writeToStream(@NonNull DataOutputStream dos,
304             android.content.pm.@NonNull Checksum checksum) throws IOException {
305         dos.writeInt(checksum.getType());
306 
307         final byte[] valueBytes = checksum.getValue();
308         dos.writeInt(valueBytes.length);
309         dos.write(valueBytes);
310     }
311 
convertToSet(@ullable List<Certificate> array)312     private static Set<Signature> convertToSet(@Nullable List<Certificate> array) throws
313             CertificateEncodingException {
314         if (array == null) {
315             return null;
316         }
317         final Set<Signature> set = new ArraySet<>(array.size());
318         for (Certificate item : array) {
319             set.add(new Signature(item.getEncoded()));
320         }
321         return set;
322     }
323 
isTrusted(Signature[] signatures, Set<Signature> trusted)324     private static Signature isTrusted(Signature[] signatures, Set<Signature> trusted) {
325         if (signatures == null) {
326             return null;
327         }
328         for (Signature signature : signatures) {
329             if (trusted.contains(signature)) {
330                 return signature;
331             }
332         }
333         return null;
334     }
335 
336     /**
337      * Verifies signature over binary serialized checksums.
338      * @param checksums array of checksums
339      * @param signature detached PKCS7 signature in DER format
340      * @return all certificates that passed verification
341      */
verifySignature(android.content.pm.Checksum[] checksums, byte[] signature)342     private static Certificate @NonNull [] verifySignature(android.content.pm.Checksum[] checksums,
343             byte[] signature) throws NoSuchAlgorithmException, IOException, SignatureException {
344         final byte[] blob;
345         try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
346             writeChecksums(os, checksums);
347             blob = os.toByteArray();
348         }
349 
350         try {
351             CMSSignedData cms = new CMSSignedData(new CMSProcessableByteArray(blob), signature);
352 
353             SignerInformationStore signers = cms.getSignerInfos();
354             if (signers == null || signers.size() == 0) {
355                 throw new SignatureException("Signature missing signers");
356             }
357 
358             ArrayList<Certificate> certificates = new ArrayList<>(signers.size());
359 
360             final CertificateFactory cf = CertificateFactory.getInstance("X.509");
361             for (SignerInformation signer : signers) {
362                 for (Object certHolderObject : cms.getCertificates().getMatches(signer.getSID())) {
363                     X509CertificateHolder certHolder = (X509CertificateHolder) certHolderObject;
364                     if (!signer.verify(
365                             new JcaSimpleSignerInfoVerifierBuilder().build(certHolder))) {
366                         throw new SignatureException("Verification failed");
367                     }
368                     certificates.add((X509Certificate) cf.generateCertificate(
369                             new ByteArrayInputStream(certHolder.getEncoded())));
370                 }
371             }
372 
373             return certificates.toArray(new Certificate[certificates.size()]);
374         } catch (CMSException | CertificateException | OperatorCreationException e) {
375             throw new SignatureException("Verification exception", e);
376         }
377     }
378 }
379