diff options
12 files changed, 282 insertions, 1 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 53be53c62786..4152a47514e9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -849,6 +849,7 @@ package android { field public static final int keyboardNavigationCluster = 16844096; // 0x1010540 field public static final int keycode = 16842949; // 0x10100c5 field public static final int killAfterRestore = 16843420; // 0x101029c + field public static final int knownCerts = 16844330; // 0x101062a field public static final int label = 16842753; // 0x1010001 field public static final int labelFor = 16843718; // 0x10103c6 field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index dffa0cc315ae..632c1d4fec00 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2632,6 +2632,7 @@ package android.content.pm { field public static final int PROTECTION_FLAG_CONFIGURATOR = 524288; // 0x80000 field public static final int PROTECTION_FLAG_DOCUMENTER = 262144; // 0x40000 field public static final int PROTECTION_FLAG_INCIDENT_REPORT_APPROVER = 1048576; // 0x100000 + field public static final int PROTECTION_FLAG_KNOWN_SIGNER = 134217728; // 0x8000000 field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000 field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000 field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000 diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 0819d1743ad6..bf8d1f6ab07b 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -6151,6 +6151,56 @@ public class PackageParser { } /** + * Returns whether this instance is currently signed, or has ever been signed, with a + * signing certificate from the provided {@link Set} of {@code certDigests}. + * + * <p>The provided {@code certDigests} should contain the SHA-256 digest of the DER encoding + * of each trusted certificate with the digest characters in upper case. If this instance + * has multiple signers then all signers must be in the provided {@code Set}. If this + * instance has a signing lineage then this method will return true if any of the previous + * signers in the lineage match one of the entries in the {@code Set}. + */ + public boolean hasAncestorOrSelfWithDigest(Set<String> certDigests) { + if (this == UNKNOWN || certDigests == null || certDigests.size() == 0) { + return false; + } + // If an app is signed by multiple signers then all of the signers must be in the Set. + if (signatures.length > 1) { + // If the Set has less elements than the number of signatures then immediately + // return false as there's no way to satisfy the requirement of all signatures being + // in the Set. + if (certDigests.size() < signatures.length) { + return false; + } + for (Signature signature : signatures) { + String signatureDigest = PackageUtils.computeSha256Digest( + signature.toByteArray()); + if (!certDigests.contains(signatureDigest)) { + return false; + } + } + return true; + } + + String signatureDigest = PackageUtils.computeSha256Digest(signatures[0].toByteArray()); + if (certDigests.contains(signatureDigest)) { + return true; + } + if (hasPastSigningCertificates()) { + // The last element in the pastSigningCertificates array is the current signer; + // since that was verified above just check all the signers in the lineage. + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + signatureDigest = PackageUtils.computeSha256Digest( + pastSigningCertificates[i].toByteArray()); + if (certDigests.contains(signatureDigest)) { + return true; + } + } + } + return false; + } + + /** * Returns the SigningDetails with a descendant (or same) signer after verifying the * descendant has the same, a superset, or a subset of the lineage of the ancestor. * diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java index 35f02a8d5dd2..a2e533af64a0 100644 --- a/core/java/android/content/pm/PermissionInfo.java +++ b/core/java/android/content/pm/PermissionInfo.java @@ -30,6 +30,7 @@ import android.text.TextUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Set; /** * Information you can retrieve about a particular security permission @@ -278,6 +279,15 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { @SystemApi public static final int PROTECTION_FLAG_ROLE = 0x4000000; + /** + * Additional flag for {@link #protectionLevel}, correspoinding to the {@code knownSigner} value + * of {@link android.R.attr#protectionLevel}. + * + * @hide + */ + @SystemApi + public static final int PROTECTION_FLAG_KNOWN_SIGNER = 0x8000000; + /** @hide */ @IntDef(flag = true, prefix = { "PROTECTION_FLAG_" }, value = { PROTECTION_FLAG_PRIVILEGED, @@ -303,6 +313,7 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { PROTECTION_FLAG_RETAIL_DEMO, PROTECTION_FLAG_RECENTS, PROTECTION_FLAG_ROLE, + PROTECTION_FLAG_KNOWN_SIGNER, }) @Retention(RetentionPolicy.SOURCE) public @interface ProtectionFlags {} @@ -466,6 +477,15 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { */ public @Nullable CharSequence nonLocalizedDescription; + /** + * A {@link Set} of trusted signing certificate digests. If this permission has the {@link + * #PROTECTION_FLAG_KNOWN_SIGNER} flag set the permission will be granted to a requesting app + * if the app is signed by any of these certificates. + * + * @hide + */ + public @Nullable Set<String> knownCerts; + /** @hide */ public static int fixProtectionLevel(int level) { if (level == PROTECTION_SIGNATURE_OR_SYSTEM) { @@ -570,6 +590,9 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { if ((level & PermissionInfo.PROTECTION_FLAG_ROLE) != 0) { protLevel.append("|role"); } + if ((level & PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER) != 0) { + protLevel.append("|knownSigner"); + } return protLevel.toString(); } diff --git a/core/java/android/content/pm/parsing/PackageInfoWithoutStateUtils.java b/core/java/android/content/pm/parsing/PackageInfoWithoutStateUtils.java index fb0d90490567..9a84ded99c67 100644 --- a/core/java/android/content/pm/parsing/PackageInfoWithoutStateUtils.java +++ b/core/java/android/content/pm/parsing/PackageInfoWithoutStateUtils.java @@ -655,6 +655,7 @@ public class PackageInfoWithoutStateUtils { pi.protectionLevel = p.getProtectionLevel(); pi.descriptionRes = p.getDescriptionRes(); pi.flags = p.getFlags(); + pi.knownCerts = p.getKnownCerts(); if ((flags & PackageManager.GET_META_DATA) == 0) { return pi; diff --git a/core/java/android/content/pm/parsing/component/ParsedPermission.java b/core/java/android/content/pm/parsing/component/ParsedPermission.java index f99a0b1dcadb..35bb33c84d56 100644 --- a/core/java/android/content/pm/parsing/component/ParsedPermission.java +++ b/core/java/android/content/pm/parsing/component/ParsedPermission.java @@ -26,6 +26,8 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DataClass; import com.android.internal.util.Parcelling.BuiltIn.ForInternedString; +import java.util.Set; + /** @hide */ public class ParsedPermission extends ParsedComponent { @@ -39,6 +41,8 @@ public class ParsedPermission extends ParsedComponent { boolean tree; @Nullable private ParsedPermissionGroup parsedPermissionGroup; + @Nullable + Set<String> knownCerts; @VisibleForTesting public ParsedPermission() { @@ -81,6 +85,10 @@ public class ParsedPermission extends ParsedComponent { return protectionLevel & ~PermissionInfo.PROTECTION_MASK_BASE; } + public @Nullable Set<String> getKnownCerts() { + return knownCerts; + } + public int calculateFootprint() { int size = getName().length(); if (getNonLocalizedLabel() != null) { diff --git a/core/java/android/content/pm/parsing/component/ParsedPermissionUtils.java b/core/java/android/content/pm/parsing/component/ParsedPermissionUtils.java index 9012b5ce2b1e..a7cecbee8aec 100644 --- a/core/java/android/content/pm/parsing/component/ParsedPermissionUtils.java +++ b/core/java/android/content/pm/parsing/component/ParsedPermissionUtils.java @@ -25,6 +25,7 @@ import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.util.ArraySet; import android.util.Slog; import com.android.internal.R; @@ -32,6 +33,8 @@ import com.android.internal.R; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.util.Locale; +import java.util.Set; /** @hide */ public class ParsedPermissionUtils { @@ -90,6 +93,43 @@ public class ParsedPermissionUtils { permission.flags = sa.getInt( R.styleable.AndroidManifestPermission_permissionFlags, 0); + final int knownCertsResource = sa.getResourceId( + R.styleable.AndroidManifestPermission_knownCerts, 0); + if (knownCertsResource != 0) { + // The knownCerts attribute supports both a string array resource as well as a + // string resource for the case where the permission should only be granted to a + // single known signer. + final String resourceType = res.getResourceTypeName(knownCertsResource); + if (resourceType.equals("array")) { + final String[] knownCerts = res.getStringArray(knownCertsResource); + if (knownCerts != null) { + // Convert the provided digest to upper case for consistent Set membership + // checks when verifying the signing certificate digests of requesting apps. + permission.knownCerts = new ArraySet<>(); + for (String knownCert : knownCerts) { + permission.knownCerts.add(knownCert.toUpperCase(Locale.US)); + } + } + } else { + final String knownCert = res.getString(knownCertsResource); + if (knownCert != null) { + permission.knownCerts = Set.of(knownCert.toUpperCase(Locale.US)); + } + } + if (permission.knownCerts == null) { + Slog.w(TAG, packageName + " defines a knownSigner permission but" + + " the provided knownCerts resource is null"); + } + } else { + // If the knownCerts resource ID is null check if the app specified a string + // value for the attribute representing a single trusted signer. + final String knownCert = sa.getString( + R.styleable.AndroidManifestPermission_knownCerts); + if (knownCert != null) { + permission.knownCerts = Set.of(knownCert.toUpperCase(Locale.US)); + } + } + // For now only platform runtime permissions can be restricted if (!permission.isRuntime() || !"android".equals(permission.getPackageName())) { permission.flags &= ~PermissionInfo.FLAG_HARD_RESTRICTED; diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index b4e580aac959..0ae6a76e2a60 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -311,6 +311,10 @@ <flag name="recents" value="0x2000000" /> <!-- Additional flag from base permission type: this permission is managed by role. --> <flag name="role" value="0x4000000" /> + <!-- Additional flag from base permission type: this permission can also be granted if the + requesting application is signed by, or has in its signing lineage, any of the + certificate digests declared in {@link android.R.attr#knownCerts}. --> + <flag name="knownSigner" value="0x8000000" /> </attr> <!-- Flags indicating more context for a permission group. --> @@ -364,6 +368,15 @@ {@link android.R.styleable#AndroidManifestPermissionGroup permission-group} tag. --> <attr name="permissionGroup" format="string" /> + <!-- A reference to an array resource containing the signing certificate digests to be granted + this permission when using the {@code knownSigner} protection flag. The digest should + be computed over the DER encoding of the trusted certificate using the SHA-256 digest + algorithm. + <p> + If only a single signer is declared this can also be a string resource, or the digest + can be declared inline as the value for this attribute. --> + <attr name="knownCerts" format="reference|string" /> + <!-- Specify the name of a user ID that will be shared between multiple packages. By default, each package gets its own unique user-id. By setting this value on two or more packages, each of these packages @@ -1935,6 +1948,7 @@ <attr name="request" /> <attr name="protectionLevel" /> <attr name="permissionFlags" /> + <attr name="knownCerts" /> </declare-styleable> <!-- The <code>permission-group</code> tag declares a logical grouping of diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 40c5206b86d8..b5ecad6ccbe5 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3064,6 +3064,7 @@ <public name="previewLayout" /> <public name="clipToOutline" /> <public name="edgeEffectType" /> + <public name="knownCerts" /> </public-group> <public-group type="drawable" first-id="0x010800b5"> diff --git a/core/tests/coretests/src/android/content/pm/SigningDetailsTest.java b/core/tests/coretests/src/android/content/pm/SigningDetailsTest.java index bffd1e4a86d6..49b720cfba07 100644 --- a/core/tests/coretests/src/android/content/pm/SigningDetailsTest.java +++ b/core/tests/coretests/src/android/content/pm/SigningDetailsTest.java @@ -29,6 +29,7 @@ import static org.junit.Assert.fail; import android.content.pm.PackageParser.SigningDetails; import android.util.ArraySet; +import android.util.PackageUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -817,6 +818,124 @@ public class SigningDetailsTest { assertFalse(secondDetails.hasCommonSignerWithCapability(firstDetails, PERMISSION)); } + @Test + public void hasAncestorOrSelfWithDigest_nullSet_returnsFalse() throws Exception { + // The hasAncestorOrSelfWithDigest method is intended to verify whether the SigningDetails + // is currently signed, or has previously been signed, by any of the certificate digests + // in the provided Set. This test verifies if a null Set is provided then false is returned. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(null)); + } + + @Test + public void hasAncestorOrSelfWithDigest_unknownDetails_returnsFalse() throws Exception { + // If hasAncestorOrSelfWithDigest is invoked against an UNKNOWN + // instance of the SigningDetails then false is returned. + SigningDetails details = SigningDetails.UNKNOWN; + Set<String> digests = createDigestSet(FIRST_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_singleSignerInSet_returnsTrue() throws Exception { + // If the single signer of an app is in the provided digest Set then + // the method should return true. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE); + Set<String> digests = createDigestSet(FIRST_SIGNATURE, SECOND_SIGNATURE); + + assertTrue(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_singleSignerNotInSet_returnsFalse() throws Exception { + // If the single signer of an app is not in the provided digest Set then + // the method should return false. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE); + Set<String> digests = createDigestSet(SECOND_SIGNATURE, THIRD_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_multipleSignersInSet_returnsTrue() throws Exception { + // If an app is signed by multiple signers and all of the signers are in + // the digest Set then the method should return true. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE, SECOND_SIGNATURE); + Set<String> digests = createDigestSet(FIRST_SIGNATURE, SECOND_SIGNATURE, THIRD_SIGNATURE); + + assertTrue(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_multipleSignersNotInSet_returnsFalse() + throws Exception { + // If an app is signed by multiple signers then all signers must be in the digest Set; if + // only a subset of the signers are in the Set then the method should return false. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE, SECOND_SIGNATURE); + Set<String> digests = createDigestSet(FIRST_SIGNATURE, THIRD_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_multipleSignersOneInSet_returnsFalse() + throws Exception { + // If an app is signed by multiple signers and the Set size is smaller than the number of + // signers then the method should immediately return false since there's no way for the + // requirement of all signers in the Set to be met. + SigningDetails details = createSigningDetails(FIRST_SIGNATURE, SECOND_SIGNATURE); + Set<String> digests = createDigestSet(FIRST_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_lineageSignerInSet_returnsTrue() throws Exception { + // If an app has a rotated signing key and a previous key in the lineage is in the digest + // Set then this method should return true. + SigningDetails details = createSigningDetailsWithLineage(FIRST_SIGNATURE, SECOND_SIGNATURE); + Set<String> digests = createDigestSet(FIRST_SIGNATURE, THIRD_SIGNATURE); + + assertTrue(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_lineageSignerNotInSet_returnsFalse() throws Exception { + // If an app has a rotated signing key, but neither the current key nor any of the signers + // in the lineage are in the digest set then the method should return false. + SigningDetails details = createSigningDetailsWithLineage(FIRST_SIGNATURE, SECOND_SIGNATURE); + Set<String> digests = createDigestSet(THIRD_SIGNATURE, FOURTH_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_lastSignerInLineageInSet_returnsTrue() + throws Exception { + // If an app has multiple signers in the lineage only one of those signers must be in the + // Set for this method to return true. This test verifies if the last signer in the lineage + // is in the set then the method returns true. + SigningDetails details = createSigningDetailsWithLineage(FIRST_SIGNATURE, SECOND_SIGNATURE, + THIRD_SIGNATURE); + Set<String> digests = createDigestSet(SECOND_SIGNATURE); + + assertTrue(details.hasAncestorOrSelfWithDigest(digests)); + } + + @Test + public void hasAncestorOrSelfWithDigest_nullLineageSingleSIgner_returnsFalse() + throws Exception { + // Under some instances an app with only a single signer can have a null lineage; this + // test verifies that null lineage does not result in a NullPointerException and instead the + // method returns false if the single signer is not in the Set. + SigningDetails details = createSigningDetails(true, FIRST_SIGNATURE); + Set<String> digests = createDigestSet(SECOND_SIGNATURE, THIRD_SIGNATURE); + + assertFalse(details.hasAncestorOrSelfWithDigest(digests)); + } + private SigningDetails createSigningDetailsWithLineage(String... signers) throws Exception { int[] capabilities = new int[signers.length]; for (int i = 0; i < capabilities.length; i++) { @@ -853,12 +972,21 @@ public class SigningDetailsTest { // If there are multiple signers then the pastSigningCertificates should be set to null, but // if there is only a single signer both the current signer and the past signers should be // set to that one signer. - if (signers.length > 1) { + if (signers.length > 1 || useNullPastSigners) { return new SigningDetails(currentSignatures, SIGNING_BLOCK_V3, null); } return new SigningDetails(currentSignatures, SIGNING_BLOCK_V3, currentSignatures); } + private Set<String> createDigestSet(String... signers) { + Set<String> digests = new ArraySet<>(); + for (String signer : signers) { + String digest = PackageUtils.computeSha256Digest(new Signature(signer).toByteArray()); + digests.add(digest); + } + return digests; + } + private void assertSigningDetailsContainsLineage(SigningDetails details, String... pastSigners) { // This method should only be invoked for results that contain a single signer. diff --git a/services/core/java/com/android/server/pm/permission/Permission.java b/services/core/java/com/android/server/pm/permission/Permission.java index 30c334d22b6a..32bee5809b11 100644 --- a/services/core/java/com/android/server/pm/permission/Permission.java +++ b/services/core/java/com/android/server/pm/permission/Permission.java @@ -38,6 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collection; import java.util.Objects; +import java.util.Set; /** * Permission definition. @@ -345,6 +346,14 @@ public final class Permission { return (mPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_ROLE) != 0; } + public boolean isKnownSigner() { + return (mPermissionInfo.protectionLevel & PermissionInfo.PROTECTION_FLAG_KNOWN_SIGNER) != 0; + } + + public Set<String> getKnownCerts() { + return mPermissionInfo.knownCerts; + } + public void transfer(@NonNull String oldPackageName, @NonNull String newPackageName) { if (!oldPackageName.equals(mPermissionInfo.packageName)) { return; diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index aff871118a34..0669581ad090 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -3438,6 +3438,11 @@ public class PermissionManagerService extends IPermissionManager.Stub { // Any pre-installed system app is allowed to get this permission. allowed = true; } + if (!allowed && bp.isKnownSigner()) { + // If the permission is to be granted to a known signer then check if any of this + // app's signing certificates are in the trusted certificate digest Set. + allowed = pkg.getSigningDetails().hasAncestorOrSelfWithDigest(bp.getKnownCerts()); + } // Deferred to be checked under permission data lock inside restorePermissionState(). //if (!allowed && bp.isDevelopment()) { // // For development permissions, a development permission |