AppAuthenticator.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.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Binder;
import android.text.TextUtils;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.XmlRes;
import androidx.collection.ArrayMap;
import androidx.collection.ArraySet;

import com.google.auto.value.AutoValue;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * Provides methods to verify the signing identity of other apps on the device.
 */
// TODO(b/175503230): Add usage details to class level documentation once implementation is
//  complete.
public class AppAuthenticator {
    private static final String TAG = "AppAuthenticator";

    /**
     * This is returned by {@link #checkCallingAppIdentity(String, String)} and
     * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name
     * has the expected signing identity for the provided permission.
     */
    public static final int PERMISSION_GRANTED = 0;

    /**
     * This is returned by {@link #checkCallingAppIdentity(String, String)} and
     * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name
     * does not have any of the expected signing identities for the provided permission.
     *
     * @see PackageManager#SIGNATURE_NO_MATCH
     */
    public static final int PERMISSION_DENIED_NO_MATCH = -3;

    /**
     * This is returned by {@link #checkCallingAppIdentity(String, String)} and
     * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name
     * does not belong to an app installed on the device.
     *
     * @see PackageManager#SIGNATURE_UNKNOWN_PACKAGE
     */
    public static final int PERMISSION_DENIED_UNKNOWN_PACKAGE = -4;

    /**
     * This is returned by {@link #checkCallingAppIdentity(String, String)} and
     * {@link #checkCallingAppIdentity(String, String, int, int)} when the specified package name
     * does not belong to the provided calling UID, or if the UID is not provided and the
     * specified package name does not belong to the UID of the calling process as returned by
     * {@link Binder#getCallingUid()}.
     */
    public static final int PERMISSION_DENIED_PACKAGE_UID_MISMATCH = -5;

    /**
     * This is returned by {@link #checkAppIdentity(String)} when the specified package name has
     * the expected signing identity.
     *
     * @see PackageManager#SIGNATURE_MATCH
     */
    public static final int SIGNATURE_MATCH = 0;

    /**
     * This is returned by {@link #checkAppIdentity(String)} when the specified package name does
     * not have the expected signing identity.
     *
     * @see PackageManager#SIGNATURE_NO_MATCH
     */
    public static final int SIGNATURE_NO_MATCH = -1;

    /**
     * The root tag for an AppAuthenticator XMl config file.
     */
    private static final String ROOT_TAG = "app-authenticator";
    /**
     * The tag to declare a new permission that can be granted to enclosed packages / signing
     * identities.
     */
    private static final String PERMISSION_TAG = "permission";
    /**
     * The tag to begin declaration of the expected signing identities for the enclosed packages.
     */
    private static final String EXPECTED_IDENTITY_TAG = "expected-identity";
    /**
     * The tag to declare a new signing identity of a package within either a permission or
     * expected-identity element.
     */
    private static final String PACKAGE_TAG = "package";
    /**
     * The tag to declare all packages signed with the enclosed signing identities are to be
     * granted to the enclosing permission.
     */
    static final String ALL_PACKAGES_TAG = "all-packages";
    /**
     * The tag to declare a known signing certificate digest for the enclosing package.
     */
    private static final String CERT_DIGEST_TAG = "cert-digest";
    /**
     * The attribute to declare the name within a permission or package element.
     */
    private static final String NAME_ATTRIBUTE = "name";
    /**
     * The default digest algorithm used for all certificate digests if one is not specified in
     * the root element.
     */
    static final String DEFAULT_DIGEST_ALGORITHM = "SHA-256";

    private AppSignatureVerifier mAppSignatureVerifier;
    private AppAuthenticatorUtils mAppAuthenticatorUtils;

    /**
     * Private constructor; instances should be created through the static factory methods.
     *
     * @param appSignatureVerifier the verifier to be used to verify app signing identities
     * @param appAuthenticatorUtils the utils to be used
     */
    AppAuthenticator(AppSignatureVerifier appSignatureVerifier,
            AppAuthenticatorUtils appAuthenticatorUtils) {
        mAppSignatureVerifier = appSignatureVerifier;
        mAppAuthenticatorUtils = appAuthenticatorUtils;
    }

    /**
     * Allows injection of the {@code appSignatureVerifier} to be used during tests.
     */
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    void setAppSignatureVerifier(AppSignatureVerifier appSignatureVerifier) {
        mAppSignatureVerifier = appSignatureVerifier;
    }

