Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 480843a9 authored by Nikita Ioffe's avatar Nikita Ioffe Committed by Automerger Merge Worker
Browse files

Merge "Check for downgrade and signature of outer .apex certificate" into sc-dev am: ab4f8efa

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14807571

Change-Id: Ic2b87598ef37b42576de43e155075555a70d1e30
parents cbce4f35 ab4f8efa
Loading
Loading
Loading
Loading
+79 −7
Original line number Diff line number Diff line
@@ -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");
        }

+4 −2
Original line number Diff line number Diff line
@@ -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);
        }
+5 −0
Original line number Diff line number Diff line
@@ -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"],
}
+134 −15
Original line number Diff line number Diff line
@@ -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);