From 51903825c20adde491049d7132f92cb093d3c845 Mon Sep 17 00:00:00 2001 From: Nikita Ioffe Date: Thu, 3 Jun 2021 03:14:06 +0100 Subject: [PATCH] Check for downgrade and signature of outer .apex certificate This logic is very similar to the one in StagingManager. We can look into consolidating it in one place as one of follow up tasks. Bug: 188713546 Bug: 187864524 Test: atest ApexManagerTest Test: adb install --force-non-staged rebootless_v2.apex Test: adb install pm list packages --apex-only -f Change-Id: I12ff0ce121a349799062c76302de7c8f6407d8c2 --- .../com/android/server/pm/ApexManager.java | 88 ++++++++++- .../server/pm/PackageManagerService.java | 6 +- services/tests/servicestests/Android.bp | 5 + .../android/server/pm/ApexManagerTest.java | 149 ++++++++++++++++-- 4 files changed, 223 insertions(+), 25 deletions(-) diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index c97095d8a54e9..3d328f8d8a680 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -32,6 +32,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageParser.PackageParserException; +import android.content.pm.SigningDetails; import android.content.pm.parsing.PackageInfoWithoutStateUtils; import android.content.pm.parsing.ParsingPackageUtils; import android.os.Binder; @@ -45,6 +46,7 @@ import android.util.ArraySet; import android.util.Singleton; import android.util.Slog; import android.util.SparseArray; +import android.util.apk.ApkSignatureVerifier; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -52,6 +54,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.parsing.pkg.AndroidPackage; +import com.android.server.pm.parsing.pkg.ParsedPackage; import com.android.server.utils.TimingsTraceAndSlog; import com.google.android.collect.Lists; @@ -390,9 +393,10 @@ public abstract class ApexManager { throws RemoteException; /** - * Performs a non-staged install of an APEX package with given {@code packagePath}. + * Performs a non-staged install of the given {@code apexFile}. */ - abstract void installPackage(String packagePath) throws PackageManagerException; + abstract void installPackage(File apexFile, PackageParser2 packageParser) + throws PackageManagerException; /** * Dumps various state information to the provided {@link PrintWriter} object. @@ -979,12 +983,80 @@ public abstract class ApexManager { waitForApexService().reserveSpaceForCompressedApex(infoList); } - @Override - void installPackage(String packagePath) throws PackageManagerException { + private SigningDetails getSigningDetails(PackageInfo pkg) throws PackageManagerException { + int minSignatureScheme = + ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + pkg.applicationInfo.targetSdkVersion); try { - // TODO(b/187864524): do pre-install verification. - waitForApexService().installAndActivatePackage(packagePath); - // TODO(b/187864524): update mAllPackagesCache. + return ApkSignatureVerifier.verify(pkg.applicationInfo.sourceDir, + minSignatureScheme); + } catch (PackageParserException e) { + throw PackageManagerException.from(e); + } + } + + private void checkApexSignature(PackageInfo existingApexPkg, PackageInfo newApexPkg) + throws PackageManagerException { + final SigningDetails existingSigningDetails = getSigningDetails(existingApexPkg); + final SigningDetails newSigningDetails = getSigningDetails(newApexPkg); + if (!newSigningDetails.checkCapability(existingSigningDetails, + SigningDetails.CertCapabilities.INSTALLED_DATA)) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, + "APK container signature of " + newApexPkg.applicationInfo.sourceDir + + " is not compatible with currently installed on device"); + } + } + + private void checkDowngrade(PackageInfo existingApexPkg, PackageInfo newApexPkg) + throws PackageManagerException { + final long currentVersionCode = existingApexPkg.applicationInfo.longVersionCode; + final long newVersionCode = newApexPkg.applicationInfo.longVersionCode; + if (currentVersionCode > newVersionCode) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, + "Downgrade of APEX package " + newApexPkg.packageName + + " is not allowed"); + } + } + + @Override + void installPackage(File apexFile, PackageParser2 packageParser) + throws PackageManagerException { + try { + final int flags = PackageManager.GET_META_DATA + | PackageManager.GET_SIGNING_CERTIFICATES + | PackageManager.GET_SIGNATURES; + final ParsedPackage parsedPackage = packageParser.parsePackage( + apexFile, flags, /* useCaches= */ false); + final PackageInfo newApexPkg = PackageInfoWithoutStateUtils.generate(parsedPackage, + /* apexInfo= */ null, flags); + if (newApexPkg == null) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_INVALID_APK, + "Failed to generate package info for " + apexFile.getAbsolutePath()); + } + final PackageInfo existingApexPkg = getPackageInfo(newApexPkg.packageName, + MATCH_ACTIVE_PACKAGE); + if (existingApexPkg == null) { + Slog.w(TAG, "Attempting to install new APEX package " + newApexPkg.packageName); + throw new PackageManagerException(PackageManager.INSTALL_FAILED_PACKAGE_CHANGED, + "It is forbidden to install new APEX packages"); + } + checkApexSignature(existingApexPkg, newApexPkg); + checkDowngrade(existingApexPkg, newApexPkg); + ApexInfo apexInfo = waitForApexService().installAndActivatePackage( + apexFile.getAbsolutePath()); + final ParsedPackage parsedPackage2 = packageParser.parsePackage( + new File(apexInfo.modulePath), flags, /* useCaches= */ false); + final PackageInfo finalApexPkg = PackageInfoWithoutStateUtils.generate( + parsedPackage, apexInfo, flags); + // Installation was successful, time to update mAllPackagesCache + synchronized (mLock) { + for (int i = 0, size = mAllPackagesCache.size(); i < size; i++) { + if (mAllPackagesCache.get(i).equals(existingApexPkg)) { + mAllPackagesCache.set(i, finalApexPkg); + break; + } + } + } } catch (RemoteException e) { throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, "apexservice not available"); @@ -1262,7 +1334,7 @@ public abstract class ApexManager { } @Override - void installPackage(String packagePath) { + void installPackage(File apexFile, PackageParser2 packageParser) { throw new UnsupportedOperationException("APEX updates are not supported"); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 02ea3c117297e..e42e221106579 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -17120,7 +17120,7 @@ public class PackageManagerService extends IPackageManager.Stub try { // Should directory scanning logic be moved to ApexManager for better test coverage? final File dir = request.args.origin.resolvedFile; - final String[] apexes = dir.list(); + final File[] apexes = dir.listFiles(); if (apexes == null) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, dir.getAbsolutePath() + " is not a directory"); @@ -17130,7 +17130,9 @@ public class PackageManagerService extends IPackageManager.Stub "Expected exactly one .apex file under " + dir.getAbsolutePath() + " got: " + apexes.length); } - mApexManager.installPackage(dir.getAbsolutePath() + "/" + apexes[0]); + try (PackageParser2 packageParser = mInjector.getScanningPackageParser()) { + mApexManager.installPackage(apexes[0], packageParser); + } } catch (PackageManagerException e) { request.installResult.setError("APEX installation failed", e); } diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 9d055e0d431fb..339a5f916b4b8 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -118,6 +118,11 @@ android_test { ":PackageParserTestApp3", ":PackageParserTestApp4", ":apex.test", + ":test.rebootless_apex_v1", + ":test.rebootless_apex_v2", + ":com.android.apex.cts.shim.v1_prebuilt", + ":com.android.apex.cts.shim.v2_different_certificate_prebuilt", + ":com.android.apex.cts.shim.v2_unsigned_apk_container_prebuilt", ], resource_zips: [":FrameworksServicesTests_apks_as_resources"], } diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java index 9b25d0dc9c773..4e350b673b38c 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -29,6 +30,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import static org.testng.Assert.expectThrows; import android.apex.ApexInfo; import android.apex.ApexSessionInfo; @@ -84,7 +86,7 @@ public class ApexManagerTest { @Test public void testGetPackageInfo_setFlagsMatchActivePackage() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, false)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); final PackageInfo activePkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG, @@ -101,7 +103,7 @@ public class ApexManagerTest { @Test public void testGetPackageInfo_setFlagsMatchFactoryPackage() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); PackageInfo factoryPkgPi = mApexManager.getPackageInfo(TEST_APEX_PKG, @@ -118,7 +120,7 @@ public class ApexManagerTest { @Test public void testGetPackageInfo_setFlagsNone() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -127,7 +129,7 @@ public class ApexManagerTest { @Test public void testGetActivePackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -136,7 +138,7 @@ public class ApexManagerTest { @Test public void testGetActivePackages_noneActivePackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -145,7 +147,7 @@ public class ApexManagerTest { @Test public void testGetFactoryPackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -154,7 +156,7 @@ public class ApexManagerTest { @Test public void testGetFactoryPackages_noneFactoryPackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, false)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -163,7 +165,7 @@ public class ApexManagerTest { @Test public void testGetInactivePackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -172,7 +174,7 @@ public class ApexManagerTest { @Test public void testGetInactivePackages_noneInactivePackages() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, false)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, false)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -181,7 +183,7 @@ public class ApexManagerTest { @Test public void testIsApexPackage() throws RemoteException { - when(mApexService.getAllPackages()).thenReturn(createApexInfo(false, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(false, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -276,11 +278,11 @@ public class ApexManagerTest { @Test public void testReportErrorWithApkInApex() throws RemoteException { - when(mApexService.getActivePackages()).thenReturn(createApexInfo(true, true)); + when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true)); final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0); assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG); - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -297,7 +299,7 @@ public class ApexManagerTest { */ @Test public void testRegisterApkInApexDoesNotRegisterSimilarPrefix() throws RemoteException { - when(mApexService.getActivePackages()).thenReturn(createApexInfo(true, true)); + when(mApexService.getActivePackages()).thenReturn(createApexInfoForTestPkg(true, true)); final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0); assertThat(activeApex.apexModuleName).isEqualTo(TEST_APEX_PKG); @@ -305,7 +307,7 @@ public class ApexManagerTest { when(fakeApkInApex.getBaseApkPath()).thenReturn("/apex/" + TEST_APEX_PKG + "randomSuffix"); when(fakeApkInApex.getPackageName()).thenReturn("randomPackageName"); - when(mApexService.getAllPackages()).thenReturn(createApexInfo(true, true)); + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, true)); mApexManager.scanApexPackagesTraced(mPackageParser2, ParallelPackageParser.makeExecutorService()); @@ -314,7 +316,112 @@ public class ApexManagerTest { assertThat(mApexManager.getApksInApex(activeApex.apexModuleName)).isEmpty(); } - private ApexInfo[] createApexInfo(boolean isActive, boolean isFactory) { + @Test + public void testInstallPackageFailsToInstallNewApex() throws Exception { + when(mApexService.getAllPackages()).thenReturn(createApexInfoForTestPkg(true, false)); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + File apex = extractResource("test.apex_rebootless_v1", "test.rebootless_apex_v1.apex"); + PackageManagerException e = expectThrows(PackageManagerException.class, + () -> mApexManager.installPackage(apex, mPackageParser2)); + assertThat(e).hasMessageThat().contains("It is forbidden to install new APEX packages"); + } + + @Test + public void testInstallPackageDowngrade() throws Exception { + File activeApex = extractResource("test.apex_rebootless_v2", + "test.rebootless_apex_v2.apex"); + ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true, + /* isFactory= */ false, activeApex); + when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo}); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + File installedApex = extractResource("test.apex_rebootless_v1", + "test.rebootless_apex_v1.apex"); + PackageManagerException e = expectThrows(PackageManagerException.class, + () -> mApexManager.installPackage(installedApex, mPackageParser2)); + assertThat(e).hasMessageThat().contains( + "Downgrade of APEX package test.apex.rebootless is not allowed"); + } + + @Test + public void testInstallPackage() throws Exception { + ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true, + /* isFactory= */ false, extractResource("test.apex_rebootless_v1", + "test.rebootless_apex_v1.apex")); + when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo}); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + File finalApex = extractResource("test.rebootles_apex_v2", "test.rebootless_apex_v2.apex"); + ApexInfo newApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true, + /* isFactory= */ false, finalApex); + when(mApexService.installAndActivatePackage(anyString())).thenReturn(newApexInfo); + + File installedApex = extractResource("installed", "test.rebootless_apex_v2.apex"); + mApexManager.installPackage(installedApex, mPackageParser2); + + PackageInfo newInfo = mApexManager.getPackageInfo("test.apex.rebootless", + ApexManager.MATCH_ACTIVE_PACKAGE); + assertThat(newInfo.applicationInfo.sourceDir).isEqualTo(finalApex.getAbsolutePath()); + assertThat(newInfo.applicationInfo.longVersionCode).isEqualTo(2); + } + + @Test + public void testInstallPackageBinderCallFails() throws Exception { + ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true, + /* isFactory= */ false, extractResource("test.apex_rebootless_v1", + "test.rebootless_apex_v1.apex")); + when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo}); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + when(mApexService.installAndActivatePackage(anyString())).thenThrow( + new RuntimeException("install failed :(")); + + File installedApex = extractResource("test.apex_rebootless_v1", + "test.rebootless_apex_v1.apex"); + assertThrows(PackageManagerException.class, + () -> mApexManager.installPackage(installedApex, mPackageParser2)); + } + + @Test + public void testInstallPackageSignedWithWrongCertificate() throws Exception { + File activeApex = extractResource("shim_v1", "com.android.apex.cts.shim.apex"); + ApexInfo activeApexInfo = createApexInfo("com.android.apex.cts.shim", 1, + /* isActive= */ true, /* isFactory= */ false, activeApex); + when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo}); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + File installedApex = extractResource("shim_different_certificate", + "com.android.apex.cts.shim.v2_different_certificate.apex"); + PackageManagerException e = expectThrows(PackageManagerException.class, + () -> mApexManager.installPackage(installedApex, mPackageParser2)); + assertThat(e).hasMessageThat().contains("APK container signature of "); + assertThat(e).hasMessageThat().contains( + "is not compatible with currently installed on device"); + } + + @Test + public void testInstallPackageUnsignedApexContainer() throws Exception { + File activeApex = extractResource("shim_v1", "com.android.apex.cts.shim.apex"); + ApexInfo activeApexInfo = createApexInfo("com.android.apex.cts.shim", 1, + /* isActive= */ true, /* isFactory= */ false, activeApex); + when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo}); + mApexManager.scanApexPackagesTraced(mPackageParser2, + ParallelPackageParser.makeExecutorService()); + + File installedApex = extractResource("shim_unsigned_apk_container", + "com.android.apex.cts.shim.v2_unsigned_apk_container.apex"); + PackageManagerException e = expectThrows(PackageManagerException.class, + () -> mApexManager.installPackage(installedApex, mPackageParser2)); + assertThat(e).hasMessageThat().contains("Failed to collect certificates from "); + } + + private ApexInfo[] createApexInfoForTestPkg(boolean isActive, boolean isFactory) { File apexFile = extractResource(TEST_APEX_PKG, TEST_APEX_FILE_NAME); ApexInfo apexInfo = new ApexInfo(); apexInfo.isActive = isActive; @@ -327,6 +434,17 @@ public class ApexManagerTest { return new ApexInfo[]{apexInfo}; } + private ApexInfo createApexInfo(String moduleName, int versionCode, boolean isActive, + boolean isFactory, File apexFile) { + ApexInfo apexInfo = new ApexInfo(); + apexInfo.moduleName = moduleName; + apexInfo.versionCode = versionCode; + apexInfo.isActive = isActive; + apexInfo.isFactory = isFactory; + apexInfo.modulePath = apexFile.getPath(); + return apexInfo; + } + private ApexSessionInfo getFakeStagedSessionInfo() { ApexSessionInfo stagedSessionInfo = new ApexSessionInfo(); stagedSessionInfo.sessionId = TEST_SESSION_ID; @@ -358,6 +476,7 @@ public class ApexManagerTest { } catch (IOException e) { throw new AssertionError("CreateTempFile IOException" + e); } + try ( InputStream in = ApexManager.class.getClassLoader() .getResourceAsStream(fullResourceName);