Add signature verifier to be used by AppAuthenticator
This commit adds a signature verifier that the AppAuthenticator can use
in a future commit to verify an app's signing identity against that
declared in the AppAuthenticator's XML config file.
Bug: 171453012
Test: AppAuthenticatorUtilsTest and AppSignatureVerifierTest
Change-Id: Ia1d9079f0eaa1ef2aad1e30733a85fe3e14005f7
diff --git a/security/security-app-authenticator/build.gradle b/security/security-app-authenticator/build.gradle
index ddfa7f9..186da78 100644
--- a/security/security-app-authenticator/build.gradle
+++ b/security/security-app-authenticator/build.gradle
@@ -28,15 +28,27 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
-
+ implementation(AUTO_VALUE_ANNOTATIONS)
+ annotationProcessor(AUTO_VALUE)
implementation("androidx.collection:collection:1.1.0")
+ testImplementation("junit:junit:4.13")
+ testImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ testImplementation(ANDROIDX_TEST_CORE)
+ testImplementation(ANDROIDX_TEST_RUNNER)
+ testImplementation(ANDROIDX_TEST_RULES)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(ROBOLECTRIC)
+
androidTestImplementation("junit:junit:4.13")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(ANDROIDX_TEST_RULES)
- androidTestImplementation(MOCKITO_CORE)
+}
+
+android {
+ testOptions.unitTests.includeAndroidResources = true
}
androidx {
diff --git a/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java b/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java
new file mode 100644
index 0000000..136f7a7
--- /dev/null
+++ b/security/security-app-authenticator/src/androidTest/java/androidx/security/app/authenticator/AppAuthenticatorUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 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 androidx.security.app.authenticator;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.security.MessageDigest;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AppAuthenticatorUtilsTest {
+ private static final byte[] TEST_DATA = new byte[]{0x01, 0x23, 0x7f, (byte) 0xab};
+
+ @Test
+ public void getApiLevel_returnsPlatformLevel() throws Exception {
+ // The default behavior of getApiLevel should return the value of Build.VERSION.SDK_INT on
+ // the device.
+ assertEquals(AppAuthenticatorUtils.getApiLevel(), Build.VERSION.SDK_INT);
+ }
+
+ @Test
+ public void toHexString_returnsExpectedString() throws Exception {
+ // toHexString accepts a byte array and should return a String hex representation of the
+ // array.
+ assertEquals(AppAuthenticatorUtils.toHexString(TEST_DATA), "01237fab");
+ }
+
+ @RunWith(Parameterized.class)
+ public static class AppAuthenticatorUtilsParameterizedTest {
+ private String mDigestAlgorithm;
+
+ // Verify the returned digest for each of the supported digest algorithms.
+ @Parameterized.Parameters
+ public static Object[] getDigestAlgorithms() {
+ return new Object[]{
+ "SHA-256",
+ "SHA-384",
+ "SHA-512",
+ };
+ }
+
+ public AppAuthenticatorUtilsParameterizedTest(final String digestAlgorithm) {
+ mDigestAlgorithm = digestAlgorithm;
+ }
+
+ @Test
+ public void computeDigest_returnsExpectedDigest() throws Exception {
+ assertEquals(AppAuthenticatorUtils.computeDigest(mDigestAlgorithm, TEST_DATA),
+ getExpectedDigest(mDigestAlgorithm, TEST_DATA));
+ }
+
+ private String getExpectedDigest(String digestAlgorithm, byte[] data) throws Exception {
+ MessageDigest messageDigest = MessageDigest.getInstance(digestAlgorithm);
+ return AppAuthenticatorUtils.toHexString(messageDigest.digest(data));
+ }
+ }
+}
diff --git a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticator.java b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticator.java
index 14c8c67..5d5536a 100644
--- a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticator.java
+++ b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticator.java
@@ -65,7 +65,7 @@
* The tag to declare all packages signed with the enclosed signing identities are to be
* granted to the enclosing permission.
*/
- private static final String ALL_PACKAGES_TAG = "all-packages";
+ static final String ALL_PACKAGES_TAG = "all-packages";
/**
* The tag to declare a known signing certificate digest for the enclosing package.
*/
@@ -82,7 +82,7 @@
* The default digest algorithm used for all certificate digests if one is not specified in
* the root element.
*/
- private static final String DEFAULT_DIGEST_ALGORITHM = "SHA-256";
+ static final String DEFAULT_DIGEST_ALGORITHM = "SHA-256";
/**
* The set of digest algorithms supported by the AppAuthenticator; insecure algorithms and
* those that do not support all platform levels have been removed.
diff --git a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java
new file mode 100644
index 0000000..c50e0f9
--- /dev/null
+++ b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppAuthenticatorUtils.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2020 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 androidx.security.app.authenticator;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Provides utility methods that facilitate app signing identity verification.
+ */
+class AppAuthenticatorUtils {
+ private AppAuthenticatorUtils() {
+ }
+
+ private static final char[] HEX_CHARACTERS = "0123456789abcdef".toCharArray();
+
+ /**
+ * Returns the API level as reported by {@code Build.VERSION.SDK_INT}.
+ */
+ static int getApiLevel() {
+ return Build.VERSION.SDK_INT;
+ }
+
+ /**
+ * Computes the digest of the provided {@code data} using the specified {@code
+ * digestAlgorithm}, returning a {@code String} representing the hex encoding of the digest.
+ *
+ * <p>The specified {@code digestAlgorithm} must be one supported from API level 1; use of
+ * MD5 and SHA-1 are strongly discouraged.
+ */
+ static String computeDigest(@NonNull String digestAlgorithm, @NonNull byte[] data) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance(digestAlgorithm);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen; the AppAuthenticator only accepts digest algorithms that are
+ // available from API level 1.
+ throw new AppAuthenticatorUtilsException(digestAlgorithm + " not supported on this "
+ + "device", e);
+ }
+ return toHexString(messageDigest.digest(data));
+ }
+
+ /**
+ * Returns a {@code String} representing the hex encoding of the provided {@code data}.
+ */
+ static String toHexString(@NonNull byte[] data) {
+ char[] result = new char[data.length * 2];
+ for (int i = 0; i < data.length; i++) {
+ int resultIndex = i * 2;
+ result[resultIndex] = HEX_CHARACTERS[(data[i] >> 4) & 0x0f];
+ result[resultIndex + 1] = HEX_CHARACTERS[data[i] & 0x0f];
+ }
+ return new String(result);
+ }
+
+ /**
+ * This {@code RuntimeException} is thrown when an unexpected error is encountered while
+ * performing a utility operation.
+ */
+ private static class AppAuthenticatorUtilsException extends RuntimeException {
+ AppAuthenticatorUtilsException(@NonNull String message, Throwable reason) {
+ super(message, reason);
+ }
+ }
+}
diff --git a/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java
new file mode 100644
index 0000000..ac8a291
--- /dev/null
+++ b/security/security-app-authenticator/src/main/java/androidx/security/app/authenticator/AppSignatureVerifier.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright 2020 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 androidx.security.app.authenticator;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.collection.ArrayMap;
+import androidx.collection.LruCache;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides methods to verify the signatures of apps based on the expected signing identities
+ * provided to the {@link Builder#setPermissionAllowMap(Map)} and {@link
+ * Builder#setExpectedIdentities(Map)} builder methods.
+ */
+class AppSignatureVerifier {
+ private static final String TAG = AppSignatureVerifier.class.getSimpleName();
+ private static final String EXPECTED_IDENTITY_QUERY = "expected-identity";
+ private static final int DEFAULT_CACHE_SIZE = 16;
+
+ private final PackageManager mPackageManager;
+ private final String mDigestAlgorithm;
+ private final Cache mCache;
+ /**
+ * A mapping from permission to allowed packages / signing identities.
+ */
+ private final Map<String, Map<String, Set<String>>> mPermissionAllowMap;
+ /**
+ * A mapping from package name to expected signing identities.
+ */
+ private final Map<String, Set<String>> mExpectedIdentities;
+
+ /**
+ * Private constructor; instances should be instantiated through a {@link Builder} obtained
+ * with {@link #builder}.
+ */
+ AppSignatureVerifier(Context context,
+ Map<String, Map<String, Set<String>>> permissionAllowMap,
+ Map<String, Set<String>> expectedIdentities,
+ String digestAlgorithm,
+ Cache cache) {
+ mPackageManager = context.getPackageManager();
+ mPermissionAllowMap = permissionAllowMap;
+ mExpectedIdentities = expectedIdentities;
+ mDigestAlgorithm = digestAlgorithm;
+ mCache = cache;
+ }
+
+ /**
+ * Returns a new {@link Builder} that can be used to instantiate a new {@code
+ * AppSignatureVerifier}.
+ */
+ static Builder builder(Context context) {
+ return new Builder(context);
+ }
+
+ /**
+ * Provides methods to configure a new {@code AppSignatureVerifier} instance.
+ */
+ static class Builder {
+ private final Context mContext;
+ private String mDigestAlgorithm;
+ private Cache mCache;
+ private Map<String, Map<String, Set<String>>> mPermissionAllowMap;
+ private Map<String, Set<String>> mExpectedIdentities;
+
+ /**
+ * Constructor accepting the {@code context} used to instantiate a new {@code
+ * AppSignatureVerifier}.
+ */
+ Builder(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Sets the {@code digestAlgorithm} to be used by the {@code AppSignatureVerifier}; all
+ * signing identities provided to {@link #setPermissionAllowMap} and
+ * {@link #setExpectedIdentities} must be computed using this same {@code digestAlgorithm}.
+ */
+ Builder setDigestAlgorithm(String digestAlgorithm) {
+ mDigestAlgorithm = digestAlgorithm;
+ return this;
+ }
+
+ /**
+ * Sets the {@code cache} to be used by the {@code AppSignatureVerifier}.
+ */
+ Builder setCache(Cache cache) {
+ mCache = cache;
+ return this;
+ }
+
+ /**
+ * Sets the {@code permissionAllowMap} to be used by the {@code AppSignatureVerifier}.
+ *
+ * This {@code Map} should contain a mapping from permission names to a mapping of package
+ * names to expected signing identities; each permission can also contain a mapping to
+ * the {@link AppAuthenticator#ALL_PACKAGES_TAG} which allow signing identities to be
+ * specified without knowing the exact packages that will be signed by them.
+ */
+ Builder setPermissionAllowMap(Map<String, Map<String, Set<String>>> permissionAllowMap) {
+ mPermissionAllowMap = permissionAllowMap;
+ return this;
+ }
+
+ /**
+ * Sets the {@code expectedIdentities} to be used by the {@code AppSignatureVerifier}.
+ *
+ * This {@code Map} should contain a mapping from package name to the expected signing
+ * certificate digest(s).
+ */
+ Builder setExpectedIdentities(Map<String, Set<String>> expectedIdentities) {
+ mExpectedIdentities = expectedIdentities;
+ return this;
+ }
+
+ /**
+ * Builds a new {@code AppSignatureVerifier} instance using the provided configuration.
+ */
+ AppSignatureVerifier build() {
+ if (mPermissionAllowMap == null) {
+ mPermissionAllowMap = new ArrayMap<>();
+ }
+ if (mExpectedIdentities == null) {
+ mExpectedIdentities = new ArrayMap<>();
+ }
+ if (mDigestAlgorithm == null) {
+ mDigestAlgorithm = AppAuthenticator.DEFAULT_DIGEST_ALGORITHM;
+ }
+ if (mCache == null) {
+ mCache = new Cache(DEFAULT_CACHE_SIZE);
+ }
+ return new AppSignatureVerifier(mContext, mPermissionAllowMap, mExpectedIdentities,
+ mDigestAlgorithm, mCache);
+ }
+ }
+
+ /**
+ * Verifies the signing identity of the provided {@code packageName} for the specified {@code
+ * permission}, returning {@code true} if the signing identity matches that declared under
+ * the {@code permission} for the {@code package} as specified to {@link
+ * Builder#setPermissionAllowMap}.
+ */
+ boolean verifySigningIdentity(String packageName, String permission) {
+ // If there are no declared expected certificate digests for the specified package or
+ // all-packages under the permission then return immediately.
+ Map<String, Set<String>> allowedCertDigests = mPermissionAllowMap.get(permission);
+ if (allowedCertDigests == null) {
+ Log.d(TAG, "No expected signing identities declared for permission " + permission);
+ return false;
+ }
+ Set<String> packageCertDigests = allowedCertDigests.get(packageName);
+ Set<String> allPackagesCertDigests =
+ allowedCertDigests.get(AppAuthenticator.ALL_PACKAGES_TAG);
+ if (packageCertDigests == null && allPackagesCertDigests == null) {
+ return false;
+ }
+ return verifySigningIdentityForQuery(packageName, permission, packageCertDigests,
+ allPackagesCertDigests);
+ }
+
+ /**
+ * Verifies the signing identity of the provided {@code packageName} against the expected
+ * signing identity set through {@link Builder#setExpectedIdentities(Map)}.
+ */
+ boolean verifyExpectedIdentity(String packageName) {
+ Set<String> packageCertDigests = mExpectedIdentities.get(packageName);
+ if (packageCertDigests == null) {
+ return false;
+ }
+ return verifySigningIdentityForQuery(packageName, EXPECTED_IDENTITY_QUERY,
+ packageCertDigests, null);
+ }
+
+ /**
+ * Verifies the signing identity of the provided {@code packageName} based on the provided
+ * {@code query} against the expected {@code packageCertDigests}, and where applicable the
+ * {@code allPackageCertDigests}.
+ *
+ * The {@code query} can either be a permission or {@code EXPECTED_IDENTITY_QUERY} when
+ * verifying the identity of another app before establishing communication.
+ */
+ private boolean verifySigningIdentityForQuery(String packageName, String query,
+ Set<String> packageCertDigests, Set<String> allPackagesCertDigests) {
+ AppSigningInfo appSigningInfo;
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ appSigningInfo = Api28Impl.getAppSigningInfo(mPackageManager, packageName);
+ } else {
+ appSigningInfo = DefaultImpl.getAppSigningInfo(mPackageManager, packageName);
+ }
+ } catch (AppSignatureVerifierException e) {
+ Log.e(TAG, "Caught an exception obtaining signing info for package " + packageName, e);
+ return false;
+ }
+ // If a previous verification result exists for this package and query, and the package
+ // has not yet been updated, then use the result from the previous verification. An app's
+ // signing identity can only be changed on an update which should result in an update of
+ // the last update time.
+ CacheEntry cacheEntry = mCache.get(packageName, query);
+ if (cacheEntry != null
+ && cacheEntry.getLastUpdateTime() == appSigningInfo.getLastUpdateTime()) {
+ return cacheEntry.getVerificationResult();
+ }
+ boolean verificationResult;
+ // API levels >= 28 support obtaining the signing lineage of a package after a key
+ // rotation; if the signing lineage is available then verify each entry in the lineage
+ // against the expected signing identities.
+ if (appSigningInfo.getSigningLineage() != null) {
+ verificationResult = verifySigningLineage(appSigningInfo.getSigningLineage(),
+ packageCertDigests, allPackagesCertDigests);
+ } else {
+ verificationResult = verifyCurrentSigners(appSigningInfo.getCurrentSignatures(),
+ packageCertDigests, allPackagesCertDigests);
+ }
+ mCache.put(packageName, query, CacheEntry.create(verificationResult,
+ appSigningInfo.getLastUpdateTime()));
+ return verificationResult;
+ }
+
+ /**
+ * Verifies the provided {@code signatures} signing lineage against the expected signing
+ * identities in the {@code packageCertDigests} and {@code allPackagesCertDigests}.
+ *
+ * <p>A signing identity is successfully verified if any of the signatures in the lineage
+ * matches any of the expected signing certificate digest declarations in the provided {@code
+ * Map}s.
+ */
+ private boolean verifySigningLineage(List<Signature> signatures, Set<String> packageCertDigests,
+ Set<String> allPackagesCertDigests) {
+ for (Signature signature : signatures) {
+ String signatureDigest = AppAuthenticatorUtils.computeDigest(mDigestAlgorithm,
+ signature.toByteArray());
+ if (packageCertDigests != null && packageCertDigests.contains(signatureDigest)) {
+ return true;
+ }
+ if (allPackagesCertDigests != null
+ && allPackagesCertDigests.contains(signatureDigest)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Verifies the provided current {@code signatures} against the expected signing identities
+ * in the {@code packageCertDigests} and {@code allPackagesCertDigests}.
+ *
+ * <p>A signing identity is successfully verified if all of the current signers are
+ * declared in either of the expected signing certificate digest {@code Map}s.
+ */
+ boolean verifyCurrentSigners(List<Signature> signatures, Set<String> packageCertDigests,
+ Set<String> allPackagesCertDigests) {
+ List<String> signatureDigests = new ArrayList<>(signatures.size());
+ for (Signature signature : signatures) {
+ signatureDigests.add(AppAuthenticatorUtils.computeDigest(mDigestAlgorithm,
+ signature.toByteArray()));
+ }
+ if (packageCertDigests != null && packageCertDigests.containsAll(signatureDigests)) {
+ return true;
+ }
+ return allPackagesCertDigests != null
+ && allPackagesCertDigests.containsAll(signatureDigests);
+ }
+
+ /**
+ * Provides a method to support package signature queries for API levels >= 28. Starting at
+ * API level 28 the platform added support for app signing key rotation, so apps signed by a
+ * single signer can include the entire signing lineage from
+ * {@link AppSigningInfo#getSigningLineage()}.
+ */
+ @RequiresApi(28)
+ private static class Api28Impl {
+ private Api28Impl() {
+ }
+
+ /**
+ * Returns the {@link AppSigningInfo} for the specified {@code packageName} using the
+ * provided {@code packageManager}, including full signing lineage for apps signed by a
+ * single signer.
+ *
+ * @throws AppSignatureVerifierException if the specified package is not found, or if the
+ * {@code SigningInfo} is not returned for the package.
+ */
+ static AppSigningInfo getAppSigningInfo(PackageManager packageManager,
+ String packageName) throws AppSignatureVerifierException {
+ PackageInfo packageInfo;
+ try {
+ packageInfo = packageManager.getPackageInfo(packageName,
+ PackageManager.GET_SIGNING_CERTIFICATES);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AppSignatureVerifierException("Package " + packageName + " not found", e);
+ }
+ if (packageInfo.signingInfo == null) {
+ throw new AppSignatureVerifierException(
+ "No SigningInfo returned for package " + packageName);
+ }
+ return AppSigningInfo.create(packageName,
+ packageInfo.signingInfo.getApkContentsSigners(),
+ packageInfo.signingInfo.getSigningCertificateHistory(),
+ packageInfo.lastUpdateTime);
+ }
+ }
+
+ /**
+ * Provides a method to support package signature queries for API levels < 28. Prior to API
+ * level 28 the platform only supported returning an app's original signature(s), and an app
+ * signed with a rotated signing key will still return the original signing key when queried
+ * with the {@link PackageManager#GET_SIGNATURES} flag.
+ */
+ private static class DefaultImpl {
+ private DefaultImpl() {
+ }
+
+ /**
+ * Returns the {@link AppSigningInfo} for the specified {@code packageName} using the
+ * provided {@code packageManager}, containing only the original / current signer for the
+ * package.
+ *
+ * @throws AppSignatureVerifierException if the specified package is not found, or if the
+ * {@code signatures} are not returned for the package.
+ */
+ // Suppress the deprecation and GetSignatures warnings for the GET_SIGNATURES flag and the
+ // use of PackageInfo.Signatures since this method is intended for API levels < 28 which
+ // only support these.
+ @SuppressWarnings("deprecation")
+ @SuppressLint("PackageManagerGetSignatures")
+ static AppSigningInfo getAppSigningInfo(PackageManager packageManager,
+ String packageName) throws AppSignatureVerifierException {
+ PackageInfo packageInfo;
+ try {
+ packageInfo = packageManager.getPackageInfo(packageName,
+ PackageManager.GET_SIGNATURES);
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new AppSignatureVerifierException("Package " + packageName + " not found", e);
+ }
+ if (packageInfo.signatures == null) {
+ throw new AppSignatureVerifierException(
+ "No signatures returned for package " + packageName);
+ }
+ // When using the GET_SIGNATURES flag to obtain the app's signing info only the
+ // current signers are returned, so set the lineage to null in the AppSigningInfo.
+ return AppSigningInfo.create(packageName, packageInfo.signatures, null,
+ packageInfo.lastUpdateTime);
+ }
+ }
+
+ /**
+ * Cache containing previous signing identity version results stored by package name and
+ * query where the query is either the permission name or the {@code EXPECTED_IDENTITY_QUERY}.
+ */
+ static class Cache extends LruCache<String, CacheEntry> {
+ /**
+ * Constructs a new {@code Cache} with the provided {@code maxSize}.
+ */
+ Cache(int maxSize) {
+ super(maxSize);
+ }
+
+ /**
+ * Returns the {@link CacheEntry} in the cache for the specified {@code packageName} and
+ * {@code query}.
+ */
+ CacheEntry get(String packageName, String query) {
+ return get(packageName + query);
+ }
+
+ /**
+ * Puts the provided {@code cacheEntry} in the cache for the specified {@code packageName}
+ * and {@code query}.
+ */
+ void put(String packageName, String query, CacheEntry cacheEntry) {
+ put(packageName + query, cacheEntry);
+ }
+ }
+
+ /**
+ * Value class containing the verification result and the last update time for an entry in
+ * the {@link Cache}.
+ */
+ @AutoValue
+ abstract static class CacheEntry {
+ abstract boolean getVerificationResult();
+ abstract long getLastUpdateTime();
+
+ /**
+ * Creates a new instance with the provided {@code verificationResult} and {@code
+ * lastUpdateTime}.
+ */
+ static CacheEntry create(boolean verificationResult, long lastUpdateTime) {
+ return new AutoValue_AppSignatureVerifier_CacheEntry(verificationResult,
+ lastUpdateTime);
+ }
+ }
+
+ /**
+ * Value class containing generic signing info for a package.
+ */
+ // Suppressing the AutoValue immutable field warning as this class is only used internally
+ // and is not worth bringing in the dependency for an ImmutableList.
+ @SuppressWarnings("AutoValueImmutableFields")
+ @AutoValue
+ abstract static class AppSigningInfo {
+ abstract String getPackageName();
+ abstract List<Signature> getCurrentSignatures();
+ @Nullable
+ abstract List<Signature> getSigningLineage();
+ abstract long getLastUpdateTime();
+
+ /**
+ * Creates a new instance with the provided {@code packageName}, {@code currentSignatures},
+ * {@code signingLineage}, and {@code lastUpdateTime}.
+ *
+ * <p>Note, the {@code signingLineage} can be null as this was not available prior to API
+ * level 28, but a non-null value must be specified for the {@code currentSignatures}.
+ */
+ static AppSigningInfo create(@NonNull String packageName,
+ @NonNull Signature[] currentSignatures, Signature[] signingLineage,
+ long lastUpdateTime) {
+ return new AutoValue_AppSignatureVerifier_AppSigningInfo(packageName,
+ Arrays.asList(currentSignatures),
+ signingLineage != null ? Arrays.asList(signingLineage) : null,
+ lastUpdateTime);
+ }
+ }
+
+ /**
+ * This {@code Exception} is thrown when an unexpected error is encountered when querying for
+ * or verifying package signing identities.
+ */
+ private static class AppSignatureVerifierException extends Exception {
+ AppSignatureVerifierException(@NonNull String message) {
+ super(message);
+ }
+
+ AppSignatureVerifierException(@NonNull String message, @NonNull Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
new file mode 100644
index 0000000..1854c03
--- /dev/null
+++ b/security/security-app-authenticator/src/test/java/androidx/security/app/authenticator/AppSignatureVerifierTest.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright 2020 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 androidx.security.app.authenticator;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
+import android.os.Build;
+
+import androidx.collection.ArrayMap;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@DoNotInstrument
+// API Level 28 introduced signing key rotation, so run the tests with and without rotation support.
+@Config(minSdk = 27, maxSdk = 28)
+// This test class supports setting the results for the GET_SIGNATURES flag which is deprecated in
+// API levels >= 28 but is the only option available to obtain a package's signing info in
+// API levels < 28.
+@SuppressWarnings("deprecation")
+public class AppSignatureVerifierTest {
+ private static final String TEST_PACKAGE_NAME = "com.android.testapp";
+ private static final String TEST_PERMISSION_NAME = "com.android.testapp.TEST_PERMISSION";
+ private static final long LAST_UPDATE_TIME = 1234;
+
+ private static final String DIGEST_ALGORITHM = "SHA-256";
+ private static final String SIGNATURE1 = "01234567890a";
+ private static final String SIGNATURE2 = "abcdef012345";
+ private static final String SIGNATURE3 = "543201fedcba";
+ private static final String SIGNATURE1_DIGEST =
+ "1bb82badeb591f3a7ba3f82e938d9364e35b2fc649da7d4aea29313cb5214e0c";
+ private static final String SIGNATURE2_DIGEST =
+ "91a5dc2d6f379fcabb87d7d131a1ebccf789cfc3a4716f622adc0086b8d9742b";
+ private static final String SIGNATURE3_DIGEST =
+ "1b39648cad5202eeec496cc224d138c7744bb8675a734e29cae723de3fccad3d";
+
+ @Mock
+ private Context mMockContext;
+ @Mock
+ private PackageManager mMockPackageManager;
+
+ private AppSignatureVerifierTestBuilder mBuilder;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ mBuilder = new AppSignatureVerifierTestBuilder(mMockContext);
+ }
+
+ @Test
+ public void verifySigningIdentity_oneSignerDigestInPackageCertSet() throws Exception {
+ // When a package only has a single signer and that signer's digest is in the package
+ // cert Set then the verifier should return true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_oneSignerDigestInAllPackagesCertSet() throws Exception {
+ // When a package only has a single signer and that signer's digest is in the all-packages
+ // cert Set then the verifier should return true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setAllPackagesCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST),
+ TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_oneSignerDigestNotInPackageCertSet() throws Exception {
+ // When a package only has a single signer and that signer's digest is not in the package
+ // cert Set then the verifier should return false.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_oneSignerDigestNotInAllPackagesCertSet() throws Exception {
+ // When a package only has a single signer and that signer's digest is not in the
+ // all-packages cert Set then the verifier should return false.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setAllPackagesCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST),
+ TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_multipleSignersDigestsInPackageCertSet() throws Exception {
+ // When a package is signed with multiple signers all of the digests of the signers must
+ // be in one of the Sets; this test verifies when the package cert Set contains all of the
+ // signing digests then the verifier returns true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST, SIGNATURE1_DIGEST),
+ TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_multipleSignersDigestsInAllPackagesCertSet()
+ throws Exception {
+ // When a package is signed with multiple signers all of the digests of the signers must
+ // be in one of the Sets; this test verifies when the all-packages cert Set contains all of
+ // the signing digests then the verifier returns true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setAllPackagesCertDigestsForPermission(
+ Set.of(SIGNATURE2_DIGEST, SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_multipleSignersNotInEitherSet() throws Exception {
+ // When a package is signed with multiple signers and neither of the sets contains all of
+ // the signers the Verifier should return false.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .setAllPackagesCertDigestsForPermission(
+ Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_unknownPackageName() throws Exception {
+ // When a package name is specified that is not on the device the #getPackageInfo call
+ // should result in a NameNotFoundException; when this is caught the verifier should
+ // return false.
+ final String unknownPackageName = "com.android.unknown";
+ when(mMockPackageManager.getPackageInfo(unknownPackageName,
+ PackageManager.GET_SIGNATURES)).thenThrow(
+ PackageManager.NameNotFoundException.class);
+ // API Level 28 introduced the GET_SIGNING_CERTIFICATES flag to obtain the full signing
+ // lineage of a package.
+ if (Build.VERSION.SDK_INT >= 28) {
+ when(mMockPackageManager.getPackageInfo(unknownPackageName,
+ PackageManager.GET_SIGNING_CERTIFICATES)).thenThrow(
+ PackageManager.NameNotFoundException.class);
+ }
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(unknownPackageName, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_emptyDigestSets() throws Exception {
+ // When both the package and all-packages cert digest Sets are empty then
+ // verifySigningIdentity should return false immediately without querying the
+ // PackageManager for the signatures.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1).build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_rotatedKeyOriginalDigestInPackageCertSet() throws Exception {
+ // When a package is signed with a rotated signing key the signing lineage should be
+ // included in the SigningInfo result (for API >= 28), or the original signer in the
+ // signatures array. When the first signer is in the package cert digest Set then the
+ // verifier should return true for all API levels.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_rotatedKeyOriginalDigestInAllPackagesCertSet()
+ throws Exception {
+ // When a package is signed with a rotated signing key the signing lineage should be
+ // included in the SigningInfo result (for API >= 28), or the original signer in the
+ // signatures array. When the first signer is in the all-packages cert digest Set then the
+ // verifier should return true for all API levels.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setAllPackagesCertDigestsForPermission(
+ Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ @Config(maxSdk = 27)
+ public void verifySigningIdentity_rotatedKeyDigestInPackageCertSetApi27() throws Exception {
+ // When a package is signed with a rotated signing key and only this newly rotated
+ // signing key is in the digest Sets then API levels < 28 will return false because only
+ // the original signer of the package will be returned from the #getPackageInfo API. This
+ // test is intended to stress if an app is targeting API levels < 28 and interacting with
+ // packages with rotated keys the original signing key must also be included in the
+ // allow-list.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ @Config(maxSdk = 27)
+ public void verifySigningIdentity_rotatedKeyDigestInAllPackagesCertSetApi27() throws Exception {
+ // Similar to above if only the rotated key is in the all-packages cert set then for API
+ // levels < 28 the verifier will return false since it will only have the original
+ // signing certificate to compare against.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setAllPackagesCertDigestsForPermission(
+ Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void verifySigningIdentity_rotatedKeyDigestInPackageCertSetApi28() throws Exception {
+ // When a package is signed with a rotated signing key and only this newly rotated
+ // signing key is in the package cert Set then API Level 28+ devices should return true
+ // for the rotated key, but since earlier API Levels only see the original signing key
+ // they would return false.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void verifySigningIdentity_rotatedKeyDigestInAllPackagesCertSetApi28() throws Exception {
+ // When a package is signed with a rotated signing key and only this newly rotated
+ // signing key is in the all-packages cert Set then API Level 28+ devices should return
+ // true for the rotated key, but since earlier API Levels only see the original signing key
+ // they would return false.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setAllPackagesCertDigestsForPermission(
+ Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_singleSignerInSet() throws Exception {
+ // Since an app typically verifies the signing identity of an app before establishing
+ // communication with that app they are not tied to a permission and all-package
+ // declarations are not allowed. This test verifies if an app is signed with a single
+ // signing key and that signing certificate's digest is in the Set then the verifier
+ // returns true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setExpectedIdentities(Set.of(SIGNATURE2_DIGEST, SIGNATURE1_DIGEST))
+ .build();
+
+ assertTrue(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_singleSignerNotInSet() throws Exception {
+ // When a package is signed by a single signer and that signer is not in the Set then the
+ // verifier should return false.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setExpectedIdentities(Set.of(SIGNATURE2_DIGEST))
+ .build();
+
+ assertFalse(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_multipleSignersInSet() throws Exception {
+ // When a package is signed by multiple signers and all of the signers are in the Set
+ // then the verifier should return true.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setExpectedIdentities(
+ Set.of(SIGNATURE1_DIGEST, SIGNATURE2_DIGEST, SIGNATURE3_DIGEST))
+ .build();
+
+ assertTrue(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_multipleSignersNotInSet() throws Exception {
+ // When a package is signed by multiple signers all of the signers must be in the Set;
+ // this test verifies is only one of the signers is in the Set then the verifier returns
+ // false.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setExpectedIdentities(Set.of(SIGNATURE1_DIGEST, SIGNATURE3_DIGEST))
+ .build();
+
+ assertFalse(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_rotatedKeyOriginalInSet() throws Exception {
+ // When a package is signed with a rotated key and the original signing key is in the Set
+ // then the verifier should return true.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setExpectedIdentities(Set.of(SIGNATURE1_DIGEST))
+ .build();
+
+ assertTrue(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ @Config(maxSdk = 27)
+ public void verifyExpectedIdentity_rotatedKeyOnlyNewInSetApi27() throws Exception {
+ // When a package is signed with a rotated key API levels < 28 will only return the
+ // original signing key. If an app is targeting API levels < 28 and only the new signing
+ // certificate digest is in the Set then the verifier will return false on these API levels.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setExpectedIdentities(Set.of(SIGNATURE2_DIGEST))
+ .build();
+
+ assertFalse(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void verifyExpectedIdentity_rotatedKeyOnlyNewInSetApi28() throws Exception {
+ // When a package is signed with a rotated key API levels >= 28 will return the current
+ // signing certificate as well as the previous signers in the lineage. This test verifies
+ // if only the new signing certificate digest is in the Set then the verifier will return
+ // true for API levels >= 28.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addCurrentSigner(SIGNATURE2)
+ .setExpectedIdentities(Set.of(SIGNATURE2_DIGEST))
+ .build();
+
+ assertTrue(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ @Config(minSdk = 28)
+ public void verifyExpectedIdentity_multipleKeyRotationsMiddleInSetApi28() throws Exception {
+ // This test verifies if a package's signing key has been rotated multiple times and only
+ // one of the intermediate signing certificate digests is in the Set the verifier will
+ // match this certificate from the lineage and return true.
+ AppSignatureVerifier verifier = mBuilder.addSignerInLineage(SIGNATURE1)
+ .addSignerInLineage(SIGNATURE2)
+ .addCurrentSigner(SIGNATURE3)
+ .setExpectedIdentities(Set.of(SIGNATURE2_DIGEST))
+ .build();
+
+ assertTrue(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifyExpectedIdentity_singleSignerNoDigestsInSet() throws Exception {
+ // If the caller has not specified any expected signing certificate digests for a package
+ // then the verifier should return false without querying the PackageManager.
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1).build();
+
+ assertFalse(verifier.verifyExpectedIdentity(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_valueWrittenToCache() throws Exception {
+ // When a package is first verified its signing certificate digest(s) are computed and
+ // compared against the expected certificates. Since a package's signing identity cannot
+ // change without an update the verifier uses a cache to keep track of the results of
+ // previous queries; this test verifies when a package is successfully verified that
+ // package and its verification results are written to the cache.
+ AppSignatureVerifier.Cache cache = new AppSignatureVerifier.Cache(1);
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE1_DIGEST), TEST_PERMISSION_NAME)
+ .setCache(cache)
+ .build();
+ verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME);
+ AppSignatureVerifier.CacheEntry cacheEntry =
+ cache.get(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME);
+
+ assertTrue(cacheEntry.getVerificationResult());
+ }
+
+ @Test
+ public void verifySigningIdentity_requestInCache() throws Exception {
+ // After a package has been verified if it has not been updated before a subsequent
+ // request is made the verification result from the cache can be used. This test
+ // intentionally sets a different signing identity from that of the package being
+ // verified to ensure the value is taken from the cache; note that on a real device a
+ // signature change like this would not be possible without an app update.
+ AppSignatureVerifier.Cache cache = new AppSignatureVerifier.Cache(1);
+ cache.put(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME,
+ AppSignatureVerifier.CacheEntry.create(true, LAST_UPDATE_TIME));
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .setCache(cache)
+ .build();
+
+ assertTrue(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ @Test
+ public void verifySigningIdentity_packageUpdatedAfterCacheEntryCreated() throws Exception {
+ // When an app is updated or removed / reinstalled the signing identity of the app can
+ // change; the verifier uses the lastUpdateTime to determine if an app has been updated
+ // and if its signatures need to be verified again. This test verifies if a package
+ // previously passed the verification but has since been updated with a new signing
+ // identity the cache entry is invalidated and the verifier returns false.
+ AppSignatureVerifier.Cache cache = new AppSignatureVerifier.Cache(1);
+ cache.put(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME,
+ AppSignatureVerifier.CacheEntry.create(true, LAST_UPDATE_TIME));
+ AppSignatureVerifier verifier = mBuilder.addCurrentSigner(SIGNATURE1)
+ .setPackageCertDigestsForPermission(Set.of(SIGNATURE2_DIGEST), TEST_PERMISSION_NAME)
+ .setCache(cache)
+ .setLastUpdateTime(LAST_UPDATE_TIME + 1)
+ .build();
+
+ assertFalse(verifier.verifySigningIdentity(TEST_PACKAGE_NAME, TEST_PERMISSION_NAME));
+ }
+
+ /**
+ * Utility test builder that can be used to configure the AppSignatureVerifier under test as
+ * well as the signing identity to be returned by the {@link PackageManager#getPackageInfo}
+ * query for the package with name {@code TEST_PACKAGE_NAME}.
+ */
+ private static class AppSignatureVerifierTestBuilder {
+ private final List<Signature> mCurrentSigners;
+ private final List<Signature> mSigningLineage;
+ private final PackageManager mMockPackageManager;
+ private long mLastUpdateTime;
+ private final AppSignatureVerifier.Builder mBuilder;
+ private Map<String, Map<String, Set<String>>> mPermissionAllowMap;
+ private Map<String, Set<String>> mExpectedIdentities;
+
+ private AppSignatureVerifierTestBuilder(Context mockContext) {
+ mCurrentSigners = new ArrayList<>();
+ mSigningLineage = new ArrayList<>();
+ mMockPackageManager = mockContext.getPackageManager();
+ mLastUpdateTime = LAST_UPDATE_TIME;
+ mBuilder = AppSignatureVerifier.builder(mockContext);
+ }
+
+ private AppSignatureVerifierTestBuilder addCurrentSigner(String signer) {
+ mCurrentSigners.add(new Signature(signer));
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder addSignerInLineage(String signer) {
+ mSigningLineage.add(new Signature(signer));
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder setLastUpdateTime(long lastUpdateTime) {
+ mLastUpdateTime = lastUpdateTime;
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder setCache(AppSignatureVerifier.Cache cache) {
+ mBuilder.setCache(cache);
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder setPackageCertDigestsForPermission(
+ Set<String> packageCertDigests, String permission) {
+ Map<String, Set<String>> allowedPackageCerts = null;
+ if (mPermissionAllowMap == null) {
+ mPermissionAllowMap = new ArrayMap<>();
+ } else {
+ allowedPackageCerts = mPermissionAllowMap.get(permission);
+ }
+ if (allowedPackageCerts == null) {
+ allowedPackageCerts = new ArrayMap<>();
+ }
+ allowedPackageCerts.put(TEST_PACKAGE_NAME, packageCertDigests);
+ mPermissionAllowMap.put(permission, allowedPackageCerts);
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder setAllPackagesCertDigestsForPermission(
+ Set<String> allPackagesCertDigests, String permission) {
+ Map<String, Set<String>> allowedPackageCerts = null;
+ if (mPermissionAllowMap == null) {
+ mPermissionAllowMap = new ArrayMap<>();
+ } else {
+ allowedPackageCerts = mPermissionAllowMap.get(permission);
+ }
+ if (allowedPackageCerts == null) {
+ allowedPackageCerts = new ArrayMap<>();
+ }
+ allowedPackageCerts.put(AppAuthenticator.ALL_PACKAGES_TAG, allPackagesCertDigests);
+ mPermissionAllowMap.put(permission, allowedPackageCerts);
+ return this;
+ }
+
+ private AppSignatureVerifierTestBuilder setExpectedIdentities(
+ Set<String> expectedIdentities) {
+ if (mExpectedIdentities == null) {
+ mExpectedIdentities = new ArrayMap<>();
+ }
+ mExpectedIdentities.put(TEST_PACKAGE_NAME, expectedIdentities);
+ return this;
+ }
+
+ private AppSignatureVerifier build() throws Exception {
+ if (mCurrentSigners.isEmpty()) {
+ throw new IllegalArgumentException("At least one current signer must be specified");
+ }
+ Signature[] signatures = new Signature[mCurrentSigners.size()];
+ mCurrentSigners.toArray(signatures);
+ // If there is more than one current signer then the SigningInfo should return null
+ // for the lineage.
+ Signature[] signingLineage = null;
+ if (mCurrentSigners.size() == 1) {
+ // When there is only a single signer the current signer should be the last element
+ // in the signing lineage.
+ mSigningLineage.add(mCurrentSigners.get(0));
+ signingLineage = new Signature[mSigningLineage.size()];
+ mSigningLineage.toArray(signingLineage);
+ }
+
+ PackageInfo packageInfo = new PackageInfo();
+ // In the case of a rotated signing key the GET_SIGNATURES result will return the
+ // original signing key in the signatures array, so if the signing lineage is not
+ // empty then use the first key from the lineage for the signatures.
+ packageInfo.signatures = signingLineage != null
+ ? new Signature[]{signingLineage[0]} : signatures;
+ packageInfo.packageName = TEST_PACKAGE_NAME;
+ packageInfo.lastUpdateTime = mLastUpdateTime;
+ when(mMockPackageManager.getPackageInfo(TEST_PACKAGE_NAME,
+ PackageManager.GET_SIGNATURES)).thenReturn(packageInfo);
+ // API Level 28 introduced the SigningInfo to the PackageInfo object which is populated
+ // when the PackageManager.GET_SIGNING_CERTIFICATES flag is specified.
+ if (Build.VERSION.SDK_INT >= 28) {
+ packageInfo = new PackageInfo();
+ SigningInfo signingInfo = Shadow.newInstanceOf(SigningInfo.class);
+ shadowOf(signingInfo).setSignatures(signatures);
+ shadowOf(signingInfo).setPastSigningCertificates(signingLineage);
+ packageInfo.signingInfo = signingInfo;
+ packageInfo.packageName = TEST_PACKAGE_NAME;
+ packageInfo.lastUpdateTime = mLastUpdateTime;
+ when(mMockPackageManager.getPackageInfo(TEST_PACKAGE_NAME,
+ PackageManager.GET_SIGNING_CERTIFICATES)).thenReturn(packageInfo);
+ }
+ // Build the AppSignatureVerifier with the provided parameters to be used for the test.
+ return mBuilder.setPermissionAllowMap(mPermissionAllowMap)
+ .setExpectedIdentities(mExpectedIdentities)
+ .setDigestAlgorithm(DIGEST_ALGORITHM)
+ .build();
+ }
+ }
+}