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();
+        }
+    }
+}