AppSignatureVerifier.java

/*
 * 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);
        }
    }
}