[ECM] Add ECM allowlist to SystemConfig

This introduces a new configurable XML file
(/etc/sysconfig/enhanced-confirmation.xml) for ECM (Enhanced
Confirmation Mode). This file enables OEMs to declare a list of
"trusted packages" and/or "trusted installer" packages.  A "trusted
package" will be exempt from ECM restrictions. A "trusted installer",
and all packages that it installs, will be exempt from ECM restrictions.

The file may contain zero or more XML elements of the form:

    <enhanced-confirmation-trusted-package
         package="com.example.app"
         sha256-cert-digest="E9:7A:BC:2C:D1:..."/>

...and/or...

    <enhanced-confirmation-trusted-installer
         package="com.example.app"
         sha256-cert-digest="E9:7A:BC:2C:D1:..."/>

(Where the 'package' attribute is a package name, and
'sha256-cert-digest' is a hex-encoded SHA-256 digest of a signing
certificate. Both fields are required for each XML element.)

This file is parsed by the SystemConfig class, where the collection of
all XML elements are deserialized into (SignedPackage) objects which
are cached within SystemConfig.

These objects are accessible by calling either the following SystemAPI
methods:

    SystemConfigManager::getEnhancedConfirmationTrustedPackages
    SystemConfigManager::getEnhancedConfirmationTrustedInstallers

...which in turn call the (respective) binder methods:

    SystemConfigService::getEnhancedConfirmationTrustedPackages
    SystemConfigService::getEnhancedConfirmationTrustedInstallers

...which read the data directly from SystemConfig.

The only intended caller of this API is ECM
(EnhancedConfirmationManager/EnhancedConfirmationService), which runs in
SystemServer.

The reason this needs to be SystemApi(MODULE_LIBRARIES) is that the ECM
source code lives within the packages/modules/Permission mainline
module.

Bug: 310654834
Test: atest FrameworksServicesTests:com.android.server.systemconfig.SystemConfigTest
Change-Id: I50e524e5782cea4e66232acef493edbe62aa1f61
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 24b9233..f02402c 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -127,6 +127,11 @@
     field public static final int MATCH_STATIC_SHARED_AND_SDK_LIBRARIES = 67108864; // 0x4000000
   }
 
+  @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public class SignedPackage {
+    method @NonNull public byte[] getCertificateDigest();
+    method @NonNull public String getPkgName();
+  }
+
 }
 
 package android.hardware.usb {
@@ -422,6 +427,8 @@
 
   public class SystemConfigManager {
     method @NonNull public java.util.List<android.content.ComponentName> getEnabledComponentOverrides(@NonNull String);
+    method @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public java.util.Set<android.content.pm.SignedPackage> getEnhancedConfirmationTrustedInstallers();
+    method @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public java.util.Set<android.content.pm.SignedPackage> getEnhancedConfirmationTrustedPackages();
   }
 
   public final class Trace {
diff --git a/core/java/android/content/pm/SignedPackage.java b/core/java/android/content/pm/SignedPackage.java
new file mode 100644
index 0000000..4d1b136
--- /dev/null
+++ b/core/java/android/content/pm/SignedPackage.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package android.content.pm;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A data class representing a package and (SHA-256 hash of) a signing certificate.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+@FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+public class SignedPackage {
+    @NonNull
+    private final SignedPackageParcel mData;
+
+    /** @hide */
+    public SignedPackage(@NonNull String pkgName, @NonNull byte[] certificateDigest) {
+        SignedPackageParcel data = new SignedPackageParcel();
+        data.pkgName = pkgName;
+        data.certificateDigest = certificateDigest;
+        mData = data;
+    }
+
+    /** @hide */
+    public SignedPackage(@NonNull SignedPackageParcel data) {
+        mData = data;
+    }
+
+    /** @hide */
+    public final @NonNull SignedPackageParcel getData() {
+        return mData;
+    }
+
+    public @NonNull String getPkgName() {
+        return mData.pkgName;
+    }
+
+    public @NonNull byte[] getCertificateDigest() {
+        return mData.certificateDigest;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof SignedPackage that)) return false;
+        return mData.pkgName.equals(that.mData.pkgName) && Arrays.equals(mData.certificateDigest,
+                that.mData.certificateDigest);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mData.pkgName, Arrays.hashCode(mData.certificateDigest));
+    }
+}
diff --git a/core/java/android/content/pm/SignedPackageParcel.aidl b/core/java/android/content/pm/SignedPackageParcel.aidl
new file mode 100644
index 0000000..7957f7f
--- /dev/null
+++ b/core/java/android/content/pm/SignedPackageParcel.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package android.content.pm;
+
+import android.content.ComponentName;
+
+/** @hide */
+parcelable SignedPackageParcel {
+    String pkgName;
+    byte[] certificateDigest;
+}
diff --git a/core/java/android/os/ISystemConfig.aidl b/core/java/android/os/ISystemConfig.aidl
index b7649ba..650aead 100644
--- a/core/java/android/os/ISystemConfig.aidl
+++ b/core/java/android/os/ISystemConfig.aidl
@@ -17,6 +17,8 @@
 package android.os;
 
 import android.content.ComponentName;
