diff options
17 files changed, 1076 insertions, 61 deletions
diff --git a/api/current.txt b/api/current.txt index c6af6beca60b..e5baa7c07cd5 100644 --- a/api/current.txt +++ b/api/current.txt @@ -11617,6 +11617,16 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 340d5a12f92e..2780036d8102 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -73,6 +73,7 @@ import android.os.Bundle; 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 dalvik.system.VMRuntime; 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 @@ public class ApplicationPackageManager extends PackageManager { // 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 @@ public class ApplicationPackageManager extends PackageManager { } } + 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 000000000000..109f211033c1 --- /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 000000000000..55430c2b877b --- /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 6a8dd81051eb..1f8cee25be51 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -743,6 +743,8 @@ interface IPackageManager { 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 7b2955db3318..da8d15af92b8 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -79,8 +79,11 @@ import com.android.internal.util.ArrayUtils; 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 abstract class PackageManager { 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 @@ public abstract class PackageManager { } /** + * 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 6e34666aea84..f74990a82327 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -149,7 +149,7 @@ public class ApkSignatureSchemeV2Verifier { * @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 93572857796c..5f963b019335 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java +++ b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java @@ -142,7 +142,7 @@ public class ApkSignatureSchemeV3Verifier { * @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 e0258f7657b2..02edb7ed50a5 100644 --- a/core/java/android/util/apk/ApkSignatureVerifier.java +++ b/core/java/android/util/apk/ApkSignatureVerifier.java @@ -92,6 +92,20 @@ public class ApkSignatureVerifier { 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 @@ public class ApkSignatureVerifier { 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 @@ public class ApkSignatureVerifier { * @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 @@ public class ApkSignatureVerifier { } } - 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 @@ public class ApkSignatureVerifier { * @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 @@ public class ApkSignatureVerifier { 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 @@ public class ApkSignatureVerifier { * @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 @@ public class ApkSignatureVerifier { * @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 @@ public class ApkSignatureVerifier { } } } - 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 @@ public class ApkSignatureVerifier { 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 990092caa833..021f232979ef 100644 --- a/core/java/android/util/apk/ApkSigningBlockUtils.java +++ b/core/java/android/util/apk/ApkSigningBlockUtils.java @@ -39,7 +39,7 @@ import java.util.Map; * * @hide for internal use only. */ -final class ApkSigningBlockUtils { +public final class ApkSigningBlockUtils { private ApkSigningBlockUtils() { } @@ -146,6 +146,37 @@ final class ApkSigningBlockUtils { 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 @@ final class ApkSigningBlockUtils { // 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 @@ final class ApkSigningBlockUtils { 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 @@ final class ApkSigningBlockUtils { 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 8e1233af34a1..7638293618ba 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 @@ class SignatureInfo { 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 e81e3f7b38d6..4596c6e8f83d 100644 --- a/core/java/android/util/apk/VerityBuilder.java +++ b/core/java/android/util/apk/VerityBuilder.java @@ -116,6 +116,34 @@ public abstract class VerityBuilder { } /** + * 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 @@ public abstract class VerityBuilder { // 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 @@ public abstract class VerityBuilder { } @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 e0ebec6cbd01..e1b3151acd5b 100644 --- a/non-updatable-api/current.txt +++ b/non-updatable-api/current.txt @@ -11617,6 +11617,16 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 @@ package android.content.pm { 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 000000000000..d8745abb4a07 --- /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 c05bc455887d..bb7b63bbc933 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 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.ComponentInfo; 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.IBinder; 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.FileDescriptor; 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.MessageDigest; 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 @@ public class PackageManagerService extends IPackageManager.Stub 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 2b793c894eb0..f204aa2cc1ca 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 @@ abstract public class VerityUtils { /** 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 @@ abstract public class VerityUtils { 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 0277f16d5e54..46e6f912edb0 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 @@ int enableFsverity(JNIEnv* env, jobject /* clazz */, jstring filePath, jbyteArra 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 @@ int statxForFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) { 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 |