TestAppSignatureVerifier.java

/*
 * Copyright 2021 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.content.Context;

import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;

import java.util.Map;
import java.util.Set;

/**
 * An extension of the {@link AppSignatureVerifier} used by the {@link AppAuthenticator} that can
 * be injected into the {@code AppAuthenticator} to configure it to behave as required by the test.
 *
 * <p>This test class supports setting a {@link TestAppAuthenticatorBuilder.TestPolicy},
 * configuring generic acceptance per package, specifying the signing identity per package, and
 * treating packages as not installed.
 */
class TestAppSignatureVerifier extends AppSignatureVerifier {
    /**
     * A Set of classes to be treated as always accepted as long as they are in the XML config file.
     */
    private final Set<String> mSignatureAcceptedPackages;
    /**
     * A Set of classes to be treated as not installed.
     */
    private final Set<String> mNotInstalledPackages;
    /**
     * A mapping from the package name to the digest to be used as the signing identity for the
     * package during the test.
     */
    private final Map<String, String> mSigningIdentities;
    /**
     * The test policy to be used.
     */
    private final @TestAppAuthenticatorBuilder.TestPolicy int mTestPolicy;

    /**
     * Constructor that should only be invoked by the {@link Builder}.
     */
    TestAppSignatureVerifier(Context context,
            Map<String, Map<String, Set<String>>> permissionAllowMap,
            Map<String, Set<String>> expectedIdentities,
            Set<String> signatureAcceptedPackages,
            Set<String> notInstalledPackages,
            Map<String, String> signingIdentities,
            @TestAppAuthenticatorBuilder.TestPolicy int testPolicy) {
        super(context, permissionAllowMap, expectedIdentities,
                AppAuthenticator.DEFAULT_DIGEST_ALGORITHM, new NullCache());
        mSignatureAcceptedPackages = signatureAcceptedPackages;
        mNotInstalledPackages = notInstalledPackages;
        mSigningIdentities = signingIdentities;
        mTestPolicy = testPolicy;
    }

    /*
     * Builder for a new {@link TestAppSignatureVerifier} that allows this test class to be
     * configured as required for the test.
     */
    static class Builder {
        private final Context mContext;
        private Map<String, Map<String, Set<String>>> mPermissionAllowMap;
        private Map<String, Set<String>> mExpectedIdentities;
        private Set<String> mSignatureAcceptedPackages;
        private Set<String> mNotInstalledPackages;
        private String mDigestAlgorithm;
        private Map<String, String> mSigningIdentities;
        private @ TestAppAuthenticatorBuilder.TestPolicy int mTestPolicy;

        /**
         * Constructor accepting the {@code context} used to instantiate a new {@code
         * TestAppSignatureVerifier}.
         */
        Builder(Context context) {
            mContext = context;
            mSignatureAcceptedPackages = new ArraySet<>();
            mNotInstalledPackages = new ArraySet<>();
            mSigningIdentities = new ArrayMap<>();
        }

        /**
         * Configures the resulting {@link TestAppSignatureVerifier} to always return that the
         * signing identity matches the expected value when the specified {@code packageName} is
         * queried.
         *
         * @param packageName the name of the package for which the signing identity should be
         *                    treated as matching the expected value
         * @return this instance of the {@code Builder}
         */
        Builder setSignatureAcceptedForPackage(String packageName) {
            mSignatureAcceptedPackages.add(packageName);
            return this;
        }

        /**
         * Sets the provided {@code certDigest} as the signing identity for the specified {@code
         * packageName}.
         *
         * @param packageName the name of the package that will use the provided signing identity
         * @param certDigest the digest to be treated as the signing identity of the specified
         *                  package
         * @return this instance of the {@code Builder}
         */
        Builder setSigningIdentityForPackage(String packageName, String certDigest) {
            mSigningIdentities.put(packageName, certDigest);
            return this;
        }

        /**
         * Sets the {@code permissionAllowMap} to be used by the {@code TestAppSignatureVerifier}.
         *
         * 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.
         *
         * @return this instance of the {@code Builder}
         */
        Builder setPermissionAllowMap(Map<String, Map<String, Set<String>>> permissionAllowMap) {
            mPermissionAllowMap = permissionAllowMap;
            return this;
        }

        /**
         * Sets the {@code expectedIdentities} to be used by the {@code TestAppSignatureVerifier}.
         *
         * This {@code Map} should contain a mapping from package name to the expected signing
         * certificate digest(s).
         *
         * @return this instance of the {@code Builder}
         */
        Builder setExpectedIdentities(Map<String, Set<String>> expectedIdentities) {
            mExpectedIdentities = expectedIdentities;
            return this;
        }