+import android.os.Bundle;
+import android.content.pm.SignedPackageParcel;
 
 /**
   * Binder interface to query SystemConfig in the system server.
@@ -57,4 +59,14 @@
      * @see SystemConfigManager#getPreventUserDisablePackages
      */
     List<String> getPreventUserDisablePackages();
+
+    /**
+     * @see SystemConfigManager#getEnhancedConfirmationTrustedPackages
+     */
+    List<SignedPackageParcel> getEnhancedConfirmationTrustedPackages();
+
+    /**
+     * @see SystemConfigManager#getEnhancedConfirmationTrustedInstallers
+     */
+    List<SignedPackageParcel> getEnhancedConfirmationTrustedInstallers();
 }
diff --git a/core/java/android/os/SystemConfigManager.java b/core/java/android/os/SystemConfigManager.java
index 21ffbf1..13bc398 100644
--- a/core/java/android/os/SystemConfigManager.java
+++ b/core/java/android/os/SystemConfigManager.java
@@ -16,12 +16,15 @@
 package android.os;
 
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.SignedPackage;
+import android.content.pm.SignedPackageParcel;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -29,6 +32,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 
 /**
@@ -175,4 +179,69 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+
+    /**
+     * Returns a set of signed packages, represented as (packageName, certificateDigest) pairs, that
+     * should be considered "trusted packages" by ECM (Enhanced Confirmation Mode).
+     *
+     * <p>"Trusted packages" are exempt from ECM (i.e., they will never be considered "restricted").
+     *
+     * <p>A package will be considered "trusted package" if and only if it *matches* least one of
+     * the (*packageName*, *certificateDigest*) pairs in this set, where *matches* means satisfying
+     * both of the following:
+     *
+     * <ol>
+     *   <li>The package's name equals *packageName*
+     *   <li>The package is, or was ever, signed by *certificateDigest*, according to the package's
+     *       {@link android.content.pm.SigningDetails}
+     * </ol>
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+    @RequiresPermission(Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
+    @NonNull
+    public Set<SignedPackage> getEnhancedConfirmationTrustedPackages() {
+        try {
+            List<SignedPackageParcel> parcels = mInterface.getEnhancedConfirmationTrustedPackages();
+            return parcels.stream().map(SignedPackage::new).collect(Collectors.toSet());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a set of signed packages, represented as (packageName, certificateDigest) pairs, that
+     * should be considered "trusted installers" by ECM (Enhanced Confirmation Mode).
+     *
+     * <p>"Trusted installers", and all apps installed by a trusted installer, are exempt from ECM
+     * (i.e., they will never be considered "restricted").
+     *
+     * <p>A package will be considered a "trusted installer" if and only if it *matches* least one
+     * of the (*packageName*, *certificateDigest*) pairs in this set, where *matches* means
+     * satisfying both of the following:
+     *
+     * <ol>
+     *   <li>The package's name equals *packageName*
+     *   <li>The package is, or was ever, signed by *certificateDigest*, according to the package's
+     *       {@link android.content.pm.SigningDetails}
+     * </ol>
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+    @RequiresPermission(Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES)
+    @NonNull
+    public Set<SignedPackage> getEnhancedConfirmationTrustedInstallers() {
+        try {
+            List<SignedPackageParcel> parcels =
+                    mInterface.getEnhancedConfirmationTrustedInstallers();
+            return parcels.stream().map(SignedPackage::new).collect(Collectors.toSet());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index ade20d2..1fd1003 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -66,6 +66,12 @@
     src: "preinstalled-packages-strict-signature.xml",
 }
 
+prebuilt_etc {
+    name: "enhanced-confirmation.xml",
+    sub_dir: "sysconfig",
+    src: "enhanced-confirmation.xml",
+}
+
 // Privapp permission whitelist files
 
 prebuilt_etc {
diff --git a/data/etc/enhanced-confirmation.xml b/data/etc/enhanced-confirmation.xml
new file mode 100644
index 0000000..4a9dd2f
--- /dev/null
+++ b/data/etc/enhanced-confirmation.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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 defines an allowlist of packages that should be exempt from ECM (Enhanced Confirmation
+Mode).
+
+Example usage:
+
+    <enhanced-confirmation-trusted-installer
+         package="com.example.app"
+         signature="E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0"/>
+
+This indicates that "com.example.app" should be exempt from ECM, and that, if "com.example.app" is
+an installer, all packages installed via "com.example.app" will also be exempt from ECM.
+-->
+
+<config></config>
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index a493d7a..797a2e6 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -24,6 +24,8 @@
 import android.content.ComponentName;
 import android.content.pm.FeatureInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SignedPackage;
 import android.os.Build;
 import android.os.CarrierAssociatedAppEntry;
 import android.os.Environment;
@@ -350,6 +352,16 @@
     // updated to avoid cached/potentially tampered results.
     private final Set<String> mPreinstallPackagesWithStrictSignatureCheck = new ArraySet<>();
 
+    // A set of packages that should be considered "trusted packages" by ECM (Enhanced
+    // Confirmation Mode). "Trusted packages" are exempt from ECM (i.e., they will never be
+    // considered "restricted").
+    private final ArraySet<SignedPackage> mEnhancedConfirmationTrustedPackages = new ArraySet<>();
+
+    // A set of packages that should be considered "trusted installers" by ECM (Enhanced
+    // Confirmation Mode). "Trusted installers", and all apps installed by a trusted installer, are
+    // exempt from ECM (i.e., they will never be considered "restricted").
+    private final ArraySet<SignedPackage> mEnhancedConfirmationTrustedInstallers = new ArraySet<>();
+
     /**
      * Map of system pre-defined, uniquely named actors; keys are namespace,
      * value maps actor name to package name.
@@ -560,6 +572,14 @@
         return mPreinstallPackagesWithStrictSignatureCheck;
     }
 
+    public ArraySet<SignedPackage> getEnhancedConfirmationTrustedPackages() {
+        return mEnhancedConfirmationTrustedPackages;
+    }
+
+    public ArraySet<SignedPackage> getEnhancedConfirmationTrustedInstallers() {
+        return mEnhancedConfirmationTrustedInstallers;
+    }
+
     /**
      * Only use for testing. Do NOT use in production code.
      * @param readPermissions false to create an empty SystemConfig; true to read the permissions.
@@ -1558,6 +1578,26 @@
                             }
                         }
                     } break;
+                    case "enhanced-confirmation-trusted-package": {
+                        if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+                            SignedPackage signedPackage = parseEnhancedConfirmationTrustedPackage(
+                                    parser, permFile, name);
+                            if (signedPackage != null) {
+                                mEnhancedConfirmationTrustedPackages.add(signedPackage);
+                            }
+                            break;
+                        }
+                    } // fall through if flag is not enabled
+                    case "enhanced-confirmation-trusted-installer": {
+                        if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+                            SignedPackage signedPackage = parseEnhancedConfirmationTrustedPackage(
+                                    parser, permFile, name);
+                            if (signedPackage != null) {
+                                mEnhancedConfirmationTrustedInstallers.add(signedPackage);
+                            }
+                            break;
+                        }
+                    } // fall through if flag is not enabled
                     default: {
                         Slog.w(TAG, "Tag " + name + " is unknown in "
                                 + permFile + " at " + parser.getPositionDescription());
@@ -1619,6 +1659,33 @@
         }
     }
 
+    private @Nullable SignedPackage parseEnhancedConfirmationTrustedPackage(XmlPullParser parser,
+            File permFile, String elementName) {
+        String pkgName = parser.getAttributeValue(null, "package");
+        if (TextUtils.isEmpty(pkgName)) {
+            Slog.w(TAG, "<" + elementName + "> without package " + permFile + " at "
+                    + parser.getPositionDescription());
+            return null;
+        }
+
+        String certificateDigestStr = parser.getAttributeValue(null, "sha256-cert-digest");
+        if (TextUtils.isEmpty(certificateDigestStr)) {
+            Slog.w(TAG, "<" + elementName + "> without sha256-cert-digest in " + permFile
+                    + " at " + parser.getPositionDescription());
+            return null;
+        }
+        byte[] certificateDigest = null;
+        try {
+            certificateDigest = new Signature(certificateDigestStr).toByteArray();
+        } catch (IllegalArgumentException e) {
+            Slog.w(TAG, "<" + elementName + "> with invalid sha256-cert-digest in "
+                    + permFile + " at " + parser.getPositionDescription());
+            return null;
+        }
+
+        return new SignedPackage(pkgName, certificateDigest);
+    }
+
     // This method only enables a new Android feature added in U and will not have impact on app
     // compatibility
     @SuppressWarnings("AndroidFrameworkCompatChange")
diff --git a/services/java/com/android/server/SystemConfigService.java b/services/java/com/android/server/SystemConfigService.java
index fd21a32..2359422c9 100644
--- a/services/java/com/android/server/SystemConfigService.java
+++ b/services/java/com/android/server/SystemConfigService.java
@@ -22,6 +22,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManagerInternal;
+import android.content.pm.SignedPackage;
+import android.content.pm.SignedPackageParcel;
 import android.os.Binder;
 import android.os.ISystemConfig;
 import android.util.ArrayMap;
@@ -119,6 +121,26 @@
                             pmi.canQueryPackage(Binder.getCallingUid(), preventUserDisablePackage))
                     .collect(toList());
         }
+
+        @Override
+        public List<SignedPackageParcel> getEnhancedConfirmationTrustedPackages() {
+            getContext().enforceCallingOrSelfPermission(
+                    Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES,
+                    "Caller must hold " + Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES);
+
+            return SystemConfig.getInstance().getEnhancedConfirmationTrustedPackages().stream()
+                    .map(SignedPackage::getData).toList();
+        }
+
+        @Override
+        public List<SignedPackageParcel> getEnhancedConfirmationTrustedInstallers() {
+            getContext().enforceCallingOrSelfPermission(
+                    Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES,
+                    "Caller must hold " + Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES);
+
+            return SystemConfig.getInstance().getEnhancedConfirmationTrustedInstallers().stream()
+                    .map(SignedPackage::getData).toList();
+        }
     };
 
     public SystemConfigService(Context context) {
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index aca96ad..03cdbbd 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -21,8 +21,13 @@
 import static org.junit.Assert.assertEquals;
 import static org.testng.Assert.expectThrows;
 
+import android.content.pm.Signature;
+import android.content.pm.SignedPackage;
 import android.os.Build;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -68,6 +73,8 @@
 
     @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
 
+    @Rule public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mSysConfig = new SystemConfigTestClass();
@@ -696,6 +703,43 @@
         assertThat(mSysConfig.getSystemAppUpdateOwnerPackageName("com.foo")).isNull();
     }
 
+    /**
+     * Tests that SystemConfig::getEnhancedConfirmationTrustedInstallers correctly parses a list of
+     * SignedPackage objects.
+     */
+    @Test
+    @RequiresFlagsEnabled(
+            android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
+    public void getEnhancedConfirmationTrustedInstallers_returnsTrustedInstallers()
+            throws IOException {
+        String pkgName = "com.example.app";
+        String certificateDigestStr = "E9:7A:BC:2C:D1:CA:8D:58:6A:57:0B:8C:F8:60:AA:D2:"
+                + "8D:13:30:2A:FB:C9:00:2C:5D:53:B2:6C:09:A4:85:A0";
+
+        byte[] certificateDigest = new Signature(certificateDigestStr).toByteArray();
+        String contents = "<config>"
+                + "<" + "enhanced-confirmation-trusted-installer" + " "
+                + "package=\"" + pkgName + "\""
+                + " sha256-cert-digest=\"" + certificateDigestStr + "\""
+                + "/>"
+                + "</config>";
+
+        final File folder = createTempSubfolder("folder");
+        createTempFile(folder, "enhanced-confirmation.xml", contents);
+        readPermissions(folder, /* Grant all permission flags */ ~0);
+
+        ArraySet<SignedPackage> actualTrustedInstallers =
+                mSysConfig.getEnhancedConfirmationTrustedInstallers();
+
+        assertThat(actualTrustedInstallers.size()).isEqualTo(1);
+        SignedPackage actual = actualTrustedInstallers.stream().findFirst().orElseThrow();
+        SignedPackage expected = new SignedPackage(pkgName, certificateDigest);
+
+        assertThat(actual.getCertificateDigest()).isEqualTo(expected.getCertificateDigest());
+        assertThat(actual.getPkgName()).isEqualTo(expected.getPkgName());
+        assertThat(actual).isEqualTo(expected);
+    }
+
     private void parseSharedLibraries(String contents) throws IOException {
         File folder = createTempSubfolder("permissions_folder");
         createTempFile(folder, "permissions.xml", contents);