    /**
     * Allows injection of the {@code appAuthenticatorUtils} to be used during tests.
     * @param appAuthenticatorUtils
     */
    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
    void setAppAuthenticatorUtils(AppAuthenticatorUtils appAuthenticatorUtils) {
        mAppAuthenticatorUtils = appAuthenticatorUtils;
    }

    /**
     * Enforces the specified {@code packageName} has the expected signing identity for the
     * provided {@code permission}.
     *
     * <p>This method should be used when verifying the identity of a calling process of an IPC.
     * This is the same as calling {@link #enforceCallingAppIdentity(String, String, int, int)} with
     * the pid and uid returned by {@link Binder#getCallingPid()} and
     * {@link Binder#getCallingUid()}.
     *
     * @param packageName the name of the package to be verified
     * @param permission the name of the permission as specified in the XML from which to verify the
     *                   package / signing identity
     * @throws SecurityException if the signing identity of the package does not match that defined
     * for the permission
     */
    public void enforceCallingAppIdentity(@NonNull String packageName, @NonNull String permission) {
        enforceCallingAppIdentity(packageName, permission,
                mAppAuthenticatorUtils.getCallingPid(), mAppAuthenticatorUtils.getCallingUid());
    }

    /**
     * Enforces the specified {@code packageName} belongs to the provided {@code pid} / {@code uid}
     * and has the expected signing identity for the {@code permission}.
     *
     * <p>This method should be used when verifying the identity of a calling process of an IPC.
     *
     * @param packageName the name of the package to be verified
     * @param permission the name of the permission as specified in the XML from which to verify the
     *                   package / signing identity
     * @param pid the expected pid of the process
     * @param uid the expected uid of the package
     * @throws SecurityException if the uid does not belong to the specified package, or if the
     * signing identity of the package does not match that defined for the permission
     */
    public void enforceCallingAppIdentity(@NonNull String packageName, @NonNull String permission,
            int pid, int uid) {
        AppAuthenticatorResult result = checkCallingAppIdentityInternal(packageName, permission,
                pid, uid);
        if (result.getResultCode() != PERMISSION_GRANTED) {
            throw new SecurityException(result.getResultMessage());
        }
    }

    /**
     * Checks the specified {@code packageName} has the expected signing identity for the
     * provided {@code permission}.
     *
     * <p>This method should be used when verifying the identity of a calling process of an IPC.
     * This is the same as calling {@link #checkCallingAppIdentity(String, String, int, int)} with
     * the pid and uid returned by {@link Binder#getCallingPid()} and
     * {@link Binder#getCallingUid()}.
     *
     * @param packageName the name of the package to be verified
     * @param permission the name of the permission as specified in the XML from which to verify the
     *                   package / signing identity
     * @return {@link #PERMISSION_GRANTED} if the specified {@code packageName} has the expected
     * signing identity for the provided {@code permission},<br>
     *     {@link #PERMISSION_DENIED_NO_MATCH} if the specified {@code packageName} does not have
     *     the expected signing identity for the provided {@code permission},<br>
     *     {@link #PERMISSION_DENIED_UNKNOWN_PACKAGE} if the specified {@code packageName} does not
     *     exist on the device,<br>
     *     {@link #PERMISSION_DENIED_PACKAGE_UID_MISMATCH} if the uid as returned from
     *     {@link Binder#getCallingUid()} does not match the uid assigned to the package
     */
    public int checkCallingAppIdentity(@NonNull String packageName, @NonNull String permission) {
        return checkCallingAppIdentity(packageName, permission,
                mAppAuthenticatorUtils.getCallingPid(), mAppAuthenticatorUtils.getCallingUid());
    }

