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