diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 68b06989b24e8..ea2b08deaf7f4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -18654,9 +18654,11 @@ public class PackageManagerService extends IPackageManager.Stub final VersionInfo versionInfo = request.versionInfos.get(installPackageName); final boolean compareCompat = isCompatSignatureUpdateNeeded(versionInfo); final boolean compareRecover = isRecoverSignatureUpdateNeeded(versionInfo); + final boolean isRollback = installArgs != null + && installArgs.installReason == PackageManager.INSTALL_REASON_ROLLBACK; final boolean compatMatch = verifySignatures(signatureCheckPs, disabledPkgSetting, parsedPackage.getSigningDetails(), compareCompat, - compareRecover); + compareRecover, isRollback); // The new KeySets will be re-added later in the scanning process. if (compatMatch) { removeAppKeySetData = true; @@ -19625,6 +19627,7 @@ public class PackageManagerService extends IPackageManager.Stub final boolean fullApp = ((installFlags & PackageManager.INSTALL_FULL_APP) != 0); final boolean virtualPreload = ((installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0); + final boolean isRollback = args.installReason == PackageManager.INSTALL_REASON_ROLLBACK; @ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE; if (args.move != null) { // moving a complete application; perform an initial scan on the new install location @@ -19805,7 +19808,8 @@ public class PackageManagerService extends IPackageManager.Stub parsedPackage); // We don't care about disabledPkgSetting on install for now. final boolean compatMatch = verifySignatures(signatureCheckPs, null, - parsedPackage.getSigningDetails(), compareCompat, compareRecover); + parsedPackage.getSigningDetails(), compareCompat, compareRecover, + isRollback); // The new KeySets will be re-added later in the scanning process. if (compatMatch) { synchronized (mLock) { @@ -20082,15 +20086,23 @@ public class PackageManagerService extends IPackageManager.Stub + pkgName11); } } else { + SigningDetails parsedPkgSigningDetails = parsedPackage.getSigningDetails(); + SigningDetails oldPkgSigningDetails = oldPackage.getSigningDetails(); // default to original signature matching - if (!parsedPackage.getSigningDetails().checkCapability( - oldPackage.getSigningDetails(), + if (!parsedPkgSigningDetails.checkCapability(oldPkgSigningDetails, SigningDetails.CertCapabilities.INSTALLED_DATA) - && !oldPackage.getSigningDetails().checkCapability( - parsedPackage.getSigningDetails(), + && !oldPkgSigningDetails.checkCapability(parsedPkgSigningDetails, SigningDetails.CertCapabilities.ROLLBACK)) { - throw new PrepareFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE, - "New package has a different signature: " + pkgName11); + // Allow the update to proceed if this is a rollback and the parsed + // package's current signing key is the current signer or in the lineage + // of the old package; this allows a rollback to a previously installed + // version after an app's signing key has been rotated without requiring + // the rollback capability on the previous signing key. + if (!isRollback || !oldPkgSigningDetails.hasAncestorOrSelf( + parsedPkgSigningDetails)) { + throw new PrepareFailure(INSTALL_FAILED_UPDATE_INCOMPATIBLE, + "New package has a different signature: " + pkgName11); + } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index d3d7c6055fcec..e6a4282950996 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -621,7 +621,7 @@ public class PackageManagerServiceUtils { */ public static boolean verifySignatures(PackageSetting pkgSetting, PackageSetting disabledPkgSetting, PackageParser.SigningDetails parsedSignatures, - boolean compareCompat, boolean compareRecover) + boolean compareCompat, boolean compareRecover, boolean isRollback) throws PackageManagerException { final String packageName = pkgSetting.name; boolean compatMatch = false; @@ -655,6 +655,13 @@ public class PackageManagerServiceUtils { match = matchSignatureInSystem(pkgSetting, 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 + // capability has not been granted. + match = pkgSetting.signatures.mSigningDetails.hasAncestorOrSelf(parsedSignatures); + } + if (!match) { throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package " + packageName + diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index e12991a220c96..9560f59924dee 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -563,6 +563,7 @@ class Rollback { params.setRequestDowngrade(true); params.setRequiredInstalledVersionCode( pkgRollbackInfo.getVersionRolledBackFrom().getLongVersionCode()); + params.setInstallReason(PackageManager.INSTALL_REASON_ROLLBACK); if (isStaged()) { params.setStaged(); } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index 0db2b2af72601..7b2a07fd80f8a 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -1225,4 +1225,44 @@ public class RollbackTest { InstallUtils.dropShellPermissionIdentity(); } } + + /** + * Tests an app can be rolled back to the previous signing key. + * + *
The rollback capability in the signing lineage allows an app to be updated to an APK + * signed with a previous signing key in the lineage; however this often defeats the purpose + * of key rotation as a compromised key could then be used to roll an app back to the previous + * key. To avoid requiring the rollback capability to support app rollbacks the PackageManager + * allows an app to be rolled back to the previous signing key if the rollback install reason + * is set. + */ + @Test + public void testRollbackAfterKeyRotation() throws Exception { + try { + InstallUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS); + + // Uninstall TestApp.A + Uninstall.packages(TestApp.A); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(-1); + + // Install v1 of the app with the original signing key (without rollbacks enabled). + Install.single(TestApp.AOriginal1).commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + + // Upgrade from v1 to v2 with the rotated signing key, with rollbacks enabled. + Install.single(TestApp.ARotated2).setEnableRollback().commit(); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + + // Roll back the app. + RollbackInfo available = waitForAvailableRollback(TestApp.A); + RollbackUtils.rollback(available.getRollbackId()); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + } finally { + InstallUtils.dropShellPermissionIdentity(); + } + } }