    /**
     * Checks the specified {@code packageName} has the expected signing identity for the
     * provided {@code permission}.
     *
     * <p>This method should be used when verifying the identity of a calling process of an IPC.
     *
     * @param packageName the name of the package to be verified
     * @param permission the name of the permission as specified in the XML from which to verify the
     *                   package / signing identity
     * @param pid the expected pid of the process
     * @param uid the expected uid of the package
     * @return {@link #PERMISSION_GRANTED} if the specified {@code packageName} has the expected
     * signing identity for the provided {@code permission},<br>
     *     {@link #PERMISSION_DENIED_NO_MATCH} if the specified {@code packageName} does not have
     *     the expected signing identity for the provided {@code permission},<br>
     *     {@link #PERMISSION_DENIED_UNKNOWN_PACKAGE} if the specified {@code packageName} does not
     *     exist on the device,<br>
     *     {@link #PERMISSION_DENIED_PACKAGE_UID_MISMATCH} if the specified {@code uid} does not
     *     match the uid assigned to the package
     */
    public int checkCallingAppIdentity(@NonNull String packageName, @NonNull String permission,
            int pid, int uid) {
        AppAuthenticatorResult result = checkCallingAppIdentityInternal(packageName, permission,
                pid, uid);
        if (result.getResultCode() != PERMISSION_GRANTED) {
            Log.e(TAG, result.getResultMessage());
        }
        return result.getResultCode();
    }

    /**
     * Checks the specified {@code packageName} has the expected signing identity for the
     * provided {@code permission} under the calling {@code pid} and {@code uid}.
     */
    // The pid variable may be used in a future release for platform verification; it is
    // currently added to the public API and this method to seamlessly make use of any platform
    // features in the future.
    @SuppressWarnings("UnusedVariable")
    private AppAuthenticatorResult checkCallingAppIdentityInternal(String packageName,
            String permission,
            int pid,
            int uid) {
        // First verify that the UID of the calling package matches the specified value.
        int packageUid;
        try {
            packageUid = mAppAuthenticatorUtils.getUidForPackage(packageName);
        } catch (PackageManager.NameNotFoundException e) {
            return AppAuthenticatorResult.create(PERMISSION_DENIED_UNKNOWN_PACKAGE,
                    "The app " + packageName + " was not found on the device");
        }
        if (packageUid != uid) {
            return AppAuthenticatorResult.create(PERMISSION_DENIED_PACKAGE_UID_MISMATCH,
                    "The expected UID, " + uid + ", of the app " + packageName
                            + " does not match the actual UID, " + packageUid);
        }
        if (mAppSignatureVerifier.verifySigningIdentity(packageName, permission)) {
            return AppAuthenticatorResult.create(PERMISSION_GRANTED, null);
        }
        return AppAuthenticatorResult.create(PERMISSION_DENIED_NO_MATCH, "The signing"
                + " identity of app " + packageName + " does not match the expected identity");
    }


    /**
     * Enforces the specified {@code packageName} has the expected signing identity as declared in
     * the {@code <expected-identity>} tag.
     *
     * <p>This method should be used when an app's signing identity must be verified; for instance
     * before a client connects to an exported service this method can be used to verify that the
     * app comes from the expected developer.
     *
     * @param packageName the name of the package to be verified
     * @throws SecurityException if the signing identity of the package does not match that defined
     * in the {@code <expected-identity>} tag
     */
    public void enforceAppIdentity(@NonNull String packageName) {
        if (checkAppIdentity(packageName) != SIGNATURE_MATCH) {
            throw new SecurityException("The app " + packageName + " does not match the expected "
                    + "signing identity");
        }
    }

    /**
     * Checks the specified {@code packageName} has the expected signing identity as specified in
     * the {@code <expected-identity>} tag.
     *
     * <p>This method should be used when an app's signing identity must be verified; for instance
     * before a client connects to an exported service this method can be used to verify that the
     * app comes from the expected developer.
     *
     * @param packageName the name of the package to be verified
     * @return {@link #SIGNATURE_MATCH} if the specified package has the expected
     * signing identity
     */
    public int checkAppIdentity(@NonNull String packageName) {
        if (mAppSignatureVerifier.verifyExpectedIdentity(packageName)) {
            return SIGNATURE_MATCH;
        }
        return SIGNATURE_NO_MATCH;
    }

