diff options
author | Nikita Ioffe <ioffe@google.com> | 2021-06-07 16:56:21 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2021-06-07 16:56:21 +0000 |
commit | ab4f8efa7fb3626b2710bb1cbcb46fb54eb23fef (patch) | |
tree | e6ee7074e504ea10a6e635f8bcf748c80975df73 | |
parent | 7a19e4a94ecbc1e6f02a3fe5f67c98b90bebec01 (diff) | |
parent | 59344a6bc52aa0219066f850c0017e6b3904649c (diff) |
Merge "Check for downgrade and signature of outer .apex certificate" into sc-dev
4 files changed, 222 insertions, 24 deletions
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index c97095d8a54e..3369dcd67b4d 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.PackageParser.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); } + private SigningDetails getSigningDetails(PackageInfo pkg) throws PackageManagerException { + int minSignatureScheme = + ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + pkg.applicationInfo.targetSdkVersion); + try { + 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(String packagePath) throws PackageManagerException { + void installPackage(File apexFile, PackageParser2 packageParser) + throws PackageManagerException { try { - // TODO(b/187864524): do pre-install verification. - waitForApexService().installAndActivatePackage(packagePath); - // TODO(b/187864524): update mAllPackagesCache. + 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 be2a63d3f11e..f00b3a0549cb 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -17118,7 +17118,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"); @@ -17128,7 +17128,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 9d055e0d431f..339a5f916b4b 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 9b25d0dc9c77..4e350b673b38 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); |