| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.pm; |
| |
| import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256; |
| import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512; |
| import static android.content.pm.Checksum.TYPE_WHOLE_MD5; |
| import static android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256; |
| import static android.content.pm.Checksum.TYPE_WHOLE_SHA1; |
| import static android.content.pm.Checksum.TYPE_WHOLE_SHA256; |
| import static android.content.pm.Checksum.TYPE_WHOLE_SHA512; |
| import static android.content.pm.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION; |
| import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256; |
| import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512; |
| import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.content.pm.ApkChecksum; |
| import android.content.pm.Checksum; |
| import android.content.pm.IOnChecksumsReadyListener; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.Signature; |
| import android.content.pm.SigningDetails.SignatureSchemeVersion; |
| import android.content.pm.parsing.ApkLiteParseUtils; |
| import android.content.pm.parsing.result.ParseResult; |
| import android.content.pm.parsing.result.ParseTypeImpl; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.incremental.IncrementalManager; |
| import android.os.incremental.IncrementalStorage; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.apk.ApkSignatureSchemeV2Verifier; |
| import android.util.apk.ApkSignatureSchemeV3Verifier; |
| import android.util.apk.ApkSignatureSchemeV4Verifier; |
| import android.util.apk.ApkSignatureVerifier; |
| import android.util.apk.ApkSigningBlockUtils; |
| import android.util.apk.ByteBufferFactory; |
| import android.util.apk.SignatureInfo; |
| import android.util.apk.SignatureNotFoundException; |
| import android.util.apk.VerityBuilder; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.security.VerityUtils; |
| import com.android.server.pm.pkg.AndroidPackage; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.RandomAccessFile; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.file.Files; |
| import java.security.DigestException; |
| import java.security.InvalidParameterException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SignatureException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import sun.security.pkcs.PKCS7; |
| import sun.security.pkcs.SignerInfo; |
| |
| /** |
| * Provides checksums for APK. |
| */ |
| public class ApkChecksums { |
| static final String TAG = "ApkChecksums"; |
| |
| private static final String DIGESTS_FILE_EXTENSION = ".digests"; |
| private static final String DIGESTS_SIGNATURE_FILE_EXTENSION = ".signature"; |
| |
| // MessageDigest algorithms. |
| static final String ALGO_MD5 = "MD5"; |
| static final String ALGO_SHA1 = "SHA1"; |
| static final String ALGO_SHA256 = "SHA256"; |
| static final String ALGO_SHA512 = "SHA512"; |
| |
| private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = {}; |
| |
| /** |
| * Arbitrary size restriction for the signature, used to sign the checksums. |
| */ |
| private static final int MAX_SIGNATURE_SIZE_BYTES = 35 * 1024; |
| |
| /** |
| * Check back in 1 second after we detected we needed to wait for the APK to be fully available. |
| */ |
| private static final long PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS = 1000; |
| |
| /** |
| * 24 hours timeout to wait till all files are loaded. |
| */ |
| private static final long PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS = 1000 * 3600 * 24; |
| |
| /** |
| * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors. |
| * |
| * NOTE: All getters should return the same instance for every call. |
| */ |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) |
| static class Injector { |
| |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) |
| interface Producer<T> { |
| /** Produce an instance of type {@link T} */ |
| T produce(); |
| } |
| |
| private final Producer<Context> mContext; |
| private final Producer<Handler> mHandlerProducer; |
| private final Producer<IncrementalManager> mIncrementalManagerProducer; |
| private final Producer<PackageManagerInternal> mPackageManagerInternalProducer; |
| |
| Injector(Producer<Context> context, Producer<Handler> handlerProducer, |
| Producer<IncrementalManager> incrementalManagerProducer, |
| Producer<PackageManagerInternal> packageManagerInternalProducer) { |
| mContext = context; |
| mHandlerProducer = handlerProducer; |
| mIncrementalManagerProducer = incrementalManagerProducer; |
| mPackageManagerInternalProducer = packageManagerInternalProducer; |
| } |
| |
| public Context getContext() { |
| return mContext.produce(); |
| } |
| |
| public Handler getHandler() { |
| return mHandlerProducer.produce(); |
| } |
| |
| public IncrementalManager getIncrementalManager() { |
| return mIncrementalManagerProducer.produce(); |
| } |
| |
| public PackageManagerInternal getPackageManagerInternal() { |
| return mPackageManagerInternalProducer.produce(); |
| } |
| } |
| |
| /** |
| * Return the digests path associated with the given code path |
| * (replaces '.apk' extension with '.digests') |
| * |
| * @throws IllegalArgumentException if the code path is not an .apk. |
| */ |
| public static String buildDigestsPathForApk(String codePath) { |
| if (!ApkLiteParseUtils.isApkPath(codePath)) { |
| throw new IllegalStateException("Code path is not an apk " + codePath); |
| } |
| return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) |
| + DIGESTS_FILE_EXTENSION; |
| } |
| |
| /** |
| * Return the signature path associated with the given digests path. |
| * (appends '.signature' to the end) |
| */ |
| public static String buildSignaturePathForDigests(String digestsPath) { |
| return digestsPath + DIGESTS_SIGNATURE_FILE_EXTENSION; |
| } |
| |
| /** Returns true if the given file looks like containing digests or digests' signature. */ |
| public static boolean isDigestOrDigestSignatureFile(File file) { |
| final String name = file.getName(); |
| return name.endsWith(DIGESTS_FILE_EXTENSION) || name.endsWith( |
| DIGESTS_SIGNATURE_FILE_EXTENSION); |
| } |
| |
| /** |
| * Search for the digests file associated with the given target file. |
| * If it exists, the method returns the digests file; otherwise it returns null. |
| */ |
| public static File findDigestsForFile(File targetFile) { |
| String digestsPath = buildDigestsPathForApk(targetFile.getAbsolutePath()); |
| File digestsFile = new File(digestsPath); |
| return digestsFile.exists() ? digestsFile : null; |
| } |
| |
| /** |
| * Search for the signature file associated with the given digests file. |
| * If it exists, the method returns the signature file; otherwise it returns null. |
| */ |
| public static File findSignatureForDigests(File digestsFile) { |
| String signaturePath = buildSignaturePathForDigests(digestsFile.getAbsolutePath()); |
| File signatureFile = new File(signaturePath); |
| return signatureFile.exists() ? signatureFile : null; |
| } |
| |
| /** |
| * Serialize checksums to the stream in binary format. |
| */ |
| public static void writeChecksums(OutputStream os, Checksum[] checksums) |
| throws IOException { |
| try (DataOutputStream dos = new DataOutputStream(os)) { |
| for (Checksum checksum : checksums) { |
| Checksum.writeToStream(dos, checksum); |
| } |
| } |
| } |
| |
| private static Checksum[] readChecksums(File file) throws IOException { |
| try (InputStream is = new FileInputStream(file)) { |
| return readChecksums(is); |
| } |
| } |
| |
| /** |
| * Deserialize array of checksums previously stored in |
| * {@link #writeChecksums(OutputStream, Checksum[])}. |
| */ |
| public static Checksum[] readChecksums(InputStream is) throws IOException { |
| try (DataInputStream dis = new DataInputStream(is)) { |
| ArrayList<Checksum> checksums = new ArrayList<>(); |
| try { |
| // 100 is an arbitrary very big number. We should stop at EOF. |
| for (int i = 0; i < 100; ++i) { |
| checksums.add(Checksum.readFromStream(dis)); |
| } |
| } catch (EOFException e) { |
| // expected |
| } |
| return checksums.toArray(new Checksum[checksums.size()]); |
| } |
| } |
| |
| /** |
| * Verifies signature over binary serialized checksums. |
| * @param checksums array of checksums |
| * @param signature detached PKCS7 signature in DER format |
| * @return all certificates that passed verification |
| * @throws SignatureException if verification fails |
| */ |
| public static @NonNull Certificate[] verifySignature(Checksum[] checksums, byte[] signature) |
| throws NoSuchAlgorithmException, IOException, SignatureException { |
| if (signature == null || signature.length > MAX_SIGNATURE_SIZE_BYTES) { |
| throw new SignatureException("Invalid signature"); |
| } |
| |
| final byte[] blob; |
| try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { |
| writeChecksums(os, checksums); |
| blob = os.toByteArray(); |
| } |
| |
| PKCS7 pkcs7 = new PKCS7(signature); |
| |
| final Certificate[] certs = pkcs7.getCertificates(); |
| if (certs == null || certs.length == 0) { |
| throw new SignatureException("Signature missing certificates"); |
| } |
| |
| final SignerInfo[] signerInfos = pkcs7.verify(blob); |
| if (signerInfos == null || signerInfos.length == 0) { |
| throw new SignatureException("Verification failed"); |
| } |
| |
| ArrayList<Certificate> certificates = new ArrayList<>(signerInfos.length); |
| for (SignerInfo signerInfo : signerInfos) { |
| ArrayList<X509Certificate> chain = signerInfo.getCertificateChain(pkcs7); |
| if (chain == null) { |
| throw new SignatureException( |
| "Verification passed, but certification chain is empty."); |
| } |
| certificates.addAll(chain); |
| } |
| |
| return certificates.toArray(new Certificate[certificates.size()]); |
| } |
| |
| /** |
| * Fetch or calculate checksums for the collection of files. |
| * |
| * @param filesToChecksum split name, null for base and File to fetch checksums for |
| * @param optional mask to fetch readily available checksums |
| * @param required mask to forcefully calculate if not available |
| * @param installerPackageName package name of the installer of the packages |
| * @param trustedInstallers array of certificate to trust, two specific cases: |
| * null - trust anybody, |
| * [] - trust nobody. |
| * @param onChecksumsReadyListener to receive the resulting checksums |
| */ |
| public static void getChecksums(List<Pair<String, File>> filesToChecksum, |
| @Checksum.TypeMask int optional, |
| @Checksum.TypeMask int required, |
| @Nullable String installerPackageName, |
| @Nullable Certificate[] trustedInstallers, |
| @NonNull IOnChecksumsReadyListener onChecksumsReadyListener, |
| @NonNull Injector injector) { |
| List<Map<Integer, ApkChecksum>> result = new ArrayList<>(filesToChecksum.size()); |
| for (int i = 0, size = filesToChecksum.size(); i < size; ++i) { |
| final String split = filesToChecksum.get(i).first; |
| final File file = filesToChecksum.get(i).second; |
| Map<Integer, ApkChecksum> checksums = new ArrayMap<>(); |
| result.add(checksums); |
| |
| try { |
| getAvailableApkChecksums(split, file, optional | required, installerPackageName, |
| trustedInstallers, checksums, injector); |
| } catch (Throwable e) { |
| Slog.e(TAG, "Preferred checksum calculation error", e); |
| } |
| } |
| |
| long startTime = SystemClock.uptimeMillis(); |
| processRequiredChecksums(filesToChecksum, result, required, onChecksumsReadyListener, |
| injector, startTime); |
| } |
| |
| private static void processRequiredChecksums(List<Pair<String, File>> filesToChecksum, |
| List<Map<Integer, ApkChecksum>> result, |
| @Checksum.TypeMask int required, |
| @NonNull IOnChecksumsReadyListener onChecksumsReadyListener, |
| @NonNull Injector injector, |
| long startTime) { |
| final boolean timeout = |
| SystemClock.uptimeMillis() - startTime >= PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS; |
| List<ApkChecksum> allChecksums = new ArrayList<>(); |
| for (int i = 0, size = filesToChecksum.size(); i < size; ++i) { |
| final String split = filesToChecksum.get(i).first; |
| final File file = filesToChecksum.get(i).second; |
| Map<Integer, ApkChecksum> checksums = result.get(i); |
| |
| try { |
| if (!timeout || required != 0) { |
| if (needToWait(file, required, checksums, injector)) { |
| // Not ready, come back later. |
| injector.getHandler().postDelayed(() -> { |
| processRequiredChecksums(filesToChecksum, result, required, |
| onChecksumsReadyListener, injector, startTime); |
| }, PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS); |
| return; |
| } |
| |
| getRequiredApkChecksums(split, file, required, checksums); |
| } |
| allChecksums.addAll(checksums.values()); |
| } catch (Throwable e) { |
| Slog.e(TAG, "Required checksum calculation error", e); |
| } |
| } |
| |
| try { |
| onChecksumsReadyListener.onChecksumsReady(allChecksums); |
| } catch (RemoteException e) { |
| Slog.w(TAG, e); |
| } |
| } |
| |
| /** |
| * Fetch readily available checksums - enforced by kernel or provided by Installer. |
| * |
| * @param split split name, null for base |
| * @param file to fetch checksums for |
| * @param types mask to fetch checksums |
| * @param installerPackageName package name of the installer of the packages |
| * @param trustedInstallers array of certificate to trust, two specific cases: |
| * null - trust anybody, |
| * [] - trust nobody. |
| * @param checksums resulting checksums |
| */ |
| private static void getAvailableApkChecksums(String split, File file, |
| @Checksum.TypeMask int types, |
| @Nullable String installerPackageName, |
| @Nullable Certificate[] trustedInstallers, |
| Map<Integer, ApkChecksum> checksums, |
| @NonNull Injector injector) { |
| if (!file.exists()) { |
| return; |
| } |
| final String filePath = file.getAbsolutePath(); |
| |
| // Always available: FSI or IncFs. |
| if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) { |
| // Hashes in fs-verity and IncFS are always verified. |
| ApkChecksum checksum = extractHashFromFS(split, filePath); |
| if (checksum != null) { |
| checksums.put(checksum.getType(), checksum); |
| } |
| } |
| |
| // System enforced: v2/v3. |
| if (isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums) || isRequired( |
| TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) { |
| Map<Integer, ApkChecksum> v2v3checksums = extractHashFromV2V3Signature( |
| split, filePath, types); |
| if (v2v3checksums != null) { |
| checksums.putAll(v2v3checksums); |
| } |
| } |
| |
| // Note: this compares installer and system digests internally and |
| // has to be called right after all system digests are populated. |
| getInstallerChecksums(split, file, types, installerPackageName, trustedInstallers, |
| checksums, injector); |
| } |
| |
| private static void getInstallerChecksums(String split, File file, |
| @Checksum.TypeMask int types, |
| @Nullable String installerPackageName, |
| @Nullable Certificate[] trustedInstallers, |
| Map<Integer, ApkChecksum> checksums, |
| @NonNull Injector injector) { |
| if (PackageManagerServiceUtils.isInstalledByAdb(installerPackageName)) { |
| return; |
| } |
| if (trustedInstallers != null && trustedInstallers.length == 0) { |
| return; |
| } |
| |
| final File digestsFile = findDigestsForFile(file); |
| if (digestsFile == null) { |
| return; |
| } |
| final File signatureFile = findSignatureForDigests(digestsFile); |
| |
| try { |
| final Checksum[] digests = readChecksums(digestsFile); |
| final Signature[] certs; |
| final Signature[] pastCerts; |
| |
| if (signatureFile != null) { |
| final Certificate[] certificates = verifySignature(digests, |
| Files.readAllBytes(signatureFile.toPath())); |
| if (certificates == null || certificates.length == 0) { |
| Slog.e(TAG, "Error validating signature"); |
| return; |
| } |
| |
| certs = new Signature[certificates.length]; |
| for (int i = 0, size = certificates.length; i < size; i++) { |
| certs[i] = new Signature(certificates[i].getEncoded()); |
| } |
| |
| pastCerts = null; |
| } else { |
| final AndroidPackage installer = injector.getPackageManagerInternal().getPackage( |
| installerPackageName); |
| if (installer == null) { |
| Slog.e(TAG, "Installer package not found."); |
| return; |
| } |
| |
| // Obtaining array of certificates used for signing the installer package. |
| certs = installer.getSigningDetails().getSignatures(); |
| pastCerts = installer.getSigningDetails().getPastSigningCertificates(); |
| } |
| if (certs == null || certs.length == 0 || certs[0] == null) { |
| Slog.e(TAG, "Can't obtain certificates."); |
| return; |
| } |
| |
| // According to V2/V3 signing schema, the first certificate corresponds to the public |
| // key in the signing block. |
| byte[] trustedCertBytes = certs[0].toByteArray(); |
| |
| final Set<Signature> trusted = convertToSet(trustedInstallers); |
| |
| if (trusted != null && !trusted.isEmpty()) { |
| // Obtaining array of certificates used for signing the installer package. |
| Signature trustedCert = isTrusted(certs, trusted); |
| if (trustedCert == null) { |
| trustedCert = isTrusted(pastCerts, trusted); |
| } |
| if (trustedCert == null) { |
| return; |
| } |
| trustedCertBytes = trustedCert.toByteArray(); |
| } |
| |
| // Compare OS-enforced digests. |
| for (Checksum digest : digests) { |
| final ApkChecksum system = checksums.get(digest.getType()); |
| if (system != null && !Arrays.equals(system.getValue(), digest.getValue())) { |
| throw new InvalidParameterException("System digest " + digest.getType() |
| + " mismatch, can't bind installer-provided digests to the APK."); |
| } |
| } |
| |
| // Append missing digests. |
| for (Checksum digest : digests) { |
| if (isRequired(digest.getType(), types, checksums)) { |
| checksums.put(digest.getType(), |
| new ApkChecksum(split, digest, installerPackageName, trustedCertBytes)); |
| } |
| } |
| } catch (IOException e) { |
| Slog.e(TAG, "Error reading .digests or .signature", e); |
| } catch (NoSuchAlgorithmException | SignatureException | InvalidParameterException e) { |
| Slog.e(TAG, "Error validating digests. Invalid digests will be removed", e); |
| try { |
| Files.deleteIfExists(digestsFile.toPath()); |
| if (signatureFile != null) { |
| Files.deleteIfExists(signatureFile.toPath()); |
| } |
| } catch (IOException ignored) { |
| } |
| } catch (CertificateEncodingException e) { |
| Slog.e(TAG, "Error encoding trustedInstallers", e); |
| } |
| } |
| |
| /** |
| * Whether the file is available for checksumming or we need to wait. |
| */ |
| private static boolean needToWait(File file, |
| @Checksum.TypeMask int types, |
| Map<Integer, ApkChecksum> checksums, |
| @NonNull Injector injector) throws IOException { |
| if (!isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums) |
| && !isRequired(TYPE_WHOLE_MD5, types, checksums) |
| && !isRequired(TYPE_WHOLE_SHA1, types, checksums) |
| && !isRequired(TYPE_WHOLE_SHA256, types, checksums) |
| && !isRequired(TYPE_WHOLE_SHA512, types, checksums) |
| && !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums) |
| && !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) { |
| return false; |
| } |
| |
| final String filePath = file.getAbsolutePath(); |
| if (!IncrementalManager.isIncrementalPath(filePath)) { |
| return false; |
| } |
| |
| IncrementalManager manager = injector.getIncrementalManager(); |
| if (manager == null) { |
| Slog.e(TAG, "IncrementalManager is missing."); |
| return false; |
| } |
| IncrementalStorage storage = manager.openStorage(filePath); |
| if (storage == null) { |
| Slog.e(TAG, "IncrementalStorage is missing for a path on IncFs: " + filePath); |
| return false; |
| } |
| |
| return !storage.isFileFullyLoaded(filePath); |
| } |
| |
| /** |
| * Fetch or calculate checksums for the specific file. |
| * |
| * @param split split name, null for base |
| * @param file to fetch checksums for |
| * @param types mask to forcefully calculate if not available |
| * @param checksums resulting checksums |
| */ |
| private static void getRequiredApkChecksums(String split, File file, |
| @Checksum.TypeMask int types, |
| Map<Integer, ApkChecksum> checksums) { |
| final String filePath = file.getAbsolutePath(); |
| |
| // Manually calculating required checksums if not readily available. |
| if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) { |
| try { |
| byte[] generatedRootHash = VerityBuilder.generateFsVerityRootHash( |
| filePath, /*salt=*/null, |
| new ByteBufferFactory() { |
| @Override |
| public ByteBuffer create(int capacity) { |
| return ByteBuffer.allocate(capacity); |
| } |
| }); |
| checksums.put(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, |
| new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, |
| verityHashForFile(file, generatedRootHash))); |
| } catch (IOException | NoSuchAlgorithmException | DigestException e) { |
| Slog.e(TAG, "Error calculating WHOLE_MERKLE_ROOT_4K_SHA256", e); |
| } |
| } |
| |
| calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_MD5); |
| calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA1); |
| calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA256); |
| calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA512); |
| |
| calculatePartialChecksumsIfRequested(checksums, split, file, types); |
| } |
| |
| private static boolean isRequired(@Checksum.Type int type, |
| @Checksum.TypeMask int types, Map<Integer, ApkChecksum> checksums) { |
| if ((types & type) == 0) { |
| return false; |
| } |
| if (checksums.containsKey(type)) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Signature class provides a fast way to compare certificates using their hashes. |
| * The hash is exactly the same as in X509/Certificate. |
| */ |
| private static Set<Signature> convertToSet(@Nullable Certificate[] array) throws |
| CertificateEncodingException { |
| if (array == null) { |
| return null; |
| } |
| final Set<Signature> set = new ArraySet<>(array.length); |
| for (Certificate item : array) { |
| set.add(new Signature(item.getEncoded())); |
| } |
| return set; |
| } |
| |
| private static Signature isTrusted(Signature[] signatures, Set<Signature> trusted) { |
| if (signatures == null) { |
| return null; |
| } |
| for (Signature signature : signatures) { |
| if (trusted.contains(signature)) { |
| return signature; |
| } |
| } |
| return null; |
| } |
| |
| private static ApkChecksum extractHashFromFS(String split, String filePath) { |
| // verity first |
| if (VerityUtils.hasFsverity(filePath)) { |
| byte[] verityHash = VerityUtils.getFsverityDigest(filePath); |
| if (verityHash != null) { |
| return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, verityHash); |
| } |
| } |
| // v4 next |
| try { |
| ApkSignatureSchemeV4Verifier.VerifiedSigner signer = |
| ApkSignatureSchemeV4Verifier.extractCertificates(filePath); |
| byte[] rootHash = signer.contentDigests.getOrDefault( |
| CONTENT_DIGEST_VERITY_CHUNKED_SHA256, null); |
| if (rootHash != null) { |
| return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, |
| verityHashForFile(new File(filePath), rootHash)); |
| } |
| } catch (SignatureNotFoundException e) { |
| // Nothing |
| } catch (SignatureException | SecurityException e) { |
| Slog.e(TAG, "V4 signature error", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns fs-verity digest as described in |
| * https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#fs-verity-descriptor |
| * @param file the Merkle tree is built over |
| * @param rootHash Merkle tree root hash |
| */ |
| static byte[] verityHashForFile(File file, byte[] rootHash) { |
| try { |
| ByteBuffer buffer = ByteBuffer.allocate(256); // sizeof(fsverity_descriptor) |
| buffer.order(ByteOrder.LITTLE_ENDIAN); |
| buffer.put((byte) 1); // __u8 version, must be 1 |
| buffer.put((byte) 1); // __u8 hash_algorithm, FS_VERITY_HASH_ALG_SHA256 |
| buffer.put((byte) 12); // __u8, FS_VERITY_LOG_BLOCKSIZE |
| buffer.put((byte) 0); // __u8, size of salt in bytes; 0 if none |
| buffer.putInt(0); // __le32 __reserved_0x04, must be 0 |
| buffer.putLong(file.length()); // __le64 data_size |
| buffer.put(rootHash); // root_hash, first 32 bytes |
| final int padding = 32 + 32 + 144; // root_hash, last 32 bytes, we are using sha256. |
| // salt, 32 bytes |
| // reserved, 144 bytes |
| for (int i = 0; i < padding; ++i) { |
| buffer.put((byte) 0); |
| } |
| |
| buffer.flip(); |
| |
| final MessageDigest md = MessageDigest.getInstance(ALGO_SHA256); |
| md.update(buffer); |
| return md.digest(); |
| } catch (NoSuchAlgorithmException e) { |
| Slog.e(TAG, "Device does not support MessageDigest algorithm", e); |
| return null; |
| } |
| } |
| |
| private static Map<Integer, ApkChecksum> extractHashFromV2V3Signature( |
| String split, String filePath, int types) { |
| Map<Integer, byte[]> contentDigests = null; |
| final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); |
| final ParseResult<ApkSignatureVerifier.SigningDetailsWithDigests> result = |
| ApkSignatureVerifier.verifySignaturesInternal(input, filePath, |
| SignatureSchemeVersion.SIGNING_BLOCK_V2, false /*verifyFull*/); |
| if (result.isError()) { |
| if (!(result.getException() instanceof SignatureNotFoundException)) { |
| Slog.e(TAG, "Signature verification error", result.getException()); |
| } |
| } else { |
| contentDigests = result.getResult().contentDigests; |
| } |
| |
| if (contentDigests == null) { |
| return null; |
| } |
| |
| Map<Integer, ApkChecksum> checksums = new ArrayMap<>(); |
| if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0) { |
| byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA256, null); |
| if (hash != null) { |
| checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, |
| new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, hash)); |
| } |
| } |
| if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0) { |
| byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA512, null); |
| if (hash != null) { |
| checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, |
| new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, hash)); |
| } |
| } |
| return checksums; |
| } |
| |
| private static String getMessageDigestAlgoForChecksumKind(int type) |
| throws NoSuchAlgorithmException { |
| switch (type) { |
| case TYPE_WHOLE_MD5: |
| return ALGO_MD5; |
| case TYPE_WHOLE_SHA1: |
| return ALGO_SHA1; |
| case TYPE_WHOLE_SHA256: |
| return ALGO_SHA256; |
| case TYPE_WHOLE_SHA512: |
| return ALGO_SHA512; |
| default: |
| throw new NoSuchAlgorithmException("Invalid checksum type: " + type); |
| } |
| } |
| |
| private static void calculateChecksumIfRequested(Map<Integer, ApkChecksum> checksums, |
| String split, File file, int required, int type) { |
| if ((required & type) != 0 && !checksums.containsKey(type)) { |
| final byte[] checksum = getApkChecksum(file, type); |
| if (checksum != null) { |
| checksums.put(type, new ApkChecksum(split, type, checksum)); |
| } |
| } |
| } |
| |
| static final int MIN_BUFFER_SIZE = 4 * 1024; |
| static final int MAX_BUFFER_SIZE = 128 * 1024; |
| |
| private static byte[] getApkChecksum(File file, int type) { |
| final int bufferSize = (int) Math.max(MIN_BUFFER_SIZE, |
| Math.min(MAX_BUFFER_SIZE, file.length())); |
| try (FileInputStream fis = new FileInputStream(file)) { |
| final byte[] buffer = new byte[bufferSize]; |
| int nread = 0; |
| |
| final String algo = getMessageDigestAlgoForChecksumKind(type); |
| MessageDigest md = MessageDigest.getInstance(algo); |
| while ((nread = fis.read(buffer)) != -1) { |
| md.update(buffer, 0, nread); |
| } |
| |
| return md.digest(); |
| } catch (IOException e) { |
| Slog.e(TAG, "Error reading " + file.getAbsolutePath() + " to compute hash.", e); |
| return null; |
| } catch (NoSuchAlgorithmException e) { |
| Slog.e(TAG, "Device does not support MessageDigest algorithm", e); |
| return null; |
| } |
| } |
| |
| private static int[] getContentDigestAlgos(boolean needSignatureSha256, |
| boolean needSignatureSha512) { |
| if (needSignatureSha256 && needSignatureSha512) { |
| // Signature block present, but no digests??? |
| return new int[]{CONTENT_DIGEST_CHUNKED_SHA256, CONTENT_DIGEST_CHUNKED_SHA512}; |
| } else if (needSignatureSha256) { |
| return new int[]{CONTENT_DIGEST_CHUNKED_SHA256}; |
| } else { |
| return new int[]{CONTENT_DIGEST_CHUNKED_SHA512}; |
| } |
| } |
| |
| private static int getChecksumKindForContentDigestAlgo(int contentDigestAlgo) { |
| switch (contentDigestAlgo) { |
| case CONTENT_DIGEST_CHUNKED_SHA256: |
| return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256; |
| case CONTENT_DIGEST_CHUNKED_SHA512: |
| return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512; |
| default: |
| return -1; |
| } |
| } |
| |
| private static void calculatePartialChecksumsIfRequested(Map<Integer, ApkChecksum> checksums, |
| String split, File file, int required) { |
| boolean needSignatureSha256 = |
| (required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0 && !checksums.containsKey( |
| TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256); |
| boolean needSignatureSha512 = |
| (required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0 && !checksums.containsKey( |
| TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512); |
| if (!needSignatureSha256 && !needSignatureSha512) { |
| return; |
| } |
| |
| try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { |
| SignatureInfo signatureInfo = null; |
| try { |
| signatureInfo = ApkSignatureSchemeV3Verifier.findSignature(raf); |
| } catch (SignatureNotFoundException e) { |
| try { |
| signatureInfo = ApkSignatureSchemeV2Verifier.findSignature(raf); |
| } catch (SignatureNotFoundException ee) { |
| } |
| } |
| if (signatureInfo == null) { |
| Slog.e(TAG, "V2/V3 signatures not found in " + file.getAbsolutePath()); |
| return; |
| } |
| |
| final int[] digestAlgos = getContentDigestAlgos(needSignatureSha256, |
| needSignatureSha512); |
| byte[][] digests = ApkSigningBlockUtils.computeContentDigestsPer1MbChunk(digestAlgos, |
| raf.getFD(), signatureInfo); |
| for (int i = 0, size = digestAlgos.length; i < size; ++i) { |
| int checksumKind = getChecksumKindForContentDigestAlgo(digestAlgos[i]); |
| if (checksumKind != -1) { |
| checksums.put(checksumKind, new ApkChecksum(split, checksumKind, digests[i])); |
| } |
| } |
| } catch (IOException | DigestException e) { |
| Slog.e(TAG, "Error computing hash.", e); |
| } |
| } |
| } |