    /**
     * Creates a new {@code AppAuthenticator} that can be used to guard resources based on
     * package name / signing identity as well as allow verification of expected signing identities
     * before interacting with other apps on a device using the configuration defined in the
     * provided {@code xmlInputStream}.
     *
     * @param context        the context within which to create the {@code AppAuthenticator}
     * @param xmlInputStream the XML {@link InputStream} containing the definitions for the
     *                       permissions and expected identities based on packages / expected
     *                       signing certificate digests
     * @return a new {@code AppAuthenticator} that can be used to enforce the signing
     * identities defined in the provided XML {@code InputStream}
     * @throws AppAuthenticatorXmlException if the provided XML {@code InputStream} is not in the
     *                                      proper format to create a new {@code AppAuthenticator}
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      the XML {@code InputStream}
     */
    public static @NonNull AppAuthenticator createFromInputStream(@NonNull Context context,
            @NonNull InputStream xmlInputStream) throws AppAuthenticatorXmlException, IOException {
        XmlPullParser parser;
        try {
            parser = XmlPullParserFactory.newInstance().newPullParser();
            parser.setInput(xmlInputStream, null);
        } catch (XmlPullParserException e) {
            throw new AppAuthenticatorXmlException("Unable to create parser from provided "
                    + "InputStream", e);
        }
        return createFromParser(context, parser);
    }

    /**
     * Creates a new {@code AppAuthenticator} that can be used to guard resources based on
     * package name / signing identity as well as allow verification of expected signing identities
     * before interacting with other apps on a device using the configuration defined in the
     * provided XML resource.
     *
     * @param context     the context within which to create the {@code AppAuthenticator}
     * @param xmlResource the ID of the XML resource containing the definitions for the
     *                    permissions and expected identities based on package / expected signing
     *                    certificate digests
     * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities
     * defined in the provided XML resource
     * @throws AppAuthenticatorXmlException if the provided XML resource is not in the proper format
     *                                      to create a new {@code AppAuthenticator}
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      the XML resource
     */
    public static @NonNull AppAuthenticator createFromResource(@NonNull Context context,
            @XmlRes int xmlResource) throws AppAuthenticatorXmlException, IOException {
        Resources resources = context.getResources();
        XmlPullParser parser = resources.getXml(xmlResource);
        return createFromParser(context, parser);
    }

    /**
     * Creates a new {@code AppAuthenticator} that can be used to guard resources based on
     * package name / signing identity as well as allow verification of expected signing identities
     * before interacting with other apps on a device using the configuration defined in the
     * provided {@code parser}.
     *
     * @param context the context within which to create the {@code AppAuthenticator}
     * @param parser  an {@link XmlPullParser} containing the definitions for the
     *                permissions and expected identities based on package / expected signing
     *                certificate digests
     * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities
     * defined in the provided {@code XmlPullParser}
     * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser}
     *                                      is not in the proper format to create a new
     *                                      {@code AppAuthenticator}
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      from the {@code XmlPullParser}
     */
    private static AppAuthenticator createFromParser(Context context, XmlPullParser parser)
            throws AppAuthenticatorXmlException, IOException {
        AppAuthenticatorConfig config = createConfigFromParser(parser);
        return createFromConfig(context, config);
    }

    /**
     * Creates a new {@code AppAuthenticator} that can be used to guard resources based on
     * package name / signing identity as well as allow verification of expected signing identities
     * before interacting with other apps on a device using the configuration defined in the
     * provided {@code config}.
     *
     * @param context the context within which to create the {@code AppAuthenticator}
     * @param config  an {@link AppAuthenticatorConfig} containing the definitions for the
     *                permissions and expected identities based on package / expected signing
     *                certificate digests
     * @return a new {@code AppAuthenticator} that can be used to enforce the signing identities
     * defined in the provided {@code config}
     */
    static AppAuthenticator createFromConfig(Context context,
            @NonNull AppAuthenticatorConfig config) {
        AppSignatureVerifier verifier = AppSignatureVerifier.builder(context)
                .setPermissionAllowMap(config.getPermissionAllowMap())
                .setExpectedIdentities(config.getExpectedIdentities())
                .setDigestAlgorithm(config.getDigestAlgorithm())
                .build();
        return new AppAuthenticator(verifier, new AppAuthenticatorUtils(context));
    }

