Ensure signature for allowlisted system app update on boot
With v4 signature and fs-verity, verifying an APK integrity is O(1)
time. This allows us to enforce signature and detect persistent attack
(via tampering with an APK) across a reboot for updated system apps.
For the first step, we don't implement a policy (e.g. all priv apps)
and only protect the packages in an allowlist specified by a resource
property. This is due to the ecosystem complexity, where some OEM may
preload app from another developer, since developer's v4 signature may
need extra plumbing depending on how they are installed.
Some implementation details:
After a system package is updated, during the boot we still want to
retrieve the SigningDetails from the APK in the dm-verity protected
partition. This is to harden and protect the allowlisted apps from
tampered record in an attacking scenario. The SigningDetails of the
disabled pacakge is then used during the reconcile phase, to ensure
the updated package has consistent signature with the original version.
Originally, canSkipForcedPackageVerification checks splits explicitly.
This is not necessary because ParsingPackageUtils.getSigningDetails can
only succeed (e.g. during collectCertificatesLI, when forced) if the
splits are consistent with the base.
Delete some dead code, e.g. in the skipVerify condition.
Bug: 277344944
Test: 1. locally add com.android.egg to the allowlist
2. build EasterEgg with v4 signature; and EasterEgg2 with a
different signing key
3. adb install-multiple --no-incr EasterEgg.apk EasterEgg.apk.idsig
4. with root, replace base.apk and base.apk.idsig with EasterEgg2,
chown and enable fsverity
5. adb shell stop/start
6. verify from logcat that the APK is recovered by expected check
* With some code change to force condition.
Change-Id: I0b62b73208c7d4e6b8613f1ae3aa726de8d8fa65
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index 6a1f3f9..ade20d2 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -60,6 +60,12 @@
src: "preinstalled-packages-asl-files.xml",
}
+prebuilt_etc {
+ name: "preinstalled-packages-strict-signature.xml",
+ sub_dir: "sysconfig",
+ src: "preinstalled-packages-strict-signature.xml",
+}
+
// Privapp permission whitelist files
prebuilt_etc {
diff --git a/data/etc/preinstalled-packages-strict-signature.xml b/data/etc/preinstalled-packages-strict-signature.xml
new file mode 100644
index 0000000..3cbfa8c
--- /dev/null
+++ b/data/etc/preinstalled-packages-strict-signature.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<!--
+This XML file declares which preinstalled apps, after updated, need to have strict signature check
+in boot time and avoid the cached results. This is to ensure the updated version still verifies
+against the preinstalled version.
+
+Example usage:
+ <require-strict-signature package="com.foo.bar"/>
+-->
+
+<config></config>
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index caf1684..40b29d7 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -340,6 +340,10 @@
// A map of preloaded package names and the path to its app metadata file path.
private final ArrayMap<String, String> mAppMetadataFilePaths = new ArrayMap<>();
+ // A set of pre-installed package names that requires strict signature verification once
+ // updated to avoid cached/potentially tampered results.
+ private final Set<String> mPreinstallPackagesWithStrictSignatureCheck = new ArraySet<>();
+
/**
* Map of system pre-defined, uniquely named actors; keys are namespace,
* value maps actor name to package name.
@@ -542,6 +546,10 @@
return mAppMetadataFilePaths;
}
+ public Set<String> getPreinstallPackagesWithStrictSignatureCheck() {
+ return mPreinstallPackagesWithStrictSignatureCheck;
+ }
+
/**
* Only use for testing. Do NOT use in production code.
* @param readPermissions false to create an empty SystemConfig; true to read the permissions.
@@ -1485,6 +1493,17 @@
mAppMetadataFilePaths.put(packageName, path);
}
} break;
+ case "require-strict-signature": {
+ if (android.security.Flags.extendVbChainToUpdatedApk()) {
+ String packageName = parser.getAttributeValue(null, "package");
+ if (TextUtils.isEmpty(packageName)) {
+ Slog.w(TAG, "<" + name + "> without valid package in " + permFile
+ + " at " + parser.getPositionDescription());
+ } else {
+ mPreinstallPackagesWithStrictSignatureCheck.add(packageName);
+ }
+ }
+ } break;
default: {
Slog.w(TAG, "Tag " + name + " is unknown in "
+ permFile + " at " + parser.getPositionDescription());
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 8f71a9b..c6388e7 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -3728,7 +3728,7 @@
final ScanResult scanResult = scanResultPair.first;
boolean shouldHideSystemApp = scanResultPair.second;
final InstallRequest installRequest = new InstallRequest(
- parsedPackage, parseFlags, scanFlags, user, scanResult);
+ parsedPackage, parseFlags, scanFlags, user, scanResult, disabledPkgSetting);
String existingApexModuleName = null;
synchronized (mPm.mLock) {
@@ -3962,6 +3962,7 @@
final String disabledPkgName = pkgAlreadyExists
? pkgSetting.getPackageName() : parsedPackage.getPackageName();
final boolean isSystemPkgUpdated;
+ final PackageSetting disabledPkgSetting;
final boolean isUpgrade;
synchronized (mPm.mLock) {
isUpgrade = mPm.isDeviceUpgrading();
@@ -3975,8 +3976,7 @@
+ "and install it as non-updated system app.");
mPm.mSettings.removeDisabledSystemPackageLPw(disabledPkgName);
}
- final PackageSetting disabledPkgSetting =
- mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName);
+ disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr(disabledPkgName);
isSystemPkgUpdated = disabledPkgSetting != null;
if (DEBUG_INSTALL && isSystemPkgUpdated) {
@@ -4048,6 +4048,23 @@
// equal to the version on the /data partition. Throw an exception and use
// the application already installed on the /data partition.
if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) {
+ // For some updated system packages, during addForInit we want to ensure the
+ // PackageSetting has the correct SigningDetails compares to the original version on
+ // the system partition. For the check to happen later during the /data scan, update
+ // the disabled package setting per the original APK on a system partition so that it
+ // can be trusted during reconcile.
+ if (needSignatureMatchToSystem(parsedPackage.getPackageName())) {
+ final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
+ final ParseResult<SigningDetails> result =
+ ParsingPackageUtils.getSigningDetails(input, parsedPackage,
+ false /*skipVerify*/);
+ if (result.isError()) {
+ throw new PrepareFailure("Failed collect during scanSystemPackageLI",
+ result.getException());
+ }
+ disabledPkgSetting.setSigningDetails(result.getResult());
+ }
+
// In the case of a skipped package, commitReconciledScanResultLocked is not called to
// add the object to the "live" data structures, so this is the final mutation step
// for the package. Which means it needs to be finalized here to cache derived fields.
@@ -4065,19 +4082,16 @@
// Verify certificates against what was last scanned. Force re-collecting certificate in two
// special cases:
// 1) when scanning system, force re-collect only if system is upgrading.
- // 2) when scanning /data, force re-collect only if the app is privileged (updated from
- // preinstall, or treated as privileged, e.g. due to shared user ID).
+ // 2) when scanning /data, force re-collect only if the package name is allowlisted.
final boolean forceCollect = scanSystemPartition ? isUpgrade
- : PackageManagerServiceUtils.isApkVerificationForced(pkgSetting);
+ : pkgAlreadyExists && needSignatureMatchToSystem(pkgSetting.getPackageName());
if (DEBUG_VERIFY && forceCollect) {
Slog.d(TAG, "Force collect certificate of " + parsedPackage.getPackageName());
}
- // Full APK verification can be skipped during certificate collection, only if the file is
- // in verified partition, or can be verified on access (when apk verity is enabled). In both
- // cases, only data in Signing Block is verified instead of the whole file.
- final boolean skipVerify = scanSystemPartition
- || (forceCollect && canSkipForcedPackageVerification(parsedPackage));
+ // APK verification can be skipped during certificate collection, only if the file is in a
+ // verified partition.
+ final boolean skipVerify = scanSystemPartition;
ScanPackageUtils.collectCertificatesLI(pkgSetting, parsedPackage,
mPm.getSettingsVersionForPackage(parsedPackage), forceCollect, skipVerify,
mPm.isPreNMR1Upgrade());
@@ -4196,22 +4210,15 @@
}
/**
- * Returns if forced apk verification can be skipped for the whole package, including splits.
+ * Returns whether the package needs a signature verification against the pre-installed version
+ * at boot.
*/
- private boolean canSkipForcedPackageVerification(AndroidPackage pkg) {
- if (!VerityUtils.hasFsverity(pkg.getBaseApkPath())) {
+ private boolean needSignatureMatchToSystem(String packageName) {
+ if (!android.security.Flags.extendVbChainToUpdatedApk()) {
return false;
}
- // TODO: Allow base and splits to be verified individually.
- String[] splitCodePaths = pkg.getSplitCodePaths();
- if (!ArrayUtils.isEmpty(splitCodePaths)) {
- for (int i = 0; i < splitCodePaths.length; i++) {
- if (!VerityUtils.hasFsverity(splitCodePaths[i])) {
- return false;
- }
- }
- }
- return true;
+ return mPm.mInjector.getSystemConfig().getPreinstallPackagesWithStrictSignatureCheck()
+ .contains(packageName);
}
/**
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index a4ee3c8..1c7024b 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -58,7 +58,6 @@
import java.io.File;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
@@ -190,7 +189,7 @@
// addForInit
InstallRequest(ParsedPackage parsedPackage, int parseFlags, int scanFlags,
- @Nullable UserHandle user, ScanResult scanResult) {
+ @Nullable UserHandle user, ScanResult scanResult, PackageSetting disabledPs) {
if (user != null) {
mUserId = user.getIdentifier();
} else {
@@ -206,6 +205,7 @@
mPackageMetrics = null; // No logging from this code path
mSessionId = -1;
mRequireUserAction = USER_ACTION_UNSPECIFIED;
+ mDisabledPs = disabledPs;
}
@Nullable
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 8e91f42..bcb7bde 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -521,11 +521,8 @@
}
/**
- * Make sure the updated priv app is signed with the same key as the original APK file on the
- * /system partition.
- *
- * <p>The rationale is that {@code disabledPkg} is a PackageSetting backed by xml files in /data
- * and is not tamperproof.
+ * Verifies the updated system app has a signature that is consistent with the pre-installed
+ * version or the signing lineage.
*/
private static boolean matchSignatureInSystem(@NonNull String packageName,
@NonNull SigningDetails signingDetails, PackageSetting disabledPkgSetting) {
@@ -559,17 +556,12 @@
== FSVERITY_ENABLED;
}
- /** Returns true to force apk verification if the package is considered privileged. */
- static boolean isApkVerificationForced(@Nullable PackageSetting ps) {
- // TODO(b/154310064): re-enable.
- return false;
- }
-
/**
* Verifies that signatures match.
* @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
* @throws PackageManagerException if the signatures did not match.
*/
+ @SuppressWarnings("ReferenceEquality")
public static boolean verifySignatures(PackageSetting pkgSetting,
@Nullable SharedUserSetting sharedUserSetting,
PackageSetting disabledPkgSetting, SigningDetails parsedSignatures,
@@ -578,13 +570,23 @@
final String packageName = pkgSetting.getPackageName();
boolean compatMatch = false;
if (pkgSetting.getSigningDetails().getSignatures() != null) {
- // Already existing package. Make sure signatures match
+ // For an already existing package, make sure the parsed signatures from the package
+ // match the one in PackageSetting.
boolean match = parsedSignatures.checkCapability(
pkgSetting.getSigningDetails(),
SigningDetails.CertCapabilities.INSTALLED_DATA)
|| pkgSetting.getSigningDetails().checkCapability(
parsedSignatures,
SigningDetails.CertCapabilities.ROLLBACK);
+ // Also make sure the parsed signatures are consistent with the disabled package
+ // setting, if any. The additional UNKNOWN check is because disabled package settings
+ // may not have SigningDetails currently, and we don't want to cause an uninstall.
+ if (android.security.Flags.extendVbChainToUpdatedApk()
+ && match && disabledPkgSetting != null
+ && disabledPkgSetting.getSigningDetails() != SigningDetails.UNKNOWN) {
+ match = matchSignatureInSystem(packageName, parsedSignatures, disabledPkgSetting);
+ }
+
if (!match && compareCompat) {
match = matchSignaturesCompat(packageName, pkgSetting.getSignatures(),
parsedSignatures);
@@ -603,11 +605,6 @@
SigningDetails.CertCapabilities.ROLLBACK);
}
- if (!match && isApkVerificationForced(disabledPkgSetting)) {
- match = matchSignatureInSystem(packageName, pkgSetting.getSigningDetails(),
- disabledPkgSetting);
- }
-
if (!match && isRollback) {
// Since a rollback can only be initiated for an APK previously installed on the
// device allow rolling back to a previous signing key even if the rollback
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
index 6c39275..b8f726b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
@@ -224,7 +224,7 @@
val scanRequest = ScanRequest(parsedPackage, null, null, null, null,
null, null, null, 0, 0, false, null, null)
val scanResult = ScanResult(scanRequest, null, null, false, 0, null, null, null)
- var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult)
+ var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult, null)
val latestInfoSetting =
mSharedLibrariesImpl.getStaticSharedLibLatestVersionSetting(installRequest)!!
@@ -309,7 +309,7 @@
val testInfo = libOfStatic(TEST_LIB_PACKAGE_NAME, TEST_LIB_NAME, 1L)
val scanResult = ScanResult(mock(), null, null,
false, 0, null, testInfo, null)
- var installRequest = InstallRequest(mock(), 0, 0, UserHandle(0), scanResult)
+ var installRequest = InstallRequest(mock(), 0, 0, UserHandle(0), scanResult, null)
val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest)
@@ -332,7 +332,7 @@
null, null, null, 0, 0, false, null, null)
val scanResult = ScanResult(scanRequest, packageSetting, null,
false, 0, null, null, listOf(testInfo))
- var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult)
+ var installRequest = InstallRequest(parsedPackage, 0, 0, UserHandle(0), scanResult, null)
val allowedInfos = mSharedLibrariesImpl.getAllowedSharedLibInfos(installRequest)