APK digest API and initial implementation.
Bug: 160605420
Test: atest ChecksumsTest
Change-Id: I08ef0b131c44313f2c6acddad00dfa03598cc1ff
diff --git a/api/current.txt b/api/current.txt
index c6af6be..e5baa7c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -11617,6 +11617,16 @@
field public int version;
}
+ public final class FileChecksum implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getKind();
+ method @Nullable public java.security.cert.Certificate getSourceCertificate() throws java.security.cert.CertificateException;
+ method @Nullable public String getSplitName();
+ method @NonNull public byte[] getValue();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.FileChecksum> CREATOR;
+ }
+
public final class InstallSourceInfo implements android.os.Parcelable {
method public int describeContents();
method @Nullable public String getInitiatingPackageName();
@@ -11992,6 +12002,7 @@
method @Nullable public abstract android.graphics.drawable.Drawable getApplicationLogo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull public CharSequence getBackgroundPermissionOptionLabel();
method @Nullable public abstract android.content.pm.ChangedPackages getChangedPackages(@IntRange(from=0) int);
+ method public void getChecksums(@NonNull String, boolean, int, @Nullable java.util.List<java.security.cert.Certificate>, @NonNull android.content.IntentSender) throws java.security.cert.CertificateEncodingException, java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
method public abstract int getComponentEnabledSetting(@NonNull android.content.ComponentName);
method @NonNull public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method @Nullable public abstract android.graphics.drawable.Drawable getDrawable(@NonNull String, @DrawableRes int, @Nullable android.content.pm.ApplicationInfo);
@@ -12082,6 +12093,7 @@
field public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3; // 0x3
field public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; // 0x1
field public static final int DONT_KILL_APP = 1; // 0x1
+ field public static final String EXTRA_CHECKSUMS = "android.content.pm.extra.CHECKSUMS";
field public static final String EXTRA_VERIFICATION_ID = "android.content.pm.extra.VERIFICATION_ID";
field public static final String EXTRA_VERIFICATION_RESULT = "android.content.pm.extra.VERIFICATION_RESULT";
field public static final String FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS = "android.software.activities_on_secondary_displays";
@@ -12236,6 +12248,8 @@
field public static final int MATCH_SYSTEM_ONLY = 1048576; // 0x100000
field public static final int MATCH_UNINSTALLED_PACKAGES = 8192; // 0x2000
field public static final long MAXIMUM_VERIFICATION_TIMEOUT = 3600000L; // 0x36ee80L
+ field public static final int PARTIAL_MERKLE_ROOT_1M_SHA256 = 32; // 0x20
+ field public static final int PARTIAL_MERKLE_ROOT_1M_SHA512 = 64; // 0x40
field public static final int PERMISSION_DENIED = -1; // 0xffffffff
field public static final int PERMISSION_GRANTED = 0; // 0x0
field public static final int SIGNATURE_FIRST_NOT_SIGNED = -1; // 0xffffffff
@@ -12245,9 +12259,16 @@
field public static final int SIGNATURE_SECOND_NOT_SIGNED = -2; // 0xfffffffe
field public static final int SIGNATURE_UNKNOWN_PACKAGE = -4; // 0xfffffffc
field public static final int SYNCHRONOUS = 2; // 0x2
+ field @Nullable public static final java.util.List<java.security.cert.Certificate> TRUST_ALL;
+ field @NonNull public static final java.util.List<java.security.cert.Certificate> TRUST_NONE;
field public static final int VERIFICATION_ALLOW = 1; // 0x1
field public static final int VERIFICATION_REJECT = -1; // 0xffffffff
field public static final int VERSION_CODE_HIGHEST = -1; // 0xffffffff
+ field public static final int WHOLE_MD5 = 2; // 0x2
+ field public static final int WHOLE_MERKLE_ROOT_4K_SHA256 = 1; // 0x1
+ field public static final int WHOLE_SHA1 = 4; // 0x4
+ field public static final int WHOLE_SHA256 = 8; // 0x8
+ field public static final int WHOLE_SHA512 = 16; // 0x10
}
public static class PackageManager.NameNotFoundException extends android.util.AndroidException {
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 340d5a1..2780036 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -73,6 +73,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.ParcelableException;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
@@ -107,7 +108,11 @@
import libcore.util.EmptyArray;
+import java.io.IOException;
import java.lang.ref.WeakReference;
+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.Collections;
@@ -135,6 +140,12 @@
// Default flags to use with PackageManager when no flags are given.
private static final int sDefaultFlags = GET_SHARED_LIBRARY_FILES;
+ /** Default set of checksums - includes all available checksums.
+ * @see PackageManager#getChecksums */
+ private static final int DEFAULT_CHECKSUMS =
+ WHOLE_MERKLE_ROOT_4K_SHA256 | WHOLE_MD5 | WHOLE_SHA1 | WHOLE_SHA256 | WHOLE_SHA512
+ | PARTIAL_MERKLE_ROOT_1M_SHA256 | PARTIAL_MERKLE_ROOT_1M_SHA512;
+
// Name of the resource which provides background permission button string
public static final String APP_PERMISSION_BUTTON_ALLOW_ALWAYS =
"app_permission_button_allow_always";
@@ -945,6 +956,39 @@
}
}
+ private static List<byte[]> encodeCertificates(List<Certificate> certs) throws
+ CertificateEncodingException {
+ if (certs == null) {
+ return null;
+ }
+ List<byte[]> result = new ArrayList<>(certs.size());
+ for (Certificate cert : certs) {
+ if (!(cert instanceof X509Certificate)) {
+ throw new CertificateEncodingException("Only X509 certificates supported.");
+ }
+ result.add(cert.getEncoded());
+ }
+ return result;
+ }
+
+ @Override
+ public void getChecksums(@NonNull String packageName, boolean includeSplits,
+ @FileChecksumKind int required, @Nullable List<Certificate> trustedInstallers,
+ @NonNull IntentSender statusReceiver)
+ throws CertificateEncodingException, IOException, NameNotFoundException {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(statusReceiver);
+ try {
+ mPM.getChecksums(packageName, includeSplits, DEFAULT_CHECKSUMS, required,
+ encodeCertificates(trustedInstallers), statusReceiver, getUserId());
+ } catch (ParcelableException e) {
+ e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ throw new RuntimeException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Wrap the cached value in a class that does deep compares on string
* arrays. The comparison is needed only for the verification mode of
diff --git a/core/java/android/content/pm/FileChecksum.aidl b/core/java/android/content/pm/FileChecksum.aidl
new file mode 100644
index 0000000..109f211
--- /dev/null
+++ b/core/java/android/content/pm/FileChecksum.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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 android.content.pm;
+
+parcelable FileChecksum;
+
diff --git a/core/java/android/content/pm/FileChecksum.java b/core/java/android/content/pm/FileChecksum.java
new file mode 100644
index 0000000..55430c2
--- /dev/null
+++ b/core/java/android/content/pm/FileChecksum.java
@@ -0,0 +1,238 @@
+/*
+ * 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 android.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.IntentSender;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.DataClass;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+/**
+ * A typed checksum.
+ *
+ * @see PackageManager#getChecksums(String, boolean, int, List, IntentSender)
+ */
+@DataClass(genHiddenConstructor = true)
+public final class FileChecksum implements Parcelable {
+ /**
+ * Checksum for which split. Null indicates base.apk.
+ */
+ private final @Nullable String mSplitName;
+ /**
+ * Checksum kind.
+ */
+ private final @PackageManager.FileChecksumKind int mKind;
+ /**
+ * Checksum value.
+ */
+ private final @NonNull byte[] mValue;
+ /**
+ * For Installer-provided checksums, certificate of the Installer/AppStore.
+ */
+ private final @Nullable byte[] mSourceCertificate;
+
+ /**
+ * Constructor, internal use only
+ *
+ * @hide
+ */
+ public FileChecksum(@Nullable String splitName, @PackageManager.FileChecksumKind int kind,
+ @NonNull byte[] value) {
+ this(splitName, kind, value, (byte[]) null);
+ }
+
+ /**
+ * Constructor, internal use only
+ *
+ * @hide
+ */
+ public FileChecksum(@Nullable String splitName, @PackageManager.FileChecksumKind int kind,
+ @NonNull byte[] value, @Nullable Certificate sourceCertificate)
+ throws CertificateEncodingException {
+ this(splitName, kind, value,
+ (sourceCertificate != null) ? sourceCertificate.getEncoded() : null);
+ }
+
+ /**
+ * Certificate of the source of this checksum.
+ * @throws CertificateException in case when certificate can't be re-created from serialized
+ * data.
+ */
+ public @Nullable Certificate getSourceCertificate() throws CertificateException {
+ if (mSourceCertificate == null) {
+ return null;
+ }
+ final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ final InputStream is = new ByteArrayInputStream(mSourceCertificate);
+ final X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
+ return cert;
+ }
+
+
+
+ // Code below generated by codegen v1.0.15.
+ //
+ // DO NOT MODIFY!
+ // CHECKSTYLE:OFF Generated code
+ //
+ // To regenerate run:
+ // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/content/pm/FileChecksum.java
+ //
+ // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+ // Settings > Editor > Code Style > Formatter Control
+ //@formatter:off
+
+
+ /**
+ * Creates a new FileChecksum.
+ *
+ * @param splitName
+ * Checksum for which split. Null indicates base.apk.
+ * @param kind
+ * Checksum kind.
+ * @param value
+ * Checksum value.
+ * @param sourceCertificate
+ * For Installer-provided checksums, certificate of the Installer/AppStore.
+ * @hide
+ */
+ @DataClass.Generated.Member
+ public FileChecksum(
+ @Nullable String splitName,
+ @PackageManager.FileChecksumKind int kind,
+ @NonNull byte[] value,
+ @Nullable byte[] sourceCertificate) {
+ this.mSplitName = splitName;
+ this.mKind = kind;
+ com.android.internal.util.AnnotationValidations.validate(
+ PackageManager.FileChecksumKind.class, null, mKind);
+ this.mValue = value;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mValue);
+ this.mSourceCertificate = sourceCertificate;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ /**
+ * Checksum for which split. Null indicates base.apk.
+ */
+ @DataClass.Generated.Member
+ public @Nullable String getSplitName() {
+ return mSplitName;
+ }
+
+ /**
+ * Checksum kind.
+ */
+ @DataClass.Generated.Member
+ public @PackageManager.FileChecksumKind int getKind() {
+ return mKind;
+ }
+
+ /**
+ * Checksum value.
+ */
+ @DataClass.Generated.Member
+ public @NonNull byte[] getValue() {
+ return mValue;
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ // You can override field parcelling by defining methods like:
+ // void parcelFieldName(Parcel dest, int flags) { ... }
+
+ byte flg = 0;
+ if (mSplitName != null) flg |= 0x1;
+ if (mSourceCertificate != null) flg |= 0x8;
+ dest.writeByte(flg);
+ if (mSplitName != null) dest.writeString(mSplitName);
+ dest.writeInt(mKind);
+ dest.writeByteArray(mValue);
+ if (mSourceCertificate != null) dest.writeByteArray(mSourceCertificate);
+ }
+
+ @Override
+ @DataClass.Generated.Member
+ public int describeContents() { return 0; }
+
+ /** @hide */
+ @SuppressWarnings({"unchecked", "RedundantCast"})
+ @DataClass.Generated.Member
+ /* package-private */ FileChecksum(@NonNull Parcel in) {
+ // You can override field unparcelling by defining methods like:
+ // static FieldType unparcelFieldName(Parcel in) { ... }
+
+ byte flg = in.readByte();
+ String splitName = (flg & 0x1) == 0 ? null : in.readString();
+ int kind = in.readInt();
+ byte[] value = in.createByteArray();
+ byte[] sourceCertificate = (flg & 0x8) == 0 ? null : in.createByteArray();
+
+ this.mSplitName = splitName;
+ this.mKind = kind;
+ com.android.internal.util.AnnotationValidations.validate(
+ PackageManager.FileChecksumKind.class, null, mKind);
+ this.mValue = value;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mValue);
+ this.mSourceCertificate = sourceCertificate;
+
+ // onConstructed(); // You can define this method to get a callback
+ }
+
+ @DataClass.Generated.Member
+ public static final @NonNull Parcelable.Creator<FileChecksum> CREATOR
+ = new Parcelable.Creator<FileChecksum>() {
+ @Override
+ public FileChecksum[] newArray(int size) {
+ return new FileChecksum[size];
+ }
+
+ @Override
+ public FileChecksum createFromParcel(@NonNull Parcel in) {
+ return new FileChecksum(in);
+ }
+ };
+
+ @DataClass.Generated(
+ time = 1598322801861L,
+ codegenVersion = "1.0.15",
+ sourceFile = "frameworks/base/core/java/android/content/pm/FileChecksum.java",
+ inputSignatures = "private final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.content.pm.PackageManager.FileChecksumKind int mKind\nprivate final @android.annotation.NonNull byte[] mValue\nprivate final @android.annotation.Nullable byte[] mSourceCertificate\npublic @android.annotation.Nullable java.security.cert.Certificate getSourceCertificate()\nclass FileChecksum extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genHiddenConstructor=true)")
+ @Deprecated
+ private void __metadata() {}
+
+
+ //@formatter:on
+ // End of generated code
+
+}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 6a8dd81..1f8cee2 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -743,6 +743,8 @@
void notifyPackagesReplacedReceived(in String[] packages);
+ void getChecksums(in String packageName, boolean includeSplits, int optional, int required, in List trustedInstallers, in IntentSender statusReceiver, int userId);
+
//------------------------------------------------------------------------
//
// The following binder interfaces have been moved to IPermissionManager
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 7b2955d..da8d15a 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -79,8 +79,11 @@
import dalvik.system.VMRuntime;
import java.io.File;
+import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -3305,6 +3308,13 @@
public static final String EXTRA_FAILURE_EXISTING_PERMISSION
= "android.content.pm.extra.FAILURE_EXISTING_PERMISSION";
+ /**
+ * Extra field name for the ID of a package pending verification. Passed to
+ * a package verifier and is used to call back to
+ * @see #getChecksums
+ */
+ public static final String EXTRA_CHECKSUMS = "android.content.pm.extra.CHECKSUMS";
+
/**
* Permission flag: The permission is set in its current state
* by the user and apps can still request it at runtime.
@@ -7842,6 +7852,114 @@
}
/**
+ * Root SHA256 hash of a 4K Merkle tree computed over all file bytes.
+ * <a href="https://source.android.com/security/apksigning/v4">See APK Signature Scheme V4</a>.
+ * <a href="https://git.kernel.org/pub/scm/fs/fscrypt/fscrypt.git/tree/Documentation/filesystems/fsverity.rst">See fs-verity</a>.
+ *
+ * @see #getChecksums
+ */
+ public static final int WHOLE_MERKLE_ROOT_4K_SHA256 = 0x00000001;
+
+ /**
+ * MD5 hash computed over all file bytes.
+ *
+ * @see #getChecksums
+ */
+ public static final int WHOLE_MD5 = 0x00000002;
+
+ /**
+ * SHA1 hash computed over all file bytes.
+ *
+ * @see #getChecksums
+ */
+ public static final int WHOLE_SHA1 = 0x00000004;
+
+ /**
+ * SHA256 hash computed over all file bytes.
+ *
+ * @see #getChecksums
+ */
+ public static final int WHOLE_SHA256 = 0x00000008;
+
+ /**
+ * SHA512 hash computed over all file bytes.
+ *
+ * @see #getChecksums
+ */
+ public static final int WHOLE_SHA512 = 0x00000010;
+
+ /**
+ * Root SHA256 hash of a 1M Merkle tree computed over protected content.
+ * Excludes signing block.
+ * <a href="https://source.android.com/security/apksigning/v2">See APK Signature Scheme V2</a>.
+ *
+ * @see #getChecksums
+ */
+ public static final int PARTIAL_MERKLE_ROOT_1M_SHA256 = 0x00000020;
+
+ /**
+ * Root SHA512 hash of a 1M Merkle tree computed over protected content.
+ * Excludes signing block.
+ * <a href="https://source.android.com/security/apksigning/v2">See APK Signature Scheme V2</a>.
+ *
+ * @see #getChecksums
+ */
+ public static final int PARTIAL_MERKLE_ROOT_1M_SHA512 = 0x00000040;
+
+ /** @hide */
+ @IntDef(flag = true, prefix = {"WHOLE_", "PARTIAL_"}, value = {
+ WHOLE_MERKLE_ROOT_4K_SHA256,
+ WHOLE_MD5,
+ WHOLE_SHA1,
+ WHOLE_SHA256,
+ WHOLE_SHA512,
+ PARTIAL_MERKLE_ROOT_1M_SHA256,
+ PARTIAL_MERKLE_ROOT_1M_SHA512,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FileChecksumKind {}
+
+ /**
+ * Trust any Installer to provide checksums for the package.
+ * @see #getChecksums
+ */
+ public static final @Nullable List<Certificate> TRUST_ALL = null;
+
+ /**
+ * Don't trust any Installer to provide checksums for the package.
+ * This effectively disables optimized Installer-enforced checksums.
+ * @see #getChecksums
+ */
+ public static final @NonNull List<Certificate> TRUST_NONE = Collections.emptyList();
+
+ /**
+ * Returns the checksums for APKs within a package.
+ *
+ * By default returns all readily available checksums:
+ * - enforced by platform,
+ * - enforced by installer.
+ * If caller needs a specific checksum kind, they can specify it as required.
+ *
+ * @param packageName whose checksums to return.
+ * @param includeSplits whether to include checksums for non-base splits.
+ * @param required explicitly request the checksum kinds. Will incur significant
+ * CPU/memory/disk usage.
+ * @param trustedInstallers for checksums enforced by Installer, which ones to be trusted.
+ * {@link #TRUST_ALL} will return checksums from any Installer,
+ * {@link #TRUST_NONE} disables optimized Installer-enforced checksums.
+ * @param statusReceiver called once when the results are available as
+ * {@link #EXTRA_CHECKSUMS} of type FileChecksum[].
+ * @throws CertificateEncodingException if an encoding error occurs for trustedInstallers.
+ * @throws NameNotFoundException if a package with the given name cannot be found on the system.
+ */
+ public void getChecksums(@NonNull String packageName, boolean includeSplits,
+ @FileChecksumKind int required, @Nullable List<Certificate> trustedInstallers,
+ @NonNull IntentSender statusReceiver)
+ throws CertificateEncodingException, IOException, NameNotFoundException {
+ throw new UnsupportedOperationException("getChecksums not implemented in subclass");
+ }
+
+ /**
* @return the default text classifier package name, or null if there's none.
*
* @hide
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 6e34666..f74990a 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -149,7 +149,7 @@
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static SignatureInfo findSignature(RandomAccessFile apk)
+ public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
index 9357285..5f963b0 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -142,7 +142,7 @@
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static SignatureInfo findSignature(RandomAccessFile apk)
+ public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
diff --git a/core/java/android/util/apk/ApkSignatureVerifier.java b/core/java/android/util/apk/ApkSignatureVerifier.java
index e0258f7..02edb7e 100644
--- a/core/java/android/util/apk/ApkSignatureVerifier.java
+++ b/core/java/android/util/apk/ApkSignatureVerifier.java
@@ -92,6 +92,20 @@
private static PackageParser.SigningDetails verifySignatures(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws PackageParserException {
+ return verifySignaturesInternal(apkPath, minSignatureSchemeVersion,
+ verifyFull).signingDetails;
+ }
+
+ /**
+ * Verifies the provided APK using all allowed signing schemas.
+ * @return the certificates associated with each signer and content digests.
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @throws PackageParserException if there was a problem collecting certificates
+ * @hide
+ */
+ public static SigningDetailsWithDigests verifySignaturesInternal(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
+ throws PackageParserException {
if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V4) {
// V3 and before are older than the requested minimum signing version
@@ -121,7 +135,7 @@
return verifyV3AndBelowSignatures(apkPath, minSignatureSchemeVersion, verifyFull);
}
- private static PackageParser.SigningDetails verifyV3AndBelowSignatures(String apkPath,
+ private static SigningDetailsWithDigests verifyV3AndBelowSignatures(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws PackageParserException {
// try v3
@@ -174,7 +188,7 @@
* @throws SignatureNotFoundException if there are no V4 signatures in the APK
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static PackageParser.SigningDetails verifyV4Signature(String apkPath,
+ private static SigningDetailsWithDigests verifyV4Signature(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws SignatureNotFoundException, PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV4" : "certsOnlyV4");
@@ -234,8 +248,8 @@
}
}
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V4);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V4), vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -256,8 +270,8 @@
* @throws SignatureNotFoundException if there are no V3 signatures in the APK
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static PackageParser.SigningDetails verifyV3Signature(String apkPath,
- boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
+ private static SigningDetailsWithDigests verifyV3Signature(String apkPath, boolean verifyFull)
+ throws SignatureNotFoundException, PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV3" : "certsOnlyV3");
try {
ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
@@ -275,8 +289,9 @@
pastSignerSigs[i].setFlags(vSigner.por.flagsList.get(i));
}
}
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs),
+ vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -297,15 +312,16 @@
* @throws SignatureNotFoundException if there are no V2 signatures in the APK
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static PackageParser.SigningDetails verifyV2Signature(String apkPath,
- boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
+ private static SigningDetailsWithDigests verifyV2Signature(String apkPath, boolean verifyFull)
+ throws SignatureNotFoundException, PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV2" : "certsOnlyV2");
try {
- Certificate[][] signerCerts = verifyFull ? ApkSignatureSchemeV2Verifier.verify(apkPath)
- : ApkSignatureSchemeV2Verifier.unsafeGetCertsWithoutVerification(apkPath);
+ ApkSignatureSchemeV2Verifier.VerifiedSigner vSigner =
+ ApkSignatureSchemeV2Verifier.verify(apkPath, verifyFull);
+ Certificate[][] signerCerts = vSigner.certs;
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V2);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V2), vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -324,8 +340,7 @@
* @param verifyFull whether to verify all contents of this APK or just collect certificates.
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static PackageParser.SigningDetails verifyV1Signature(
- String apkPath, boolean verifyFull)
+ private static SigningDetailsWithDigests verifyV1Signature(String apkPath, boolean verifyFull)
throws PackageParserException {
StrictJarFile jarFile = null;
@@ -391,7 +406,8 @@
}
}
}
- return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
+ return new SigningDetailsWithDigests(
+ new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR), null);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
@@ -542,4 +558,27 @@
return null;
}
}
+
+ /**
+ * Extended signing details.
+ * @hide for internal use only.
+ */
+ public static class SigningDetailsWithDigests {
+ public final PackageParser.SigningDetails signingDetails;
+
+ /**
+ * APK Signature Schemes v2/v3/v4 might contain multiple content digests.
+ * SignatureVerifier usually chooses one of them to verify.
+ * For certain signature schemes, e.g. v4, this digest is verified continuously.
+ * For others, e.g. v2, the caller has to specify if they want to verify.
+ * Please refer to documentation for more details.
+ */
+ public final Map<Integer, byte[]> contentDigests;
+
+ SigningDetailsWithDigests(PackageParser.SigningDetails signingDetails,
+ Map<Integer, byte[]> contentDigests) {
+ this.signingDetails = signingDetails;
+ this.contentDigests = contentDigests;
+ }
+ }
}
diff --git a/core/java/android/util/apk/ApkSigningBlockUtils.java b/core/java/android/util/apk/ApkSigningBlockUtils.java
index 990092c..021f232 100644
--- a/core/java/android/util/apk/ApkSigningBlockUtils.java
+++ b/core/java/android/util/apk/ApkSigningBlockUtils.java
@@ -39,7 +39,7 @@
*
* @hide for internal use only.
*/
-final class ApkSigningBlockUtils {
+public final class ApkSigningBlockUtils {
private ApkSigningBlockUtils() {
}
@@ -146,6 +146,37 @@
Map<Integer, byte[]> expectedDigests,
FileDescriptor apkFileDescriptor,
SignatureInfo signatureInfo) throws SecurityException {
+ int[] digestAlgorithms = new int[expectedDigests.size()];
+ int digestAlgorithmCount = 0;
+ for (int digestAlgorithm : expectedDigests.keySet()) {
+ digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
+ digestAlgorithmCount++;
+ }
+ byte[][] actualDigests;
+ try {
+ actualDigests = computeContentDigestsPer1MbChunk(digestAlgorithms, apkFileDescriptor,
+ signatureInfo);
+ } catch (DigestException e) {
+ throw new SecurityException("Failed to compute digest(s) of contents", e);
+ }
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
+ byte[] actualDigest = actualDigests[i];
+ if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " digest of contents did not verify");
+ }
+ }
+ }
+
+ /**
+ * Calculate digests using digestAlgorithms for apkFileDescriptor.
+ * This will skip signature block described by signatureInfo.
+ */
+ public static byte[][] computeContentDigestsPer1MbChunk(int[] digestAlgorithms,
+ FileDescriptor apkFileDescriptor, SignatureInfo signatureInfo) throws DigestException {
// We need to verify the integrity of the following three sections of the file:
// 1. Everything up to the start of the APK Signing Block.
// 2. ZIP Central Directory.
@@ -156,6 +187,7 @@
// avoid wasting physical memory. In most APK verification scenarios, the contents of the
// APK are already there in the OS's page cache and thus mmap does not use additional
// physical memory.
+
DataSource beforeApkSigningBlock =
new MemoryMappedFileDataSource(apkFileDescriptor, 0,
signatureInfo.apkSigningBlockOffset);
@@ -171,31 +203,8 @@
ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.apkSigningBlockOffset);
DataSource eocd = new ByteBufferDataSource(eocdBuf);
- int[] digestAlgorithms = new int[expectedDigests.size()];
- int digestAlgorithmCount = 0;
- for (int digestAlgorithm : expectedDigests.keySet()) {
- digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
- digestAlgorithmCount++;
- }
- byte[][] actualDigests;
- try {
- actualDigests =
- computeContentDigestsPer1MbChunk(
- digestAlgorithms,
- new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
- } catch (DigestException e) {
- throw new SecurityException("Failed to compute digest(s) of contents", e);
- }
- for (int i = 0; i < digestAlgorithms.length; i++) {
- int digestAlgorithm = digestAlgorithms[i];
- byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
- byte[] actualDigest = actualDigests[i];
- if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
- throw new SecurityException(
- getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
- + " digest of contents did not verify");
- }
- }
+ return computeContentDigestsPer1MbChunk(digestAlgorithms,
+ new DataSource[]{beforeApkSigningBlock, centralDir, eocd});
}
private static byte[][] computeContentDigestsPer1MbChunk(
@@ -417,14 +426,10 @@
static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0423;
static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0425;
- static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
- static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
- static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
- static final int CONTENT_DIGEST_SHA256 = 4;
-
- private static final int[] V4_CONTENT_DIGEST_ALGORITHMS =
- {CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
- CONTENT_DIGEST_CHUNKED_SHA256};
+ public static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
+ public static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+ public static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
+ public static final int CONTENT_DIGEST_SHA256 = 4;
static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
diff --git a/core/java/android/util/apk/SignatureInfo.java b/core/java/android/util/apk/SignatureInfo.java
index 8e1233a..7638293 100644
--- a/core/java/android/util/apk/SignatureInfo.java
+++ b/core/java/android/util/apk/SignatureInfo.java
@@ -16,15 +16,18 @@
package android.util.apk;
+import android.annotation.NonNull;
+
import java.nio.ByteBuffer;
/**
* APK Signature Scheme v2 block and additional information relevant to verifying the signatures
* contained in the block against the file.
+ * @hide
*/
-class SignatureInfo {
+public class SignatureInfo {
/** Contents of APK Signature Scheme v2 block. */
- public final ByteBuffer signatureBlock;
+ public final @NonNull ByteBuffer signatureBlock;
/** Position of the APK Signing Block in the file. */
public final long apkSigningBlockOffset;
@@ -36,10 +39,10 @@
public final long eocdOffset;
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
- public final ByteBuffer eocd;
+ public final @NonNull ByteBuffer eocd;
- SignatureInfo(ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset,
- long eocdOffset, ByteBuffer eocd) {
+ SignatureInfo(@NonNull ByteBuffer signatureBlock, long apkSigningBlockOffset,
+ long centralDirOffset, long eocdOffset, @NonNull ByteBuffer eocd) {
this.signatureBlock = signatureBlock;
this.apkSigningBlockOffset = apkSigningBlockOffset;
this.centralDirOffset = centralDirOffset;
diff --git a/core/java/android/util/apk/VerityBuilder.java b/core/java/android/util/apk/VerityBuilder.java
index e81e3f7..4596c6e 100644
--- a/core/java/android/util/apk/VerityBuilder.java
+++ b/core/java/android/util/apk/VerityBuilder.java
@@ -116,6 +116,34 @@
}
/**
+ * Generates the fs-verity hash tree. It is the actual verity tree format on disk, as is
+ * re-generated on device.
+ *
+ * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
+ * input file. If the total size is larger than 4 KB, take this level as input and repeat the
+ * same procedure, until the level is within 4 KB. If salt is given, it will apply to each
+ * digestion before the actual data.
+ *
+ * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
+ *
+ * @return the root hash of the generated hash tree.
+ */
+ public static byte[] generateFsVerityRootHash(@NonNull String apkPath, byte[] salt,
+ @NonNull ByteBufferFactory bufferFactory)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ int[] levelOffset = calculateVerityLevelOffset(apk.length());
+ int merkleTreeSize = levelOffset[levelOffset.length - 1];
+
+ ByteBuffer output = bufferFactory.create(
+ merkleTreeSize
+ + CHUNK_SIZE_BYTES); // maximum size of apk-verity metadata
+ output.order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer tree = slice(output, 0, merkleTreeSize);
+ return generateFsVerityTreeInternal(apk, salt, levelOffset, tree);
+ }
+ }
+ /**
* Calculates the apk-verity root hash for integrity measurement. This needs to be consistent
* to what kernel returns.
*/
@@ -259,9 +287,10 @@
// thus the syscall overhead is not too big.
private static final int MMAP_REGION_SIZE_BYTES = 1024 * 1024;
- private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file, ByteBuffer output)
+ private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file,
+ @Nullable byte[] salt, ByteBuffer output)
throws IOException, NoSuchAlgorithmException, DigestException {
- BufferedDigester digester = new BufferedDigester(null /* salt */, output);
+ BufferedDigester digester = new BufferedDigester(salt, output);
// 1. Digest the whole file by chunks.
consumeByChunk(digester,
@@ -325,6 +354,35 @@
}
@NonNull
+ private static byte[] generateFsVerityTreeInternal(@NonNull RandomAccessFile apk,
+ @Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ // 1. Digest the apk to generate the leaf level hashes.
+ generateFsVerityDigestAtLeafLevel(apk, salt,
+ slice(output, levelOffset[levelOffset.length - 2],
+ levelOffset[levelOffset.length - 1]));
+
+ // 2. Digest the lower level hashes bottom up.
+ for (int level = levelOffset.length - 3; level >= 0; level--) {
+ ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]);
+ ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]);
+
+ DataSource source = new ByteBufferDataSource(inputBuffer);
+ BufferedDigester digester = new BufferedDigester(salt, outputBuffer);
+ consumeByChunk(digester, source, CHUNK_SIZE_BYTES);
+ digester.assertEmptyBuffer();
+ digester.fillUpLastOutputChunk();
+ }
+
+ // 3. Digest the first block (i.e. first level) to generate the root hash.
+ byte[] rootHash = new byte[DIGEST_SIZE_BYTES];
+ BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash));
+ digester.consume(slice(output, 0, CHUNK_SIZE_BYTES));
+ digester.assertEmptyBuffer();
+ return rootHash;
+ }
+
+ @NonNull
private static byte[] generateVerityTreeInternal(@NonNull RandomAccessFile apk,
@Nullable SignatureInfo signatureInfo, @Nullable byte[] salt,
@NonNull int[] levelOffset, @NonNull ByteBuffer output)
diff --git a/non-updatable-api/current.txt b/non-updatable-api/current.txt
index e0ebec6..e1b3151 100644
--- a/non-updatable-api/current.txt
+++ b/non-updatable-api/current.txt
@@ -11617,6 +11617,16 @@
field public int version;
}
+ public final class FileChecksum implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getKind();
+ method @Nullable public java.security.cert.Certificate getSourceCertificate() throws java.security.cert.CertificateException;
+ method @Nullable public String getSplitName();
+ method @NonNull public byte[] getValue();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.FileChecksum> CREATOR;
+ }
+
public final class InstallSourceInfo implements android.os.Parcelable {
method public int describeContents();
method @Nullable public String getInitiatingPackageName();
@@ -11992,6 +12002,7 @@
method @Nullable public abstract android.graphics.drawable.Drawable getApplicationLogo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull public CharSequence getBackgroundPermissionOptionLabel();
method @Nullable public abstract android.content.pm.ChangedPackages getChangedPackages(@IntRange(from=0) int);
+ method public void getChecksums(@NonNull String, boolean, int, @Nullable java.util.List<java.security.cert.Certificate>, @NonNull android.content.IntentSender) throws java.security.cert.CertificateEncodingException, java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
method public abstract int getComponentEnabledSetting(@NonNull android.content.ComponentName);
method @NonNull public abstract android.graphics.drawable.Drawable getDefaultActivityIcon();
method @Nullable public abstract android.graphics.drawable.Drawable getDrawable(@NonNull String, @DrawableRes int, @Nullable android.content.pm.ApplicationInfo);
@@ -12082,6 +12093,7 @@
field public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3; // 0x3
field public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; // 0x1
field public static final int DONT_KILL_APP = 1; // 0x1
+ field public static final String EXTRA_CHECKSUMS = "android.content.pm.extra.CHECKSUMS";
field public static final String EXTRA_VERIFICATION_ID = "android.content.pm.extra.VERIFICATION_ID";
field public static final String EXTRA_VERIFICATION_RESULT = "android.content.pm.extra.VERIFICATION_RESULT";
field public static final String FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS = "android.software.activities_on_secondary_displays";
@@ -12236,6 +12248,8 @@
field public static final int MATCH_SYSTEM_ONLY = 1048576; // 0x100000
field public static final int MATCH_UNINSTALLED_PACKAGES = 8192; // 0x2000
field public static final long MAXIMUM_VERIFICATION_TIMEOUT = 3600000L; // 0x36ee80L
+ field public static final int PARTIAL_MERKLE_ROOT_1M_SHA256 = 32; // 0x20
+ field public static final int PARTIAL_MERKLE_ROOT_1M_SHA512 = 64; // 0x40
field public static final int PERMISSION_DENIED = -1; // 0xffffffff
field public static final int PERMISSION_GRANTED = 0; // 0x0
field public static final int SIGNATURE_FIRST_NOT_SIGNED = -1; // 0xffffffff
@@ -12245,9 +12259,16 @@
field public static final int SIGNATURE_SECOND_NOT_SIGNED = -2; // 0xfffffffe
field public static final int SIGNATURE_UNKNOWN_PACKAGE = -4; // 0xfffffffc
field public static final int SYNCHRONOUS = 2; // 0x2
+ field @Nullable public static final java.util.List<java.security.cert.Certificate> TRUST_ALL;
+ field @NonNull public static final java.util.List<java.security.cert.Certificate> TRUST_NONE;
field public static final int VERIFICATION_ALLOW = 1; // 0x1
field public static final int VERIFICATION_REJECT = -1; // 0xffffffff
field public static final int VERSION_CODE_HIGHEST = -1; // 0xffffffff
+ field public static final int WHOLE_MD5 = 2; // 0x2
+ field public static final int WHOLE_MERKLE_ROOT_4K_SHA256 = 1; // 0x1
+ field public static final int WHOLE_SHA1 = 4; // 0x4
+ field public static final int WHOLE_SHA256 = 8; // 0x8
+ field public static final int WHOLE_SHA512 = 16; // 0x10
}
public static class PackageManager.NameNotFoundException extends android.util.AndroidException {
diff --git a/services/core/java/com/android/server/pm/ApkChecksums.java b/services/core/java/com/android/server/pm/ApkChecksums.java
new file mode 100644
index 0000000..d8745ab
--- /dev/null
+++ b/services/core/java/com/android/server/pm/ApkChecksums.java
@@ -0,0 +1,310 @@
+/*
+ * 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.PackageManager.PARTIAL_MERKLE_ROOT_1M_SHA256;
+import static android.content.pm.PackageManager.PARTIAL_MERKLE_ROOT_1M_SHA512;
+import static android.content.pm.PackageManager.WHOLE_MD5;
+import static android.content.pm.PackageManager.WHOLE_MERKLE_ROOT_4K_SHA256;
+import static android.content.pm.PackageManager.WHOLE_SHA1;
+import static android.content.pm.PackageManager.WHOLE_SHA256;
+import static android.content.pm.PackageManager.WHOLE_SHA512;
+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.Nullable;
+import android.content.pm.FileChecksum;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.util.ArrayMap;
+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.server.security.VerityUtils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides checksums for APK.
+ */
+public class ApkChecksums {
+ static final String TAG = "ApkChecksums";
+
+ // 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";
+
+ /**
+ * Fetch or calculate checksums for the specific file.
+ *
+ * @param split split name, null for base
+ * @param file to fetch checksums for
+ * @param optional mask to fetch readily available checksums
+ * @param required mask to forcefully calculate if not available
+ * @param trustedInstallers array of certificate to trust, two specific cases:
+ * null - trust anybody,
+ * [] - trust nobody.
+ */
+ public static List<FileChecksum> getFileChecksums(String split, File file,
+ @PackageManager.FileChecksumKind int optional,
+ @PackageManager.FileChecksumKind int required,
+ @Nullable Certificate[] trustedInstallers) {
+ final String filePath = file.getAbsolutePath();
+ Map<Integer, FileChecksum> checksums = new ArrayMap<>();
+ final int kinds = (optional | required);
+ // System enforced: FSI or v2/v3/v4 signatures.
+ if ((kinds & WHOLE_MERKLE_ROOT_4K_SHA256) != 0) {
+ // Hashes in fs-verity and IncFS are always verified.
+ FileChecksum checksum = extractHashFromFS(split, filePath);
+ if (checksum != null) {
+ checksums.put(checksum.getKind(), checksum);
+ }
+ }
+ if ((kinds & (PARTIAL_MERKLE_ROOT_1M_SHA256 | PARTIAL_MERKLE_ROOT_1M_SHA512)) != 0) {
+ Map<Integer, FileChecksum> v2v3checksums = extractHashFromV2V3Signature(
+ split, filePath, kinds);
+ if (v2v3checksums != null) {
+ checksums.putAll(v2v3checksums);
+ }
+ }
+
+ // TODO(b/160605420): Installer provided.
+ // TODO(b/160605420): Wait for Incremental to be fully loaded.
+
+ // Manually calculating required checksums if not readily available.
+ if ((required & WHOLE_MERKLE_ROOT_4K_SHA256) != 0 && !checksums.containsKey(
+ WHOLE_MERKLE_ROOT_4K_SHA256)) {
+ try {
+ byte[] generatedRootHash = VerityBuilder.generateFsVerityRootHash(
+ filePath, /*salt=*/null,
+ new ByteBufferFactory() {
+ @Override
+ public ByteBuffer create(int capacity) {
+ return ByteBuffer.allocate(capacity);
+ }
+ });
+ checksums.put(WHOLE_MERKLE_ROOT_4K_SHA256,
+ new FileChecksum(split, WHOLE_MERKLE_ROOT_4K_SHA256, generatedRootHash));
+ } catch (IOException | NoSuchAlgorithmException | DigestException e) {
+ Slog.e(TAG, "Error calculating WHOLE_MERKLE_ROOT_4K_SHA256", e);
+ }
+ }
+
+ calculateChecksumIfRequested(checksums, split, file, required, WHOLE_MD5);
+ calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA1);
+ calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA256);
+ calculateChecksumIfRequested(checksums, split, file, required, WHOLE_SHA512);
+
+ calculatePartialChecksumsIfRequested(checksums, split, file, required);
+
+ return new ArrayList<>(checksums.values());
+ }
+
+ private static FileChecksum extractHashFromFS(String split, String filePath) {
+ // verity first
+ {
+ byte[] hash = VerityUtils.getFsverityRootHash(filePath);
+ if (hash != null) {
+ return new FileChecksum(split, WHOLE_MERKLE_ROOT_4K_SHA256, hash);
+ }
+ }
+ // v4 next
+ try {
+ ApkSignatureSchemeV4Verifier.VerifiedSigner signer =
+ ApkSignatureSchemeV4Verifier.extractCertificates(filePath);
+ byte[] hash = signer.contentDigests.getOrDefault(CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
+ null);
+ if (hash != null) {
+ return new FileChecksum(split, WHOLE_MERKLE_ROOT_4K_SHA256, hash);
+ }
+ } catch (SignatureNotFoundException e) {
+ // Nothing
+ } catch (SecurityException e) {
+ Slog.e(TAG, "V4 signature error", e);
+ }
+ return null;
+ }
+
+ private static Map<Integer, FileChecksum> extractHashFromV2V3Signature(
+ String split, String filePath, int kinds) {
+ Map<Integer, byte[]> contentDigests = null;
+ try {
+ contentDigests = ApkSignatureVerifier.verifySignaturesInternal(filePath,
+ PackageParser.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2,
+ false).contentDigests;
+ } catch (PackageParser.PackageParserException e) {
+ Slog.e(TAG, "Signature verification error", e);
+ }
+
+ if (contentDigests == null) {
+ return null;
+ }
+
+ Map<Integer, FileChecksum> checksums = new ArrayMap<>();
+ if ((kinds & PARTIAL_MERKLE_ROOT_1M_SHA256) != 0) {
+ byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA256, null);
+ if (hash != null) {
+ checksums.put(PARTIAL_MERKLE_ROOT_1M_SHA256,
+ new FileChecksum(split, PARTIAL_MERKLE_ROOT_1M_SHA256, hash));
+ }
+ }
+ if ((kinds & PARTIAL_MERKLE_ROOT_1M_SHA512) != 0) {
+ byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA512, null);
+ if (hash != null) {
+ checksums.put(PARTIAL_MERKLE_ROOT_1M_SHA512,
+ new FileChecksum(split, PARTIAL_MERKLE_ROOT_1M_SHA512, hash));
+ }
+ }
+ return checksums;
+ }
+
+ private static String getMessageDigestAlgoForChecksumKind(int kind)
+ throws NoSuchAlgorithmException {
+ switch (kind) {
+ case WHOLE_MD5:
+ return ALGO_MD5;
+ case WHOLE_SHA1:
+ return ALGO_SHA1;
+ case WHOLE_SHA256:
+ return ALGO_SHA256;
+ case WHOLE_SHA512:
+ return ALGO_SHA512;
+ default:
+ throw new NoSuchAlgorithmException("Invalid checksum kind: " + kind);
+ }
+ }
+
+ private static void calculateChecksumIfRequested(Map<Integer, FileChecksum> checksums,
+ String split, File file, int required, int kind) {
+ if ((required & kind) != 0 && !checksums.containsKey(kind)) {
+ final byte[] checksum = getFileChecksum(file, kind);
+ if (checksum != null) {
+ checksums.put(kind, new FileChecksum(split, kind, checksum));
+ }
+ }
+ }
+
+ private static byte[] getFileChecksum(File file, int kind) {
+ try (FileInputStream fis = new FileInputStream(file);
+ BufferedInputStream bis = new BufferedInputStream(fis)) {
+ byte[] dataBytes = new byte[512 * 1024];
+ int nread = 0;
+
+ final String algo = getMessageDigestAlgoForChecksumKind(kind);
+ MessageDigest md = MessageDigest.getInstance(algo);
+ while ((nread = bis.read(dataBytes)) != -1) {
+ md.update(dataBytes, 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 PARTIAL_MERKLE_ROOT_1M_SHA256;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return PARTIAL_MERKLE_ROOT_1M_SHA512;
+ default:
+ return -1;
+ }
+ }
+
+ private static void calculatePartialChecksumsIfRequested(Map<Integer, FileChecksum> checksums,
+ String split, File file, int required) {
+ boolean needSignatureSha256 =
+ (required & PARTIAL_MERKLE_ROOT_1M_SHA256) != 0 && !checksums.containsKey(
+ PARTIAL_MERKLE_ROOT_1M_SHA256);
+ boolean needSignatureSha512 =
+ (required & PARTIAL_MERKLE_ROOT_1M_SHA512) != 0 && !checksums.containsKey(
+ 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 FileChecksum(split, checksumKind, digests[i]));
+ }
+ }
+ } catch (IOException | DigestException e) {
+ Slog.e(TAG, "Error computing hash.", e);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index c05bc45..bb7b63b 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -40,6 +40,7 @@
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.EXTRA_CHECKSUMS;
import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID;
import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT;
import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED;
@@ -168,6 +169,7 @@
import android.content.pm.DataLoaderType;
import android.content.pm.FallbackCategoryProvider;
import android.content.pm.FeatureInfo;
+import android.content.pm.FileChecksum;
import android.content.pm.IDexModuleRegisterCallback;
import android.content.pm.IPackageChangeObserver;
import android.content.pm.IPackageDataObserver;
@@ -254,6 +256,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
+import android.os.ParcelableException;
import android.os.PatternMatcher;
import android.os.PersistableBundle;
import android.os.Process;
@@ -394,6 +397,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -404,7 +408,10 @@
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
+import java.security.cert.Certificate;
import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -2443,6 +2450,83 @@
mHandler.sendMessageDelayed(message, DEFERRED_NO_KILL_INSTALL_OBSERVER_DELAY_MS);
}
+ @Override
+ public void getChecksums(@NonNull String packageName, boolean includeSplits,
+ @PackageManager.FileChecksumKind int optional,
+ @PackageManager.FileChecksumKind int required, @Nullable List trustedInstallers,
+ @NonNull IntentSender statusReceiver, int userId) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(statusReceiver);
+
+ final ApplicationInfo applicationInfo = getApplicationInfoInternal(packageName, 0,
+ Binder.getCallingUid(), userId);
+ if (applicationInfo == null) {
+ throw new ParcelableException(new PackageManager.NameNotFoundException(packageName));
+ }
+
+ List<Pair<String, File>> filesToChecksum = new ArrayList<>();
+
+ // Adding base split.
+ filesToChecksum.add(Pair.create(null, new File(applicationInfo.sourceDir)));
+
+ // Adding other splits.
+ if (includeSplits && applicationInfo.splitNames != null) {
+ for (int i = 0, size = applicationInfo.splitNames.length; i < size; ++i) {
+ filesToChecksum.add(Pair.create(applicationInfo.splitNames[i],
+ new File(applicationInfo.splitSourceDirs[i])));
+ }
+ }
+
+ for (int i = 0, size = filesToChecksum.size(); i < size; ++i) {
+ final File file = filesToChecksum.get(i).second;
+ if (!file.exists()) {
+ throw new IllegalStateException("File not found: " + file.getPath());
+ }
+ }
+
+ final Certificate[] trustedCerts = (trustedInstallers != null) ? decodeCertificates(
+ trustedInstallers) : null;
+ final Context context = mContext;
+
+ mInjector.getBackgroundExecutor().execute(() -> {
+ final Intent intent = new Intent();
+ List<FileChecksum> result = 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;
+ try {
+ result.addAll(ApkChecksums.getFileChecksums(split, file, optional, required,
+ trustedCerts));
+ } catch (Throwable e) {
+ Slog.e(TAG, "Checksum calculation error", e);
+ }
+ }
+ intent.putExtra(EXTRA_CHECKSUMS,
+ result.toArray(new FileChecksum[result.size()]));
+
+ try {
+ statusReceiver.sendIntent(context, 1, intent, null, null);
+ } catch (SendIntentException e) {
+ Slog.w(TAG, e);
+ }
+ });
+ }
+
+ private static @NonNull Certificate[] decodeCertificates(@NonNull List certs) {
+ try {
+ final CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ final Certificate[] result = new Certificate[certs.size()];
+ for (int i = 0, size = certs.size(); i < size; ++i) {
+ final InputStream is = new ByteArrayInputStream((byte[]) certs.get(i));
+ final X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
+ result[i] = cert;
+ }
+ return result;
+ } catch (CertificateException e) {
+ throw ExceptionUtils.propagate(e);
+ }
+ }
+
/**
* Gets the type of the external storage a package is installed on.
* @param packageVolume The storage volume of the package.
diff --git a/services/core/java/com/android/server/security/VerityUtils.java b/services/core/java/com/android/server/security/VerityUtils.java
index 2b793c8..f204aa2 100644
--- a/services/core/java/com/android/server/security/VerityUtils.java
+++ b/services/core/java/com/android/server/security/VerityUtils.java
@@ -52,6 +52,9 @@
/** The maximum size of signature file. This is just to avoid potential abuse. */
private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
+ /** SHA256 hash size. */
+ private static final int HASH_SIZE_BYTES = 32;
+
private static final boolean DEBUG = false;
/** Returns true if the given file looks like containing an fs-verity signature. */
@@ -90,8 +93,23 @@
return (retval == 1);
}
+ /** Returns hash of a root node for the fs-verity enabled file. */
+ public static byte[] getFsverityRootHash(@NonNull String filePath) {
+ byte[] result = new byte[HASH_SIZE_BYTES];
+ int retval = measureFsverityNative(filePath, result);
+ if (retval < 0) {
+ if (retval != -OsConstants.ENODATA) {
+ Slog.e(TAG, "Failed to measure fs-verity, errno " + -retval + ": " + filePath);
+ }
+ return null;
+ }
+ return result;
+ }
+
private static native int enableFsverityNative(@NonNull String filePath,
@NonNull byte[] pkcs7Signature);
+ private static native int measureFsverityNative(@NonNull String filePath,
+ @NonNull byte[] digest);
private static native int statxForFsverityNative(@NonNull String filePath);
/**
diff --git a/services/core/jni/com_android_server_security_VerityUtils.cpp b/services/core/jni/com_android_server_security_VerityUtils.cpp
index 0277f16..46e6f91 100644
--- a/services/core/jni/com_android_server_security_VerityUtils.cpp
+++ b/services/core/jni/com_android_server_security_VerityUtils.cpp
@@ -33,6 +33,8 @@
#include <android-base/unique_fd.h>
+#include <type_traits>
+
namespace android {
namespace {
@@ -53,7 +55,7 @@
fsverity_enable_arg arg = {};
arg.version = 1;
- arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256;
+ arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256; // hardcoded in measureFsverity below
arg.block_size = 4096;
arg.salt_size = 0;
arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
@@ -85,9 +87,41 @@
return (out.stx_attributes & STATX_ATTR_VERITY) != 0;
}
+int measureFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath, jbyteArray digest) {
+ static constexpr auto kDigestSha256 = 32;
+ using Storage = std::aligned_storage_t<sizeof(fsverity_digest) + kDigestSha256>;
+
+ Storage bytes;
+ fsverity_digest *data = reinterpret_cast<fsverity_digest *>(&bytes);
+ data->digest_size = kDigestSha256; // the only input/output parameter
+
+ ScopedUtfChars path(env, filePath);
+ ::android::base::unique_fd rfd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
+ if (rfd.get() < 0) {
+ return rfd.get();
+ }
+ if (auto err = ioctl(rfd.get(), FS_IOC_MEASURE_VERITY, data); err < 0) {
+ return err;
+ }
+
+ if (data->digest_algorithm != FS_VERITY_HASH_ALG_SHA256) {
+ return -EINVAL;
+ }
+
+ if (digest != nullptr && data->digest_size > 0) {
+ auto digestSize = env->GetArrayLength(digest);
+ if (data->digest_size > digestSize) {
+ return -E2BIG;
+ }
+ env->SetByteArrayRegion(digest, 0, data->digest_size, (const jbyte *)data->digest);
+ }
+
+ return 0;
+}
const JNINativeMethod sMethods[] = {
{"enableFsverityNative", "(Ljava/lang/String;[B)I", (void *)enableFsverity},
{"statxForFsverityNative", "(Ljava/lang/String;)I", (void *)statxForFsverity},
+ {"measureFsverityNative", "(Ljava/lang/String;[B)I", (void *)measureFsverity},
};
} // namespace