    /**
     * Creates a new {@code AppAuthenticatorConfig} that can be used to instantiate a new {@code
     * AppAuthenticator} with the specified config.
     *
     * @param parser an {@link XmlPullParser} containing the definition for the permissions and
     *               expected identities based on package / expected signing certificate digests
     * @return a new {@code AppAuthenticatorConfig} based on the config declared in the {@code
     * parser} that can be used to instantiate a new {@code AppAuthenticator}.
     * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser}
     *                                      is not in the proper format to create a new
     *                                      {@code AppAuthenticator}
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      from the {@code XmlPullParser}
     */
    static AppAuthenticatorConfig createConfigFromParser(XmlPullParser parser)
            throws AppAuthenticatorXmlException, IOException {
        Map<String, Map<String, Set<String>>> permissionAllowMap = new ArrayMap<>();
        Map<String, Set<String>> expectedIdentities = new ArrayMap<>();
        try {
            parseToNextStartTag(parser);
            String tag = parser.getName();
            if (TextUtils.isEmpty(tag) || !tag.equalsIgnoreCase(ROOT_TAG)) {
                throw new AppAuthenticatorXmlException(
                        "Provided XML does not contain the expected root tag: " + ROOT_TAG);
            }
            assertExpectedAttribute(parser, ROOT_TAG, null, false);
            String digestAlgorithm = DEFAULT_DIGEST_ALGORITHM;
            int eventType = parser.nextTag();
            // Each new start tag should be for a new permission / expected-identity.
            while (eventType == XmlPullParser.START_TAG) {
                tag = parser.getName();
                if (tag.equalsIgnoreCase(PERMISSION_TAG)) {
                    assertExpectedAttribute(parser, PERMISSION_TAG, NAME_ATTRIBUTE, true);
                    String permissionName = parser.getAttributeValue(null, NAME_ATTRIBUTE);
                    if (TextUtils.isEmpty(permissionName)) {
                        throw new AppAuthenticatorXmlException(
                                "The " + PERMISSION_TAG + " tag requires a non-empty value for the "
                                        + NAME_ATTRIBUTE + " attribute");
                    }
                    Map<String, Set<String>> allowedPackageCerts = parsePackages(parser, true);
                    if (permissionAllowMap.containsKey(permissionName)) {
                        permissionAllowMap.get(permissionName).putAll(allowedPackageCerts);
                    } else {
                        permissionAllowMap.put(permissionName, allowedPackageCerts);
                    }
                } else if (tag.equalsIgnoreCase(EXPECTED_IDENTITY_TAG)) {
                    assertExpectedAttribute(parser, EXPECTED_IDENTITY_TAG, null, true);
                    expectedIdentities.putAll(parsePackages(parser, false));
                } else {
                    throw new AppAuthenticatorXmlException(
                            "Expected " + PERMISSION_TAG + " or " + EXPECTED_IDENTITY_TAG
                                    + " under root tag at line " + parser.getLineNumber());
                }
                eventType = parser.nextTag();
            }
            return AppAuthenticatorConfig.create(permissionAllowMap, expectedIdentities,
                    digestAlgorithm);
        } catch (XmlPullParserException e) {
            throw new AppAuthenticatorXmlException("Caught an exception parsing the provided "
                    + "XML:", e);
        }
    }