        /**
         * Sets the test policy to be used by the {@code TestAppSignatureVerifier}.
         *
         * @return this instance of the {@code Builder}
         */
        Builder setTestPolicy(@ TestAppAuthenticatorBuilder.TestPolicy int testPolicy) {
            mTestPolicy = testPolicy;
            return this;
        }

        /**
         * Treats the provided {@code packageName} as not being installed by the resulting {@link
         * TestAppSignatureVerifier}.
         *
         * @param packageName the name of the package to be treated as not installed
         * @return this instance of the {@code Builder}
         */
        Builder setPackageNotInstalled(String packageName) {
            mNotInstalledPackages.add(packageName);
            return this;
        }

        /**
         * Builds a new {@code TestAppSignatureVerifier} instance using the provided configuration.
         */
        TestAppSignatureVerifier build() {
            if (mPermissionAllowMap == null) {
                mPermissionAllowMap = new ArrayMap<>();
            }
            if (mExpectedIdentities == null) {
                mExpectedIdentities = new ArrayMap<>();
            }
            if (mDigestAlgorithm == null) {
                mDigestAlgorithm = AppAuthenticator.DEFAULT_DIGEST_ALGORITHM;
            }
            return new TestAppSignatureVerifier(mContext, mPermissionAllowMap, mExpectedIdentities,
                    mSignatureAcceptedPackages, mNotInstalledPackages, mSigningIdentities,
                    mTestPolicy);
        }
    }

    /**
     * Responds to a signing identity query using the specified config for the provided {@code
     * packageName} where the package is expected to have the signing identity in the {@code
     * packageCertDigests} and, where applicable, {@code all-packages} are supported with the
     * {@code allPackagesCertDigests}.
     *
     * <p>Package queries are performed in the following order:
     * <ul>
     *     <li>If the test policy is {@code POLICY_DENY_ALL} then {@code false} is returned</li>
     *     {li>If the test policy is {@code POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES} then
     *     {@code true} is returned as long as the specified package is explicitly declared with one
     *     or more signing identities for this query</li>
     *     <li>If the package is configured to be treated as not installed {@code false} is
     *     returned</li>
     *     <li>If the package is configured to have its signing identity accepted then {@code
     *     true} is returned</li>
     *     <li>If a signing identity is configured for the package then it is compared against
     *     the expected signing identity declared in the XML config; if there is a match then
     *     {@code true} is returned</li>
     * </ul>
     * @param packageName the name of the package being queried
     * @param query the type of query being performed
     * @param packageCertDigests a {@code Set} of certificate digests that are expected for the
     *                           package
     * @param allPackagesCertDigests a {@code Set} of certificate digests that are expected for
     *                               any package for this query
     * @return {@code true} if the package can be treated as successfully verified based on the
     * test configuration
     */
    @Override
    boolean verifySigningIdentityForQuery(String packageName, String query,
            Set<String> packageCertDigests, Set<String> allPackagesCertDigests) {
        if (mTestPolicy ==  TestAppAuthenticatorBuilder.POLICY_DENY_ALL) {
            return false;
        }
        if (mTestPolicy
                == TestAppAuthenticatorBuilder.POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES) {
            // packageCertDigests will only be set if the package is explicitly declared for the
            // query
            return packageCertDigests != null;
        }
        if (mNotInstalledPackages.contains(packageName)) {
            return false;
        }
        if (mSignatureAcceptedPackages.contains(packageName)) {
            return true;
        }
        String certDigest = mSigningIdentities.get(packageName);
        if (certDigest != null) {
            if (packageCertDigests != null && packageCertDigests.contains(certDigest)) {
                return true;
            }
            if (allPackagesCertDigests != null && allPackagesCertDigests.contains(certDigest)) {
                return true;
            }
        }
        return false;
    }

    /**
     * A test version of the {@code Cache} that always returns {@code null} for a cache query;
     * this is intended to always force the test to go through the configured verification
     * process as opposed to returning a previous query result.
     */
    static class NullCache extends Cache {
        /**
         * Instantiates a new NullCache; since it is not intended to return cached values a max
         * size is not accepted, but a value of 1 is used since a value <= 0 is treated as an
         * error by the {@link androidx.collection.LruCache}.
         */
        NullCache() {
            super(1);
        }

        /**
         * Overrides the {@link Cache#get} method to return a null value for all cache queries.
         */
        @Override
        CacheEntry get(String packageName, String query) {
            return null;
        }
    }
}