TestAppAuthenticatorBuilder.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.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.os.Binder;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.XmlRes;
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.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Builder class that can be used to facilitate the creation of a new {@link AppAuthenticator} which
* can be configured to meet the requirements of the test. Similar to the {@code AppAuthenticator},
* the static factory methods for this class require either an XML resource or {@link InputStream}
* containing the {@code app-authenticator} configuration allowing verification of your declared
* config as part of the test.
*
* <p>There are several options to configure the behavior of the resulting {@code AppAuthenticator}.
* <ul>
* <li>{@link #setTestPolicy(int)} - This sets a generic test policy. {@link
* #POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES} will cause the {@code AppAuthenticator}
* to always return that a queried package has the expected signing identity as long as it is
* explicitly declared in your configuration; that is, the package must be declared in a
* {@code package} element within either an {@code expected-identity} or {@code permission}
* element. {@link #POLICY_DENY_ALL} will cause the {@code AppAuthenticator} to always return
* that a queried package does not have the expected signing identity regardless of its
* declaration. These two policies can be used to verify good path and error path for
* scenarios where the package names can be explicitly declared in the XML configuration.
* <p>{@code POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES} is the default policy when no
* other options are configured. When any of the other set methods (except for {@link
* #setUidForPackage(String, int)}) are invoked they will set the policy to {@link
* #POLICY_CUSTOM}.
* </li>
* <li>{@link #setSignatureAcceptedForPackage(String)} - This configures the {@code
* AppAuthenticator} to always return that the specified package has the expected signing
* identity. Note this still requires the {@code app-authenticator} have a path to verify
* the provided package; that is, the package must either be explicitly declared in a
* {@code package} element or fall under a {@code all-packages} element for the query being
* performed. This is to ensure that a package being verified during the test could also be
* successfully verified in production for the given query.
* </li>
* <li>{@link #setSigningIdentityForPackage(String, String)} - This sets an explicit
* signing identity for the provided package; the signing identity should be
* specified as the SHA-256 digest of the DER encoding of the signing certificate, similar
* to how digests are specified in the {@code app-authenticator} configuration file. While
* this can be used to set a signing identity to the expected value, this is more often
* used to set the signing identity to a value that should not be accepted. For instance, a
* test suite could have a test that verifies a key that is no longer trusted is never
* added back to the configuration file.
* </li>
* <li>{@link #setPackageNotInstalled(String)} - This configures the {@code AppAuthenticator}
* to treat the specified package as not installed on the device. Since a package that is not
* installed can result in a different return code from the {@code AppAuthenticator} methods
* this configuration can be used to verify an app's behavior when an expected app is not
* installed on the device.
* </li>
* <li>{@link #setUidForPackage(String, int)} - The {@code AppAuthenticator} will
* always verify the UID of the calling package matches the specified UID (or
* {@link Binder#getCallingUid()} if a UID is not specified). By default this test {@code
* AppAuthenticator} will use the result of {@code Binder#getCallingUid()} as the UID of all
* queried packages. This method can be used to verify the expected behavior when a calling
* package's UID does not match the expected UID.
* </li>
* </ul>
*/
// The purpose of this class is to build a configurable AppAuthenticator for tests so the builder
// is the top level class.
@SuppressLint("TopLevelBuilder")
public final class TestAppAuthenticatorBuilder {
private Context mContext;
private XmlPullParser mParser;
private @TestPolicy int mTestPolicy;
private TestAppSignatureVerifier.Builder mAppSignatureVerifierBuilder;
private TestAppAuthenticatorUtils.Builder mAppAuthenticatorUtilsBuilder;
/**
* Private constructor that should only be called by the static factory methods.
*
* @param context the context within which to create the {@link AppAuthenticator}
* @param parser an {@link XmlPullParser} containing the definitions for the
* permissions and expected identities based on package / expected signing
* certificate digests
*/
private TestAppAuthenticatorBuilder(Context context, XmlPullParser parser) {
mContext = context;
mParser = parser;
mTestPolicy = POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES;
mAppSignatureVerifierBuilder = new TestAppSignatureVerifier.Builder(context);
mAppAuthenticatorUtilsBuilder = new TestAppAuthenticatorUtils.Builder(mContext);
}
/**
* This test policy will cause the AppAuthenticator to return a successful signing identity for
* all packages explicitly declared in the XML configuration. This is the default policy used
* when a new {@code AppAuthenticator} is built without calling {@link
* #setSigningIdentityForPackage(String, String)}, {@link
* #setSignatureAcceptedForPackage(String)}, and {@link #setPackageNotInstalled(String)}.
*/
public static final int POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES = 1;
/**
* This test policy will cause the AppAuthenticator to return that the signing identity of
* the package does that match the expect identity from the XML configuration for all queried
* packages.
*/
public static final int POLICY_DENY_ALL = 2;
/**
* This test policy indicates that the caller will specify the expected results for each
* package individually. This is the default policy used when a new {@code TestAppAuthenticator}
* is built after calling any of the following:
* {@link #setSigningIdentityForPackage(String, String)}, {@link
* #setSignatureAcceptedForPackage(String)}, and {@link #setPackageNotInstalled(String)}.
* Once the policy has been set to this value it cannot be changed to any of the other policies.
*/
public static final int POLICY_CUSTOM = 3;
@IntDef(value = {
POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES,
POLICY_DENY_ALL,
POLICY_CUSTOM,
})
@Retention(RetentionPolicy.SOURCE)
@interface TestPolicy {
}
/**
* Returns a new {@link TestAppAuthenticatorBuilder} that can be used to create a new {@link
* AppAuthenticator} configured to behave as required for the test.
*
* @param context the context within which to create the {@link 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 this instance of the {@code TestAppAuthenticatorBuilder}
*/
// This is not a setter for the builder but instead a static factory method to obtain a new
// builder.
@SuppressLint("BuilderSetStyle")
@NonNull
public static TestAppAuthenticatorBuilder createFromResource(@NonNull Context context,
@XmlRes int xmlResource) {
Resources resources = context.getResources();
XmlPullParser parser = resources.getXml(xmlResource);
return new TestAppAuthenticatorBuilder(context, parser);
}
/**
* Returns a new {@link TestAppAuthenticatorBuilder} that can be used to create a new {@link
* AppAuthenticator} configured to behave as required for the test.
*
* @param context the context within which to create the {@link AppAuthenticator}
* @param xmlInputStream the XML {@link InputStream} containing the definitions for the
* permissions and expected identities based on packages / expected
* signing certificate digests
* @return this instance of the {@code TestAppAuthenticatorBuilder}
*/
// This is not a setter for the builder but instead a static factory method to obtain a new
// builder.
@SuppressLint("BuilderSetStyle")
@NonNull
public static TestAppAuthenticatorBuilder createFromInputStream(
@NonNull Context context,
@NonNull InputStream xmlInputStream)
throws AppAuthenticatorXmlException {
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 new TestAppAuthenticatorBuilder(context, parser);
}
/**
* Sets the policy to be used by the {@link AppAuthenticator} for the test.
*
* @param testPolicy the test policy to be used by the {@code AppAuthenticator{}
* @return this instance of the {@code TestAppAuthenticatorBuilder}
* @see #POLICY_SIGNATURE_ACCEPTED_FOR_DECLARED_PACKAGES
* @see #POLICY_DENY_ALL
* @see #POLICY_CUSTOM
*/
// The builder allows configuring other options that are not directly controlled by the
// AppAuthenticator.
@SuppressLint("MissingGetterMatchingBuilder")
public @NonNull TestAppAuthenticatorBuilder setTestPolicy(@TestPolicy int testPolicy) {
mTestPolicy = testPolicy;
return this;
}
/**
* Configures the resulting {@link AppAuthenticator} to always return that the signing
* identity matches the expected value when the specified {@code packageName} is queried.
*
* <p>Note, the specified {@code packageName} must be defined either explicitly via a
* {@code package} element or implicitly via a {@code all-packages} element; this ensures
* that the XML configuration is correct and that the specified package could be verified
* on device.
*
* @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 TestAppAuthenticatorBuilder}
*/
// The builder allows configuring other options that are not directly controlled by the
// AppAuthenticator.
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public TestAppAuthenticatorBuilder setSignatureAcceptedForPackage(
@NonNull String packageName) {
mTestPolicy = POLICY_CUSTOM;
mAppSignatureVerifierBuilder.setSignatureAcceptedForPackage(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 TestAppAuthenticatorBuilder}
*/
// The builder allows configuring other options that are not directly controlled by the
// AppAuthenticator.
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public TestAppAuthenticatorBuilder setSigningIdentityForPackage(
@NonNull String packageName,
@NonNull String certDigest) {
mTestPolicy = POLICY_CUSTOM;
mAppSignatureVerifierBuilder.setSigningIdentityForPackage(packageName,
AppAuthenticator.normalizeCertDigest(certDigest))
;
return this;
}
/**
* Sets the provided {@code uid} as the UID of the specified {@code packageName}.
*
* <p>This method can be used to verify the scenario where a calling package does not have the
* expected calling UID.
*
* @param packageName the name of the package that will be treated as having the provided uid
* @param uid the uid to use for the specified package
* @return this instance of the {@code TestAppAuthenticatorBuilder}
*/
// The builder allows configuring other options that are not directly controlled by the
// AppAuthenticator.
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public TestAppAuthenticatorBuilder setUidForPackage(@NonNull String packageName,
int uid) {
mAppAuthenticatorUtilsBuilder.setUidForPackage(packageName, uid);
return this;
}
/**
* Treats the provided {@code packageName} as not being installed by the resulting {@link
* AppAuthenticator}.
*
* @param packageName the name of the package to be treated as not installed
* @return this instance of the {@code TestAppAuthenticatorBuilder}
*/
// The builder allows configuring other options that are not directly controlled by the
// AppAuthenticator.
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public TestAppAuthenticatorBuilder setPackageNotInstalled(
@NonNull String packageName) {
mTestPolicy = POLICY_CUSTOM;
mAppAuthenticatorUtilsBuilder.setPackageNotInstalled(packageName);
mAppSignatureVerifierBuilder.setPackageNotInstalled(packageName);
return this;
}
/**
* Builds an {@link AppAuthenticator} with the specified config that can be injected to satisfy
* test requirements.
*
* @return a new {@code AppAuthenticator} that will respond to queries as configured
* @throws AppAuthenticatorXmlException if the provided XML config file 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 config file
*/
// This class is provided so that apps can inject a configurable AppAuthenticator for their
// tests, so it needs access to the restricted test APIs.
@SuppressLint("RestrictedApi")
@NonNull
public AppAuthenticator build() throws AppAuthenticatorXmlException, IOException {
// Obtain the config from the AppAuthenticator class to ensure that the provided XML is
// properly configured.
AppAuthenticator.AppAuthenticatorConfig config =
AppAuthenticator.createConfigFromParser(mParser);
// Configure the AppSignatureVerifier that will by the test AppAuthenticator.
mAppSignatureVerifierBuilder.setPermissionAllowMap(config.getPermissionAllowMap());
mAppSignatureVerifierBuilder.setExpectedIdentities(config.getExpectedIdentities());
mAppSignatureVerifierBuilder.setTestPolicy(mTestPolicy);
// Inject the AppSignatureVerifier and AppAuthenticatorUtils into the AppAuthenticator
// to configure it to behave as requested.
AppAuthenticator appAuthenticator = AppAuthenticator.createFromConfig(mContext, config);
appAuthenticator.setAppSignatureVerifier(mAppSignatureVerifierBuilder.build());
appAuthenticator.setAppAuthenticatorUtils(mAppAuthenticatorUtilsBuilder.build());
return appAuthenticator;
}
}