    /**
     * Parses package tags from the provided {@code parser}, allowing the {@code all-packages}
     * tag if the {@code allPackagesAllowed} boolean is true.
     *
     * @param parser             the {@link XmlPullParser} from which to parser the packages
     * @param allPackagesAllowed boolean indicating whether the {@code all-packages} element is
     *                           allowed
     * @return a mapping from the enclosed packages to signing identities
     * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser}
     *                                      is not in the proper format
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      from the {@code XmlPullParser}
     * @throws XmlPullParserException       if any errors are encountered when attempting to
     *                                      parse the provided {@code XmlPullParser}
     */
    private static Map<String, Set<String>> parsePackages(XmlPullParser parser,
            boolean allPackagesAllowed)
            throws AppAuthenticatorXmlException, IOException, XmlPullParserException {
        Map<String, Set<String>> allowedPackageCerts = new ArrayMap<>();
        int eventType = parser.nextTag();
        while (eventType == XmlPullParser.START_TAG) {
            String tag = parser.getName();
            String packageName;
            if (tag.equalsIgnoreCase(PACKAGE_TAG)) {
                assertExpectedAttribute(parser, PACKAGE_TAG, NAME_ATTRIBUTE, true);
                packageName = parser.getAttributeValue(null, NAME_ATTRIBUTE);
                if (TextUtils.isEmpty(packageName)) {
                    throw new AppAuthenticatorXmlException(
                            "The " + PACKAGE_TAG + " tag requires a non-empty value for the "
                                    + NAME_ATTRIBUTE + " attribute");
                }
            } else if (tag.equalsIgnoreCase(ALL_PACKAGES_TAG)) {
                packageName = ALL_PACKAGES_TAG;
                if (!allPackagesAllowed) {
                    throw new AppAuthenticatorXmlException("The " + ALL_PACKAGES_TAG
                            + " tag is not allowed within this element on line "
                            + parser.getLineNumber());
                }
            } else {
                throw new AppAuthenticatorXmlException(
                        "Unexpected tag " + tag + " on line " + parser.getLineNumber()
                                + "; expected " + PACKAGE_TAG + "" + (allPackagesAllowed ? " or "
                                + ALL_PACKAGES_TAG : ""));
            }
            Set<String> allowedCertDigests = parseCertDigests(parser);
            if (allowedCertDigests.isEmpty()) {
                throw new AppAuthenticatorXmlException("No " + CERT_DIGEST_TAG + " tag found "
                        + "within " + tag + " element on line " + parser.getLineNumber());
            }
            if (allowedPackageCerts.containsKey(packageName)) {
                allowedPackageCerts.get(packageName).addAll(allowedCertDigests);
            } else {
                allowedPackageCerts.put(packageName, allowedCertDigests);
            }
            eventType = parser.nextTag();
        }
        return allowedPackageCerts;
    }

    /**
     * Parses certificate digests from the provided {@code parser}, returning a {@link Set} of
     * parsed digests.
     *
     * @param parser the {@link XmlPullParser} from which to parser the digests
     * @return a {@code Set} of certificate digests
     * @throws AppAuthenticatorXmlException if the provided XML parsed by the {@code XmlPullParser}
     *                                      is not in the proper format
     * @throws IOException                  if an IO error is encountered when attempting to read
     *                                      from the {@code XmlPullParser}
     * @throws XmlPullParserException       if any errors are encountered when attempting to
     *                                      parse the provided {@code XmlPullParser}
     */
    private static Set<String> parseCertDigests(XmlPullParser parser)
            throws AppAuthenticatorXmlException, IOException,
            XmlPullParserException {
        Set<String> allowedCertDigests = new ArraySet<>();
        int eventType = parser.nextTag();
        while (eventType == XmlPullParser.START_TAG) {
            String tag = parser.getName();
            if (!tag.equalsIgnoreCase(CERT_DIGEST_TAG)) {
                throw new AppAuthenticatorXmlException(
                        "Expected " + CERT_DIGEST_TAG + " on line " + parser.getLineNumber());
            }
            String digest = parser.nextText().trim();
            if (TextUtils.isEmpty(digest)) {
                throw new AppAuthenticatorXmlException("The " + CERT_DIGEST_TAG + " element "
                        + "on line " + parser.getLineNumber() + " must have non-empty text "
                        + "containing the certificate digest of the signer");
            }
            allowedCertDigests.add(normalizeCertDigest(digest));
            eventType = parser.nextTag();
        }
        return allowedCertDigests;
    }

    /**
     * Normalizes the provided {@code certDigest} to ensure it is in the proper form for {@code
     * Collection} membership checks when comparing a package's signing certificate digest against
     * those provided to the {@code AppAuthenticator}.
     *
     * @param certDigest the digest to be normalized
     * @return a normalized form of the provided digest that can be used in subsequent {@code
     * Collection} membership checks
     */
    static String normalizeCertDigest(String certDigest) {
        // The AppAuthenticatorUtils#computeDigest method uses lower case characters to compute the
        // digest.
        return certDigest.toLowerCase(Locale.US);
    }

    /**
     * Moves the provided {@code parser} to the next {@link XmlPullParser#START_TAG} or {@link
     * XmlPullParser#END_DOCUMENT} if the end of the document is reached, returning the value of
     * the event type.
     */
    private static int parseToNextStartTag(XmlPullParser parser) throws IOException,
            XmlPullParserException {
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Empty loop to reach the first start tag or end of the document.
        }
        return type;
    }

    /**
     * Asserts the current {@code tagName} contains only the specified {@code expectedAttribute},
     * or no elements if not {@code required}; a null {@code expectedAttribute} can be used to
     * assert no attributes are provided.
     *
     * <p>This method is intended to report if unsupported attributes are specified to warn the
     * caller that the provided value will not be used by this instance. Since this method is
     * checking the attributes it must only be called when the current event type is {@link
     * XmlPullParser#START_TAG}.
     */
    private static void assertExpectedAttribute(XmlPullParser parser, String tagName,
            String expectedAttribute, boolean required)
            throws AppAuthenticatorXmlException, XmlPullParserException {
        int attributeCount = parser.getAttributeCount();
        if (attributeCount == -1) {
            throw new AssertionError(
                    "parser#getAttributeCount called for event type " + parser.getEventType()
                            + " on line " + parser.getLineNumber());
        }
        if (attributeCount == 0 && expectedAttribute != null && required) {
            throw new AppAuthenticatorXmlException("The attribute " + expectedAttribute + " is "
                    + "required for tag " + tagName + " on line " + parser.getLineNumber());
        }
        StringBuilder unsupportedAttributes = null;
        for (int i = 0; i < attributeCount; i++) {
            String attributeName = parser.getAttributeName(i);
            if (!attributeName.equalsIgnoreCase(expectedAttribute)) {
                if (unsupportedAttributes == null) {
                    unsupportedAttributes = new StringBuilder();
                } else {
                    unsupportedAttributes.append(", ");
                }
                unsupportedAttributes.append(attributeName);
            }
        }
        if (unsupportedAttributes != null) {
            String prefixMessage;
            if (expectedAttribute == null) {
                prefixMessage = "Tag " + tagName + " does not support any attributes";
            } else {
                prefixMessage = "Tag " + tagName + " only supports attribute " + expectedAttribute;
            }
            throw new AppAuthenticatorXmlException(
                    prefixMessage + "; found the following unsupported attributes on line "
                            + parser.getLineNumber() + ": " + unsupportedAttributes);
        }
    }

    /**
     * Value class containing the configuration for an {@code AppAuthenticator}.
     */
    // Suppressing the AutoValue immutable field warning as this class is only used internally
    // and is not worth bringing in the dependency for the immutable classes.
    @SuppressWarnings("AutoValueImmutableFields")
    @AutoValue
    abstract static class AppAuthenticatorConfig {
        /**
         * Returns a mapping from permission to allowed packages / signing identities.
         */
        abstract Map<String, Map<String, Set<String>>> getPermissionAllowMap();

        /**
         * Returns a mapping from package name to expected signing identities.
         */
        abstract Map<String, Set<String>> getExpectedIdentities();

        /**
         * Returns the digest algorithm to be used.
         */
        abstract String getDigestAlgorithm();

        /**
         * Creates a new instance with the provided {@code permissionAllowMap}, {@code
         * expectedIdentities}, and {@code digestAlgorithm}.
         *
         * @param permissionAllowMap the mapping from permission to allowed packages / signing
         *                           identities
         * @param expectedIdentities the mapping from package name to expected signing identities
         * @param digestAlgorithm the digest algorithm to be used when computing signing
         *                        certificate digests
         * @return a new {@code AppAuthenticatorConfig} that can be used to configure the
         * AppAuthenticator instance.
         */
        static AppAuthenticatorConfig create(
                Map<String, Map<String, Set<String>>> permissionAllowMap,
                Map<String, Set<String>> expectedIdentities, String digestAlgorithm) {
            return new AutoValue_AppAuthenticator_AppAuthenticatorConfig(permissionAllowMap,
                    expectedIdentities, digestAlgorithm);

        }
    }

    /**
     * Value class for the result of an {@code AppAuthenticator} query.
     */
    @AutoValue
    abstract static class AppAuthenticatorResult {
        /**
         * Returns the result code for the query.
         */
        abstract int getResultCode();

        /**
         * Returns the result message for the query; if the query successfully verified an app's
         * signature matches the expected signing identity this value will be {@code null}.
         */
        @Nullable
        abstract String getResultMessage();

        /**
         * Creates a new instance with the provided {@code resultCode} and {@code resultMessage}.
         */
        static AppAuthenticatorResult create(int resultCode, String resultMessage) {
            return new AutoValue_AppAuthenticator_AppAuthenticatorResult(resultCode, resultMessage);
        }
    }
}