[DO NOT MERGE] Rollup changes from R

This change includes the following commits from internal R branch:

ecea8d8 Adding modern KeyChain tests
5f89de1 Expanding KeyChain service tests
bb63fca KeyChain: Adding user-selectability tests
83f9c58 Add tests for key generation & attestation
9b8aa0d KeyChain: Allow specifying UID of installed key
6e5d428 KeyChain: Delete obsolete test
3dbbb36 Fix KeyChain Robolectric tests
7cb0919 Show a dialog while certificates are loaded
e6435a7 KeyChain tests: Add TEST_MAPPING
be8a135 Throw IllegalArgumentException for invalid alias
114a9cc Add KEY_ALIAS_SELECTION_DENIED constant processing.
51a9b9f Log user ID when installing & removing CA certs
cbc62f2 Fix Cert Chooser dialog
3a91362 Inspect issuers when deciding on a certificate to display
03a1dc6 Fix theming of KeyChain activities
d1088a4 Fix cert selection dialog background
e18363f Protect against NullPointerException
02ff266 Add an exported flag in manifest
0a00ed1 Make cert selection prompt respect dark mode
fb9bae3 KeyChain: Log aliases from DPC
77192a0 Log certificate-related events
897f391 Revert "Add check for misprovisioned Pixel 2 device."

Bug: 161347472
Test: atest KeyChainTests
Change-Id: Id44c8cef275b9de7ed39d8033b19d662b44a638c
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 662b670..7f8f642 100755
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3,16 +3,23 @@
           package="com.android.keychain"
           android:sharedUserId="android.uid.system"
           >
+    <!-- Needed so KeyChainService on non-system user can write 
+         security logging events -->
+    <uses-permission android:name="android.permission.READ_LOGS"/>
+
     <application android:label="@string/app_name"
             android:allowBackup="false"
-            android:usesCleartextTraffic="false" >
-        <service android:name="com.android.keychain.KeyChainService">
+            android:usesCleartextTraffic="false"
+            android:theme="@android:style/Theme.DeviceDefault.DayNight">
+        <service android:name="com.android.keychain.KeyChainService"
+            android:exported="true">
             <intent-filter>
                 <action android:name="android.security.IKeyChainService"/>
             </intent-filter>
         </service>
         <activity android:name="com.android.keychain.KeyChainActivity"
-                  android:theme="@style/Transparent"
+                  android:theme="@style/KeyChainTransparent"
+                  android:exported="true"
 		  android:excludeFromRecents="true">
 	    <intent-filter>
 	        <action android:name="com.android.keychain.CHOOSER"/>
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..cd53830
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "KeyChainTests"
+    }
+  ]
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5e14261..2da7102 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -16,9 +16,6 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="app_name">Key Chain</string>
 
-    <!-- Dialog title when no certificates were found -->
-    <string name="title_no_certs">No certificates found</string>
-
     <!-- Dialog title when at least one certificate was found -->
     <string name="title_select_cert">Choose certificate</string>
 
@@ -37,4 +34,6 @@
     <!-- Label of button to send the deny the application certificate request -->
     <string name="deny_button">Deny</string>
 
+    <!-- Text to show while the KeyChain activity is loading certificates. -->
+    <string name="loading_certs_message">Loading certificates...</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 585b92e..2e1081b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -18,7 +18,7 @@
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <style name="Transparent">
+    <style name="KeyChainTransparent" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowIsFloating">true</item>
diff --git a/robotests/config/robolectric.properties b/robotests/config/robolectric.properties
index 41ee528..fab7251 100644
--- a/robotests/config/robolectric.properties
+++ b/robotests/config/robolectric.properties
@@ -1,2 +1 @@
-manifest=packages/apps/KeyChain/AndroidManifest.xml
 sdk=NEWEST_SDK
diff --git a/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java b/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java
index 6da16ab..8594e56 100644
--- a/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java
+++ b/robotests/src/com/android/keychain/KeyChainServiceRoboTest.java
@@ -130,7 +130,7 @@
         mKeyChain.installCaCertificate(TEST_CA.getBytes());
 
         verify(mockInjector, times(1)).writeSecurityEvent(
-                SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 1 /* success */, mSubject);
+                SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 1 /* success */, mSubject, 0);
     }
 
     @Test
@@ -147,7 +147,7 @@
         }
 
         verify(mockInjector, times(1)).writeSecurityEvent(
-                SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 0 /* failure */, mSubject);
+                SecurityLog.TAG_CERT_AUTHORITY_INSTALLED, 0 /* failure */, mSubject, 0);
     }
 
     @Test
@@ -159,7 +159,7 @@
         mKeyChain.deleteCaCertificate("alias");
 
         verify(mockInjector, times(1)).writeSecurityEvent(
-                SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 1 /* success */, mSubject);
+                SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 1 /* success */, mSubject, 0);
     }
 
     @Test
@@ -172,7 +172,7 @@
         mKeyChain.deleteCaCertificate("alias");
 
         verify(mockInjector, times(1)).writeSecurityEvent(
-                SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 0 /* failure */, mSubject);
+                SecurityLog.TAG_CERT_AUTHORITY_REMOVED, 0 /* failure */, mSubject, 0);
     }
 
     @Test
diff --git a/src/com/android/keychain/KeyChainActivity.java b/src/com/android/keychain/KeyChainActivity.java
index 4a4083a..078e377 100644
--- a/src/com/android/keychain/KeyChainActivity.java
+++ b/src/com/android/keychain/KeyChainActivity.java
@@ -18,10 +18,9 @@
 
 import android.annotation.NonNull;
 import android.app.Activity;
-import android.app.admin.IDevicePolicyManager;
 import android.app.AlertDialog;
-import android.app.Dialog;
 import android.app.PendingIntent;
+import android.app.admin.IDevicePolicyManager;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -29,8 +28,6 @@
 import android.content.res.Resources;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.security.Credentials;
@@ -38,29 +35,31 @@
 import android.security.KeyChain;
 import android.security.KeyStore;
 import android.util.Log;
+import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
-import android.widget.Button;
 import android.widget.ListView;
 import android.widget.RadioButton;
 import android.widget.TextView;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keychain.internal.KeyInfoProvider;
 import com.android.org.bouncycastle.asn1.x509.X509Name;
+
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
-import java.security.cert.Certificate;
+import java.io.IOException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.concurrent.ExecutionException;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 import javax.security.auth.x500.X500Principal;
@@ -68,10 +67,6 @@
 public class KeyChainActivity extends Activity {
     private static final String TAG = "KeyChain";
 
-    private static String KEY_STATE = "state";
-
-    private static final int REQUEST_UNLOCK = 1;
-
     private int mSenderUid;
 
     private PendingIntent mSender;
@@ -82,6 +77,10 @@
     // be done on the UI thread.
     private KeyStore mKeyStore = KeyStore.getInstance();
 
+    // A dialog to show the user while the KeyChain Activity is loading the
+    // certificates.
+    AlertDialog mLoadingDialog;
+
     @Override public void onResume() {
         super.onResume();
 
@@ -104,6 +103,16 @@
         chooseCertificate();
     }
 
+    private void showLoadingDialog() {
+        final Context themedContext = new ContextThemeWrapper(
+                this, com.android.internal.R.style.Theme_Translucent_NoTitleBar);
+        mLoadingDialog = new AlertDialog.Builder(themedContext)
+                .setTitle(R.string.app_name)
+                .setMessage(R.string.loading_certs_message)
+                .create();
+        mLoadingDialog.show();
+    }
+
     private void chooseCertificate() {
         // Start loading the set of certs to choose from now- if device policy doesn't return an
         // alias, having aliases loading already will save some time waiting for UI to start.
@@ -124,14 +133,21 @@
             }
         };
 
+        Log.i(TAG, String.format("Requested by app uid %d to provide a private key alias",
+                mSenderUid));
+
         String[] keyTypes = getIntent().getStringArrayExtra(KeyChain.EXTRA_KEY_TYPES);
         if (keyTypes == null) {
             keyTypes = new String[]{};
         }
+        Log.i(TAG, String.format("Key types specified: %s", Arrays.toString(keyTypes)));
+
         ArrayList<byte[]> issuers = (ArrayList<byte[]>) getIntent().getSerializableExtra(
                 KeyChain.EXTRA_ISSUERS);
         if (issuers == null) {
             issuers = new ArrayList<byte[]>();
+        } else {
+            Log.i(TAG, "Issuers specified, will be listed later.");
         }
 
         final AliasLoader loader = new AliasLoader(mKeyStore, this, keyInfoProvider,
@@ -140,7 +156,9 @@
 
         final IKeyChainAliasCallback.Stub callback = new IKeyChainAliasCallback.Stub() {
             @Override public void alias(String alias) {
-                // Use policy-suggested alias if provided
+                Log.i(TAG, String.format("Alias provided by device policy client: %s", alias));
+                // Use policy-suggested alias if provided or abort further actions if alias is
+                // KeyChain.KEY_ALIAS_SELECTION_DENIED
                 if (alias != null) {
                     finishWithAliasFromPolicy(alias);
                     return;
@@ -160,17 +178,24 @@
                  * the dialog. This is in line with what other operating systems do.
                  */
                 if (!certAdapter.hasKeysToChoose()) {
+                    Log.i(TAG, "No keys to choose from");
                     finish(null);
                     return;
                 }
                 runOnUiThread(new Runnable() {
                     @Override public void run() {
+                        if (mLoadingDialog != null) {
+                            mLoadingDialog.dismiss();
+                            mLoadingDialog = null;
+                        }
                         displayCertChooserDialog(certAdapter);
                     }
                 });
             }
         };
 
+        // Show a dialog to the user to indicate long-running task.
+        showLoadingDialog();
         // Give a profile or device owner the chance to intercept the request, if a private key
         // access listener is registered with the DevicePolicyManagerService.
         IDevicePolicyManager devicePolicyManager = IDevicePolicyManager.Stub.asInterface(
@@ -192,7 +217,7 @@
     }
 
     @VisibleForTesting
-    static class CertificateParametersFilter {
+    public static class CertificateParametersFilter {
         private final KeyStore mKeyStore;
         private final List<String> mKeyTypes;
         private final List<X500Principal> mIssuers;
@@ -217,14 +242,19 @@
             X509Certificate cert = loadCertificate(mKeyStore, alias);
             // If there's no certificate associated with the alias, skip.
             if (cert == null) {
+                Log.i(TAG, String.format("No certificate associated with alias %s", alias));
                 return false;
             }
-            Log.i(TAG, String.format("Inspecting certificate %s aliased with %s",
-                        cert.getSubjectDN().getName(), alias));
+            List<X509Certificate> certChain = new ArrayList(loadCertificateChain(mKeyStore, alias));
+            Log.i(TAG, String.format("Inspecting certificate %s aliased with %s, chain length %d",
+                        cert.getSubjectDN().getName(), alias, certChain.size()));
 
             // If the caller has provided a list of key types to restrict the certificates
             // offered for selection, skip this alias if the key algorithm is not in that
             // list.
+            // Note that the end entity (leaf) certificate's public key has to be compatible
+            // with the specified key algorithm, not any one of the chain (see RFC5246
+            // section 7.4.6)
             String keyAlgorithm = cert.getPublicKey().getAlgorithm();
             Log.i(TAG, String.format("Certificate key algorithm: %s", keyAlgorithm));
             if (!mKeyTypes.isEmpty() && !mKeyTypes.contains(keyAlgorithm)) {
@@ -232,19 +262,32 @@
             }
 
             // If the caller has provided a list of issuers to restrict the certificates
-            // offered for selection, skip this alias if the issuer is not in that list.
-            X500Principal issuer = cert.getIssuerX500Principal();
-            Log.i(TAG, String.format("Certificate issuer: %s", issuer.getName()));
-            if (!mIssuers.isEmpty() && !mIssuers.contains(issuer)) {
+            // offered for selection, skip this alias if none of the issuers in the client
+            // certificate chain is in that list.
+            List<X500Principal> chainIssuers = new ArrayList();
+            chainIssuers.add(cert.getIssuerX500Principal());
+            for (X509Certificate intermediate : certChain) {
+                X500Principal subject = intermediate.getSubjectX500Principal();
+                Log.i(TAG, String.format("Subject of intermediate in client certificate chain: %s",
+                            subject.getName()));
+                // Collect the subjects of all the intermediates, as the RFC specifies that
+                // "one of the certificates in the certificate chain SHOULD be issued by one of
+                // the listed CAs."
+                chainIssuers.add(subject);
+            }
+
+            if (!mIssuers.isEmpty()) {
+                for (X500Principal issuer : chainIssuers) {
+                    if (mIssuers.contains(issuer)) {
+                        Log.i(TAG, String.format("Requested issuer found: %s", issuer));
+                        return true;
+                    }
+                }
                 return false;
             }
 
             return true;
         }
-
-        public boolean areIssuersOrKeyTypesSpecified() {
-            return !(mIssuers.isEmpty() && mKeyTypes.isEmpty());
-        }
     }
 
     @VisibleForTesting
@@ -271,66 +314,65 @@
             return new CertificateAdapter(mKeyStore, mContext,
                     rawAliasList.stream().filter(mInfoProvider::isUserSelectable)
                     .filter(mCertificateFilter::shouldPresentCertificate)
-                    .sorted().collect(Collectors.toList()),
-                    mCertificateFilter.areIssuersOrKeyTypesSpecified());
+                    .sorted().collect(Collectors.toList()));
         }
     }
 
     private void displayCertChooserDialog(final CertificateAdapter adapter) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        if (adapter.mAliases.isEmpty()) {
+            Log.w(TAG, "Should not be asked to display the cert chooser without aliases.");
+            finish(null);
+            return;
+        }
 
-        boolean empty = adapter.mAliases.isEmpty();
-        int negativeLabel = empty ? android.R.string.cancel : R.string.deny_button;
-        builder.setNegativeButton(negativeLabel, new DialogInterface.OnClickListener() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setNegativeButton(R.string.deny_button, new DialogInterface.OnClickListener() {
             @Override public void onClick(DialogInterface dialog, int id) {
                 dialog.cancel(); // will cause OnDismissListener to be called
             }
         });
 
-        String title;
         int selectedItem = -1;
         Resources res = getResources();
-        if (empty) {
-            title = res.getString(R.string.title_no_certs);
-        } else {
-            title = res.getString(R.string.title_select_cert);
-            String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
+        String alias = getIntent().getStringExtra(KeyChain.EXTRA_ALIAS);
 
-            if (alias != null) {
-                // if alias was requested, set it if found
-                int adapterPosition = adapter.mAliases.indexOf(alias);
-                if (adapterPosition != -1) {
-                    // increase by 1 to account for item 0 being the header.
-                    selectedItem = adapterPosition + 1;
-                }
-            } else if (adapter.mAliases.size() == 1) {
-                // if only one choice, preselect it
-                selectedItem = 1;
+        if (alias != null) {
+            // if alias was requested, set it if found
+            int adapterPosition = adapter.mAliases.indexOf(alias);
+            if (adapterPosition != -1) {
+                // increase by 1 to account for item 0 being the header.
+                selectedItem = adapterPosition + 1;
             }
-
-            builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() {
-                @Override public void onClick(DialogInterface dialog, int id) {
-                    if (dialog instanceof AlertDialog) {
-                        ListView lv = ((AlertDialog) dialog).getListView();
-                        int listViewPosition = lv.getCheckedItemPosition();
-                        int adapterPosition = listViewPosition-1;
-                        String alias = ((adapterPosition >= 0)
-                                        ? adapter.getItem(adapterPosition)
-                                        : null);
-                        finish(alias);
-                    } else {
-                        Log.wtf(TAG, "Expected AlertDialog, got " + dialog, new Exception());
-                        finish(null);
-                    }
-                }
-            });
+        } else if (adapter.mAliases.size() == 1) {
+            // if only one choice, preselect it
+            selectedItem = 1;
         }
-        builder.setTitle(title);
+
+        builder.setPositiveButton(R.string.allow_button, new DialogInterface.OnClickListener() {
+            @Override public void onClick(DialogInterface dialog, int id) {
+                if (dialog instanceof AlertDialog) {
+                    ListView lv = ((AlertDialog) dialog).getListView();
+                    int listViewPosition = lv.getCheckedItemPosition();
+                    int adapterPosition = listViewPosition-1;
+                    String alias = ((adapterPosition >= 0)
+                                    ? adapter.getItem(adapterPosition)
+                                    : null);
+                    Log.i(TAG, String.format("User chose: %s", alias));
+                    finish(alias);
+                } else {
+                    Log.wtf(TAG, "Expected AlertDialog, got " + dialog, new Exception());
+                    finish(null);
+                }
+            }
+        });
+
+        builder.setTitle(res.getString(R.string.title_select_cert));
         builder.setSingleChoiceItems(adapter, selectedItem, null);
         final AlertDialog dialog = builder.create();
 
         // Show text above the list to explain what the certificate will be used for.
-        TextView contextView = (TextView) View.inflate(this, R.layout.cert_chooser_header, null);
+        TextView contextView = (TextView) View.inflate(
+                this, R.layout.cert_chooser_header, null);
 
         final ListView lv = dialog.getListView();
         lv.addHeaderView(contextView, null, false);
@@ -395,15 +437,12 @@
         private final List<String> mSubjects = new ArrayList<String>();
         private final KeyStore mKeyStore;
         private final Context mContext;
-        private final boolean mIssuersOrKeyTypesSpecified;
 
-        private CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases,
-                boolean issuersOrKeyTypesSpecified) {
+        private CertificateAdapter(KeyStore keyStore, Context context, List<String> aliases) {
             mAliases = aliases;
             mSubjects.addAll(Collections.nCopies(aliases.size(), (String) null));
             mKeyStore = keyStore;
             mContext = context;
-            mIssuersOrKeyTypesSpecified = issuersOrKeyTypesSpecified;
         }
         @Override public int getCount() {
             return mAliases.size();
@@ -493,7 +532,12 @@
     }
 
     private void finish(String alias, boolean isAliasFromPolicy) {
-        if (alias == null) {
+        if (mLoadingDialog != null) {
+            mLoadingDialog.dismiss();
+            mLoadingDialog = null;
+        }
+        if (alias == null || alias.equals(KeyChain.KEY_ALIAS_SELECTION_DENIED)) {
+            alias = null;
             setResult(RESULT_CANCELED);
         } else {
             Intent result = new Intent();
@@ -565,6 +609,7 @@
     private static X509Certificate loadCertificate(KeyStore keyStore, String alias) {
         byte[] bytes = keyStore.get(Credentials.USER_CERTIFICATE + alias);
         if (bytes == null) {
+            Log.i(TAG, String.format("Missing user certificate for key alias %s", alias));
             return null;
         }
         InputStream in = new ByteArrayInputStream(bytes);
@@ -576,4 +621,19 @@
             return null;
         }
     }
+
+    private static List<X509Certificate> loadCertificateChain(KeyStore keyStore, String alias) {
+        byte[] chainBytes = keyStore.get(Credentials.CA_CERTIFICATE + alias);
+        if (chainBytes == null) {
+            Log.i(TAG, String.format("Missing certificate chain for key alias %s", alias));
+            return Collections.emptyList();
+        }
+
+        try {
+            return Credentials.convertFromPem(chainBytes);
+        } catch (IOException | CertificateException e) {
+            Log.w(TAG, String.format("Error parsing certificate chain for alias %s", alias), e);
+            return Collections.emptyList();
+        }
+    }
 }
diff --git a/src/com/android/keychain/KeyChainService.java b/src/com/android/keychain/KeyChainService.java
index b226743..f348859 100644
--- a/src/com/android/keychain/KeyChainService.java
+++ b/src/com/android/keychain/KeyChainService.java
@@ -19,6 +19,7 @@
 import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_INSTALLED;
 import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_REMOVED;
 
+import android.annotation.Nullable;
 import android.app.BroadcastOptions;
 import android.app.IntentService;
 import android.app.admin.SecurityLog;
@@ -43,6 +44,7 @@
 import android.security.keystore.ParcelableKeyGenParameterSpec;
 import android.security.keystore.StrongBoxUnavailableException;
 import android.text.TextUtils;
+import android.util.Base64;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -64,8 +66,11 @@
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.security.auth.x500.X500Principal;
 
@@ -73,6 +78,8 @@
 
     private static final String TAG = "KeyChain";
     private static final String CERT_INSTALLER_PACKAGE = "com.android.certinstaller";
+    private final Set<Integer> ALLOWED_UIDS = Collections.unmodifiableSet(
+            new HashSet(Arrays.asList(KeyStore.UID_SELF, Process.WIFI_UID)));
 
     /** created in onCreate(), closed in onDestroy() */
     private GrantsDatabase mGrantsDb;
@@ -135,6 +142,7 @@
 
             final String keystoreAlias = Credentials.USER_PRIVATE_KEY + alias;
             final int uid = mInjector.getCallingUid();
+            Log.i(TAG, String.format("UID %d will be granted access to %s", uid, keystoreAlias));
             return mKeyStore.grant(keystoreAlias, uid);
         }
 
@@ -160,6 +168,8 @@
         @Override public void setUserSelectable(String alias, boolean isUserSelectable) {
             validateAlias(alias);
             checkSystemCaller();
+            Log.i(TAG, String.format("Marking certificate %s as user-selectable: %b", alias,
+                    isUserSelectable));
             mGrantsDb.setIsUserSelectable(alias, isUserSelectable);
         }
 
@@ -168,6 +178,14 @@
             checkSystemCaller();
             final KeyGenParameterSpec spec = parcelableSpec.getSpec();
             final String alias = spec.getKeystoreAlias();
+
+            Log.i(TAG, String.format("About to generate key with alias %s, algorithm %s",
+                    alias, algorithm));
+
+            if (KeyChain.KEY_ALIAS_SELECTION_DENIED.equals(alias)) {
+                throw new IllegalArgumentException("The alias specified for the key denotes "
+                        + "a reserved value and cannot be used to name a key");
+            }
             // Validate the alias here to avoid relying on KeyGenParameterSpec c'tor preventing
             // the creation of a KeyGenParameterSpec instance with a non-empty alias.
             if (TextUtils.isEmpty(alias) || spec.getUid() != KeyStore.UID_SELF) {
@@ -227,7 +245,13 @@
                 return KeyChain.KEY_ATTESTATION_MISSING_CHALLENGE;
             }
 
-            KeymasterArguments attestArgs;
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("About to attest key alias %s, challenge %s, flags %s",
+                        alias, Base64.encodeToString(attestationChallenge, Base64.NO_WRAP),
+                        Arrays.toString(idAttestationFlags)));
+            }
+
+            final KeymasterArguments attestArgs;
             try {
                 attestArgs = AttestationUtils.prepareAttestationArguments(
                         mContext, idAttestationFlags, attestationChallenge);
@@ -235,31 +259,6 @@
                 Log.e(TAG, "Failed collecting attestation data", e);
                 return KeyChain.KEY_ATTESTATION_CANNOT_COLLECT_DATA;
             }
-            int errorCode = checkKeyChainStatus(alias, attestationChain, attestArgs);
-            if (errorCode == KeyChain.KEY_ATTESTATION_CANNOT_ATTEST_IDS) {
-                // b/69471841: id attestation might fail due to incorrect provisioning of device
-                try {
-                    attestArgs =
-                            AttestationUtils.prepareAttestationArgumentsIfMisprovisioned(
-                            mContext, idAttestationFlags, attestationChallenge);
-                    if (attestArgs == null) {
-                        return errorCode;
-                    }
-                } catch (DeviceIdAttestationException e) {
-                    Log.e(TAG, "Failed collecting attestation data "
-                            + "during second attempt on misprovisioned device", e);
-                    return KeyChain.KEY_ATTESTATION_CANNOT_COLLECT_DATA;
-                }
-            }
-
-            return checkKeyChainStatus(alias, attestationChain, attestArgs);
-        }
-
-        private int checkKeyChainStatus(
-                String alias,
-                KeymasterCertificateChain attestationChain,
-                KeymasterArguments attestArgs) {
-
             final String keystoreAlias = Credentials.USER_PRIVATE_KEY + alias;
             final int errorCode = mKeyStore.attestKey(keystoreAlias, attestArgs, attestationChain);
             if (errorCode != KeyStore.NO_ERROR) {
@@ -299,6 +298,12 @@
                     Log.e(TAG, "Failed to remove CA certificate chain for alias " + alias);
                 }
             }
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Set certificate for key alias %s : user %s CA chain: %s",
+                        alias, emptyOrBase64Encoded(userCertificate),
+                        emptyOrBase64Encoded(userCertificateChain)));
+            }
             broadcastKeychainChange();
             broadcastLegacyStorageChange();
             return true;
@@ -330,24 +335,35 @@
             String subjectForAudit = null;
             try {
                 final X509Certificate cert = parseCertificate(caCertificate);
-                if (mInjector.isSecurityLoggingEnabled()) {
-                    subjectForAudit =
+                final boolean isSecurityLoggingEnabled = mInjector.isSecurityLoggingEnabled();
+                final boolean isDebugLoggable = Log.isLoggable(TAG, Log.DEBUG);
+                if (isSecurityLoggingEnabled || isDebugLoggable) {
+                    final String subject =
                             cert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
+                    if (isDebugLoggable) {
+                        Log.d(TAG, String.format("Installing CA certificate: %s", subject));
+                    }
+                    if (isSecurityLoggingEnabled) {
+                        subjectForAudit = subject;
+                    }
                 }
                 synchronized (mTrustedCertificateStore) {
                     mTrustedCertificateStore.installCertificate(cert);
                     alias = mTrustedCertificateStore.getCertificateAlias(cert);
                 }
             } catch (IOException | CertificateException e) {
+                Log.w(TAG, "Failed installing CA certificate", e);
                 if (subjectForAudit != null) {
                     mInjector.writeSecurityEvent(
-                            TAG_CERT_AUTHORITY_INSTALLED, 0 /*result*/, subjectForAudit);
+                            TAG_CERT_AUTHORITY_INSTALLED, 0 /*result*/, subjectForAudit,
+                            UserHandle.myUserId());
                 }
                 throw new IllegalStateException(e);
             }
             if (subjectForAudit != null) {
                 mInjector.writeSecurityEvent(
-                        TAG_CERT_AUTHORITY_INSTALLED, 1 /*result*/, subjectForAudit);
+                        TAG_CERT_AUTHORITY_INSTALLED, 1 /*result*/, subjectForAudit,
+                        UserHandle.myUserId());
             }
             broadcastLegacyStorageChange();
             broadcastTrustStoreChange();
@@ -360,22 +376,60 @@
          * @param privateKey The private key associated with the client certificate
          * @param userCertificate The client certificate to be installed
          * @param userCertificateChain The rest of the chain for the client certificate
-         * @param alias The alias under which the key pair is installed
+         * @param alias The alias under which the key pair is installed. It is invalid to pass
+         *              {@code KeyChain.KEY_ALIAS_SELECTION_DENIED}.
+         * @param uid Can be only one of two values: Either {@code KeyStore.UID_SELF} to indicate
+         *            installation into the current user's system Keystore instance, or
+         *            {@code Process.WIFI_UID} to indicate installation into the main user's
+         *            WiFi Keystore instance. It is only valid to pass {@code Process.WIFI_UID} to
+         *            the KeyChain service on user 0.
          * @return Whether the operation succeeded or not.
          */
-        @Override public boolean installKeyPair(byte[] privateKey, byte[] userCertificate,
-                byte[] userCertificateChain, String alias) {
+        @Override public boolean installKeyPair(@Nullable byte[] privateKey,
+                @Nullable byte[] userCertificate, @Nullable byte[] userCertificateChain,
+                String alias, int uid) {
             checkCertInstallerOrSystemCaller();
+            if (KeyChain.KEY_ALIAS_SELECTION_DENIED.equals(alias)) {
+                throw new IllegalArgumentException("The alias specified for the key denotes "
+                        + "a reserved value and cannot be used to name a key");
+            }
+            if (!ALLOWED_UIDS.contains(uid)) {
+                Log.e(TAG,
+                        String.format("Installing alias %s as UID %d is now allowed.", alias, uid));
+                return false;
+            }
+
+            if (privateKey == null && userCertificate == null && userCertificateChain == null) {
+                Log.e(TAG, String.format("Nothing to install for alias %s", alias));
+                return false;
+            }
+
+            if (uid == Process.WIFI_UID && UserHandle.myUserId() != UserHandle.USER_SYSTEM) {
+                Log.e(TAG, String.format(
+                        "Installation into the WiFi Keystore should be called from the primary "
+                                + "user, not user %d",
+                        UserHandle.myUserId()));
+                return false;
+            }
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Installing certificate and key to alias %s to uid %d: "
+                                + "user cert %s CA chain: %s", alias, uid,
+                                emptyOrBase64Encoded(userCertificate),
+                                emptyOrBase64Encoded(userCertificateChain)));
+            }
+
             if (!removeKeyPair(alias)) {
                 return false;
             }
-            if (!mKeyStore.importKey(Credentials.USER_PRIVATE_KEY + alias, privateKey, -1,
-                    KeyStore.FLAG_NONE)) {
+            if (privateKey != null && !mKeyStore.importKey(
+                    Credentials.USER_PRIVATE_KEY + alias, privateKey, uid, KeyStore.FLAG_NONE)) {
                 Log.e(TAG, "Failed to import private key " + alias);
                 return false;
             }
-            if (!mKeyStore.put(Credentials.USER_CERTIFICATE + alias, userCertificate, -1,
-                    KeyStore.FLAG_NONE)) {
+            if (userCertificate != null &&
+                    !mKeyStore.put(Credentials.USER_CERTIFICATE + alias, userCertificate,
+                        uid, KeyStore.FLAG_NONE)) {
                 Log.e(TAG, "Failed to import user certificate " + userCertificate);
                 if (!mKeyStore.delete(Credentials.USER_PRIVATE_KEY + alias)) {
                     Log.e(TAG, "Failed to delete private key after certificate importing failed");
@@ -383,7 +437,7 @@
                 return false;
             }
             if (userCertificateChain != null && userCertificateChain.length > 0) {
-                if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, userCertificateChain, -1,
+                if (!mKeyStore.put(Credentials.CA_CERTIFICATE + alias, userCertificateChain, uid,
                         KeyStore.FLAG_NONE)) {
                     Log.e(TAG, "Failed to import certificate chain" + userCertificateChain);
                     if (!removeKeyPair(alias)) {
@@ -441,6 +495,7 @@
             // only Settings should be able to delete
             checkSystemCaller();
             boolean ok = true;
+            Log.i(TAG, String.format("Deleting CA certificate %s", alias));
             synchronized (mTrustedCertificateStore) {
                 ok = deleteCertificateEntry(alias);
             }
@@ -463,14 +518,16 @@
                 mTrustedCertificateStore.deleteCertificateEntry(alias);
                 if (subjectForAudit != null) {
                     mInjector.writeSecurityEvent(
-                            TAG_CERT_AUTHORITY_REMOVED, 1 /*result*/, subjectForAudit);
+                            TAG_CERT_AUTHORITY_REMOVED, 1 /*result*/, subjectForAudit,
+                            UserHandle.myUserId());
                 }
                 return true;
             } catch (IOException | CertificateException e) {
                 Log.w(TAG, "Problem removing CA certificate " + alias, e);
                 if (subjectForAudit != null) {
                     mInjector.writeSecurityEvent(
-                            TAG_CERT_AUTHORITY_REMOVED, 0 /*result*/, subjectForAudit);
+                            TAG_CERT_AUTHORITY_REMOVED, 0 /*result*/, subjectForAudit,
+                            UserHandle.myUserId());
                 }
                 return false;
             }
@@ -490,7 +547,7 @@
         }
 
         private boolean isCallerWithSystemUid() {
-            return UserHandle.isSameApp(Binder.getCallingUid(), Process.SYSTEM_UID);
+            return UserHandle.isSameApp(mInjector.getCallingUid(), Process.SYSTEM_UID);
         }
 
         private String callingPackage() {
@@ -622,6 +679,13 @@
         }
     }
 
+    private static String emptyOrBase64Encoded(byte[] cert) {
+        if (cert == null) {
+            return "";
+        }
+        return Base64.encodeToString(cert, Base64.NO_WRAP);
+    }
+
     @VisibleForTesting
     void setInjector(Injector injector) {
         mInjector = injector;
diff --git a/support/Android.bp b/support/Android.bp
index 5743ced..426799c 100644
--- a/support/Android.bp
+++ b/support/Android.bp
@@ -17,7 +17,7 @@
     srcs: ["src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl"],
 }
 
-android_test {
+android_test_helper_app {
     name: "KeyChainTestsSupport",
     srcs: ["src/**/*.java"],
     platform_apis: true,
@@ -26,4 +26,7 @@
         "junit",
     ],
     certificate: "platform",
+    test_suites: [
+        "general-tests",
+    ]
 }
diff --git a/support/AndroidManifest.xml b/support/AndroidManifest.xml
index 934660a..11e7f55 100644
--- a/support/AndroidManifest.xml
+++ b/support/AndroidManifest.xml
@@ -18,7 +18,8 @@
           package="com.android.keychain.tests.support"
 	  android:sharedUserId="android.uid.system">
     <application android:process="system">
-        <service android:name="com.android.keychain.tests.support.KeyChainServiceTestSupport">
+        <service android:name="com.android.keychain.tests.support.KeyChainServiceTestSupport"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.keychain.tests.support.IKeyChainServiceTestSupport"/>
             </intent-filter>
diff --git a/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl b/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
index 4964a96..c62c971 100644
--- a/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
+++ b/support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
@@ -16,6 +16,7 @@
 package com.android.keychain.tests.support;
 
 import android.accounts.Account;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
 
 /**
  * Service that runs as the system user for the use of the
@@ -36,4 +37,10 @@
     boolean keystoreImportKey(String key, in byte[] value);
     void revokeAppPermission(int uid, String alias);
     void grantAppPermission(int uid, String alias);
+    boolean installKeyPair(in byte[] privateKey, in byte[] userCert, in byte[] certChain, String alias);
+    boolean removeKeyPair(String alias);
+    void setUserSelectable(String alias, boolean isUserSelectable);
+    int generateKeyPair(in String algorithm, in ParcelableKeyGenParameterSpec spec);
+    int attestKey(in String alias, in byte[] challenge, in int[] idAttestationFlags);
+    boolean setKeyPairCertificate(String alias, in byte[] userCert, in byte[] certChain);
 }
diff --git a/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java b/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
index edc35a3..5f688f8 100644
--- a/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
+++ b/support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
@@ -20,8 +20,11 @@
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.security.IKeyChainService;
 import android.security.KeyChain;
 import android.security.KeyStore;
+import android.security.keymaster.KeymasterCertificateChain;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
 import android.util.Log;
 
 public class KeyChainServiceTestSupport extends Service {
@@ -65,6 +68,58 @@
             blockingSetGrantPermission(uid, alias, true);
         }
 
+        @Override public boolean installKeyPair(
+                byte[] privateKey, byte[] userCert, byte[] certChain, String alias)
+                throws RemoteException {
+            Log.d(TAG, "installKeyPair");
+            return performBlockingKeyChainCall(keyChainService -> {
+                return keyChainService.installKeyPair(
+                        privateKey, userCert, certChain, alias, KeyStore.UID_SELF);
+            });
+        }
+
+        @Override public boolean removeKeyPair(String alias) throws RemoteException {
+            Log.d(TAG, "removeKeyPair");
+            return performBlockingKeyChainCall(keyChainService -> {
+                return keyChainService.removeKeyPair(alias);
+            });
+        }
+
+        @Override public void setUserSelectable(String alias, boolean isUserSelectable)
+                throws RemoteException {
+            Log.d(TAG, "setUserSelectable");
+            KeyChainAction<Void> action = service -> {
+                service.setUserSelectable(alias, isUserSelectable);
+                return null;
+            };
+            performBlockingKeyChainCall(action);
+        }
+
+        @Override public int generateKeyPair(String algorithm, ParcelableKeyGenParameterSpec spec)
+                throws RemoteException {
+            return performBlockingKeyChainCall(keyChainService -> {
+                return keyChainService.generateKeyPair(algorithm, spec);
+            });
+        }
+
+        @Override public int attestKey(
+                String alias, byte[] attestationChallenge,
+                int[] idAttestationFlags) throws RemoteException {
+            KeymasterCertificateChain attestationChain = new KeymasterCertificateChain();
+            return performBlockingKeyChainCall(keyChainService -> {
+                return keyChainService.attestKey(alias, attestationChallenge, idAttestationFlags,
+                        attestationChain);
+            });
+        }
+
+        @Override public boolean setKeyPairCertificate(String alias, byte[] userCertificate,
+                byte[] userCertificateChain) throws RemoteException {
+            return performBlockingKeyChainCall(keyChainService -> {
+                return keyChainService.setKeyPairCertificate(alias, userCertificate,
+                        userCertificateChain);
+            });
+        }
+
         /**
          * Binds to the KeyChainService and requests that permission for the sender to
          * access the specified alias is granted/revoked.
@@ -74,25 +129,32 @@
          */
         private void blockingSetGrantPermission(int senderUid, String alias, boolean value)
                 throws RemoteException {
-            KeyChain.KeyChainConnection connection = null;
-            try {
-                connection = KeyChain.bind(KeyChainServiceTestSupport.this);
-                connection.getService().setGrant(senderUid, alias, value);
-            } catch (InterruptedException e) {
-                // should never happen. if it does we will not grant the requested permission
-                Log.e(TAG, "interrupted while granting access");
-                Thread.currentThread().interrupt();
-            } finally {
-                if (connection != null) {
-                    connection.close();
-                }
-            }
+            KeyChainAction<Void> action = new KeyChainAction<Void>() {
+                public Void run(IKeyChainService service) throws RemoteException {
+                    service.setGrant(senderUid, alias, value);
+                    return null;
+                };
+            };
+            performBlockingKeyChainCall(action);
         }
     };
 
     @Override public IBinder onBind(Intent intent) {
-        if (IKeyChainServiceTestSupport.class.getName().equals(intent.getAction())) {
-            return mIKeyChainServiceTestSupport;
+        return mIKeyChainServiceTestSupport;
+    }
+
+    public interface KeyChainAction<T> {
+        T run(IKeyChainService service) throws RemoteException;
+    }
+
+    private <T> T performBlockingKeyChainCall(KeyChainAction<T> action) throws RemoteException {
+        try (KeyChain.KeyChainConnection connection =
+        KeyChain.bind(KeyChainServiceTestSupport.this)) {
+            return action.run(connection.getService());
+        } catch (InterruptedException e) {
+            // should never happen.
+            Log.e(TAG, "interrupted while running action");
+            Thread.currentThread().interrupt();
         }
         return null;
     }
diff --git a/tests/Android.bp b/tests/Android.bp
index 52baddd..823d7a1 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -20,6 +20,23 @@
         "com.android.keychain.tests.support",
         "core-tests-support",
         "mockwebserver",
+        "platform-test-annotations",
         "junit",
+        "mockito-target-minus-junit4",
+        "androidx.test.ext.junit",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "testng",
+        "truth-prebuilt",
     ],
+    libs: [
+        "android.test.base",
+    ],
+    test_suites: ["general-tests"],
+    required: [
+        "KeyChainTestsSupport",
+    ],
+
+    instrumentation_for: "KeyChain",
+    certificate: "platform",
 }
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index fa9cd55..754b76b 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -15,7 +15,8 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.keychain.tests">
+    package="com.android.keychain.tests"
+    android:sharedUserId="android.uid.system">
 
     <uses-permission android:name="android.permission.INTERNET"/>
 
@@ -38,20 +39,24 @@
         adb shell am start -n com.android.keychain.tests/com.android.keychain.tests.KeyChainTestActivity
     -->
     <application>
-        <service android:name="com.android.keychain.tests.KeyChainServiceTest">
-            <intent-filter>
-                <action android:name="com.android.keychain.tests.KeyChainServiceTest"/>
-            </intent-filter>
-        </service>
-        <activity android:name="com.android.keychain.tests.KeyChainTestActivity">
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="com.android.keychain.tests.KeyChainTestActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.keychain.tests.KeyChainTestActivity"/>
             </intent-filter>
         </activity>
-        <activity android:name="com.android.keychain.tests.KeyChainSocketTestActivity">
+        <activity android:name="com.android.keychain.tests.KeyChainSocketTestActivity"
+            android:exported="true">
             <intent-filter>
                 <action android:name="com.android.keychain.tests.KeyChainSocketTestActivity"/>
             </intent-filter>
         </activity>
     </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.keychain"
+        android:label="KeyChain Service Tests"
+        android:sharedUserId="android.uid.system"/>
 </manifest>
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
new file mode 100644
index 0000000..8f93cc4
--- /dev/null
+++ b/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Config for KeyChain service tests">
+    <option name="not-shardable" value="true" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="KeyChainTestsSupport.apk" />
+        <option name="test-file-name" value="KeyChainTests.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="package" value="com.android.keychain.tests" />
+        <option name="hidden-api-checks" value="false" />
+        <option name="isolated-storage" value="false" />
+    </test>
+</configuration>
+
diff --git a/tests/src/com/android/keychain/tests/BasicKeyChainServiceTest.java b/tests/src/com/android/keychain/tests/BasicKeyChainServiceTest.java
new file mode 100644
index 0000000..9d69482
--- /dev/null
+++ b/tests/src/com/android/keychain/tests/BasicKeyChainServiceTest.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2019 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 com.android.keychain.tests;
+
+import static android.os.Process.WIFI_UID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.ConditionVariable;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.platform.test.annotations.LargeTest;
+import android.security.Credentials;
+import android.security.IKeyChainService;
+import android.security.KeyChain;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.ParcelableKeyGenParameterSpec;
+import android.util.Log;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+import com.android.keychain.tests.support.IKeyChainServiceTestSupport;
+import java.io.IOException;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import libcore.java.security.TestKeyStore;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class BasicKeyChainServiceTest {
+    private static final String TAG = "BasicKeyChainServiceTest";
+    private static final String ALIAS_1 = "client";
+    private static final String ALIAS_IMPORTED = "imported";
+    private static final String ALIAS_GENERATED = "generated";
+    public static final byte[] DUMMY_CHALLENGE = {'a', 'b', 'c'};
+    private static final String ALIAS_NON_EXISTING = "nonexisting";
+
+    private Context mContext;
+
+    private final ConditionVariable mSupportServiceAvailable = new ConditionVariable(false);
+    private IKeyChainServiceTestSupport mTestSupportService;
+    private boolean mIsSupportServiceBound;
+
+    private ServiceConnection mSupportConnection =
+            new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    Log.d(TAG, "test support service connected!");
+                    mTestSupportService = IKeyChainServiceTestSupport.Stub.asInterface(service);
+                    mSupportServiceAvailable.open();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mSupportServiceAvailable.close();
+                    mTestSupportService = null;
+                }
+            };
+
+    private final ConditionVariable mKeyChainAvailable = new ConditionVariable(false);
+    private IKeyChainService mKeyChainService;
+    private boolean mIsKeyChainServiceBound;
+
+    private ServiceConnection mServiceConnection =
+            new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    Log.d(TAG, "KeyChain service connected!");
+                    mKeyChainService = IKeyChainService.Stub.asInterface(service);
+                    mKeyChainAvailable.open();
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mKeyChainAvailable.close();
+                    mKeyChainService = null;
+                }
+            };
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        bindTestSupportService();
+        assertThat(mIsSupportServiceBound).isTrue();
+        bindKeyChainService();
+        assertThat(mIsKeyChainServiceBound).isTrue();
+
+        waitForSupportService();
+        waitForKeyChainService();
+    }
+
+    @After
+    public void tearDown() {
+        // Clean up keys that might have been left over
+        try {
+            removeKeyPair(ALIAS_IMPORTED);
+            removeKeyPair(ALIAS_GENERATED);
+            removeKeyPair(ALIAS_1);
+        } catch (RemoteException e) {
+            // Nothing to do here but warn that clean-up was not successful.
+            Log.w(TAG, "Failed cleaning up installed keys", e);
+        }
+        unbindTestSupportService();
+        assertThat(mIsSupportServiceBound).isFalse();
+        unbindKeyChainService();
+        assertThat(mIsKeyChainServiceBound).isFalse();
+    }
+
+    @Test
+    public void testCanAccessKeyAfterGettingGrant()
+            throws RemoteException, IOException, CertificateException {
+        Log.d(TAG, "Testing access to imported key after getting grant.");
+        assertThat(mTestSupportService.keystoreReset()).isTrue();
+        installFirstKey();
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_1)).isNull();
+        mTestSupportService.grantAppPermission(Process.myUid(), ALIAS_1);
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_1)).isNotNull();
+    }
+
+    @Test
+    public void testInstallAndRemoveKeyPair()
+            throws RemoteException, IOException, CertificateException {
+        Log.d(TAG, "Testing importing key.");
+        assertThat(mTestSupportService.keystoreReset()).isTrue();
+        // No key installed, all should fail.
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_IMPORTED)).isNull();
+        assertThat(mKeyChainService.getCertificate(ALIAS_IMPORTED)).isNull();
+        assertThat(mKeyChainService.getCaCertificates(ALIAS_IMPORTED)).isNull();
+
+        PrivateKeyEntry privateKeyEntry =
+                TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
+        assertThat(mTestSupportService.installKeyPair(privateKeyEntry.getPrivateKey().getEncoded(),
+                    privateKeyEntry.getCertificate().getEncoded(),
+                    Credentials.convertToPem(privateKeyEntry.getCertificateChain()),
+                    ALIAS_IMPORTED)).isTrue();
+
+        // No grant, all should still fail.
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_IMPORTED)).isNull();
+        assertThat(mKeyChainService.getCertificate(ALIAS_IMPORTED)).isNull();
+        assertThat(mKeyChainService.getCaCertificates(ALIAS_IMPORTED)).isNull();
+        // Grant access
+        mTestSupportService.grantAppPermission(Process.myUid(), ALIAS_IMPORTED);
+        // Has grant, all should succeed.
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_IMPORTED)).isNotNull();
+        assertThat(mKeyChainService.getCertificate(ALIAS_IMPORTED)).isNotNull();
+        assertThat(mKeyChainService.getCaCertificates(ALIAS_IMPORTED)).isNotNull();
+        // Finally, test removal.
+        assertThat(mTestSupportService.removeKeyPair(ALIAS_IMPORTED)).isTrue();
+    }
+
+    @Test
+    public void testUserSelectability() throws RemoteException, IOException, CertificateException {
+        Log.d(TAG, "Testing user-selectability of a key.");
+        assertThat(mTestSupportService.keystoreReset()).isTrue();
+        PrivateKeyEntry privateKeyEntry =
+                TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
+        assertThat(mTestSupportService.installKeyPair(privateKeyEntry.getPrivateKey().getEncoded(),
+                privateKeyEntry.getCertificate().getEncoded(),
+                Credentials.convertToPem(privateKeyEntry.getCertificateChain()),
+                ALIAS_IMPORTED)).isTrue();
+
+        assertThat(mKeyChainService.isUserSelectable(ALIAS_IMPORTED)).isFalse();
+        mTestSupportService.setUserSelectable(ALIAS_IMPORTED, true);
+        assertThat(mKeyChainService.isUserSelectable(ALIAS_IMPORTED)).isTrue();
+        mTestSupportService.setUserSelectable(ALIAS_IMPORTED, false);
+        assertThat(mKeyChainService.isUserSelectable(ALIAS_IMPORTED)).isFalse();
+
+        // Remove key
+        assertThat(mTestSupportService.removeKeyPair(ALIAS_IMPORTED)).isTrue();
+    }
+
+    @Test
+    public void testGenerateKeyPairErrorsOnBadUid() throws RemoteException {
+        KeyGenParameterSpec specBadUid =
+                new KeyGenParameterSpec.Builder(buildRsaKeySpec(ALIAS_GENERATED))
+                .setUid(WIFI_UID)
+                .build();
+        ParcelableKeyGenParameterSpec parcelableSpec =
+                new ParcelableKeyGenParameterSpec(specBadUid);
+        assertThat(mTestSupportService.generateKeyPair("RSA", parcelableSpec)).isEqualTo(
+                KeyChain.KEY_GEN_MISSING_ALIAS);
+    }
+
+    @Test
+    public void testGenerateKeyPairErrorsOnSuperflousAttestationChallenge() throws RemoteException {
+        KeyGenParameterSpec specWithChallenge =
+                new KeyGenParameterSpec.Builder(buildRsaKeySpec(ALIAS_GENERATED))
+                        .setAttestationChallenge(DUMMY_CHALLENGE)
+                        .build();
+        ParcelableKeyGenParameterSpec parcelableSpec =
+                new ParcelableKeyGenParameterSpec(specWithChallenge);
+        assertThat(mTestSupportService.generateKeyPair("RSA", parcelableSpec)).isEqualTo(
+                KeyChain.KEY_GEN_SUPERFLUOUS_ATTESTATION_CHALLENGE);
+    }
+
+    @Test
+    public void testGenerateKeyPairErrorsOnInvalidAlgorithm() throws RemoteException {
+        ParcelableKeyGenParameterSpec parcelableSpec = new ParcelableKeyGenParameterSpec(
+                buildRsaKeySpec(ALIAS_GENERATED));
+        assertThat(mTestSupportService.generateKeyPair("BADBAD", parcelableSpec)).isEqualTo(
+                KeyChain.KEY_GEN_NO_SUCH_ALGORITHM);
+    }
+
+    @Test
+    public void testGenerateKeyPairErrorsOnInvalidAlgorithmParameters() throws RemoteException {
+        ParcelableKeyGenParameterSpec parcelableSpec = new ParcelableKeyGenParameterSpec(
+                buildRsaKeySpec(ALIAS_GENERATED));
+        // RSA key parameters do not make sense for Elliptic Curve
+        assertThat(mTestSupportService.generateKeyPair("EC", parcelableSpec)).isEqualTo(
+                KeyChain.KEY_GEN_INVALID_ALGORITHM_PARAMETERS);
+    }
+
+    @Test
+    public void testGenerateKeyPairSucceeds() throws RemoteException {
+        generateRsaKey(ALIAS_GENERATED);
+        // Test that there are no grants by default
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_GENERATED)).isNull();
+        // And is not user-selectable by default
+        assertThat(mKeyChainService.isUserSelectable(ALIAS_GENERATED)).isFalse();
+        // But after granting access, it can be used.
+        mTestSupportService.grantAppPermission(Process.myUid(), ALIAS_GENERATED);
+        assertThat(mKeyChainService.requestPrivateKey(ALIAS_GENERATED)).isNotNull();
+    }
+
+    @Test
+    public void testAttestKeyFailsOnMissingChallenge() throws RemoteException {
+        generateRsaKey(ALIAS_GENERATED);
+        assertThat(mTestSupportService.attestKey(ALIAS_GENERATED, null, new int[]{}
+                )).isEqualTo(KeyChain.KEY_ATTESTATION_MISSING_CHALLENGE);
+    }
+
+    @Test
+    public void testAttestKeyFailsOnNonExistentKey() throws RemoteException {
+        assertThat(mTestSupportService.attestKey(ALIAS_NON_EXISTING, DUMMY_CHALLENGE, new int[]{}
+                )).isEqualTo(KeyChain.KEY_ATTESTATION_FAILURE);
+    }
+
+    @Test
+    public void testAttestKeySucceedsOnGeneratedKey() throws RemoteException {
+        generateRsaKey(ALIAS_GENERATED);
+        assertThat(mTestSupportService.attestKey(ALIAS_GENERATED, DUMMY_CHALLENGE,
+                null)).isEqualTo(KeyChain.KEY_ATTESTATION_SUCCESS);
+    }
+
+    @Test
+    public void testSetKeyPairCertificate() throws RemoteException {
+        generateRsaKey(ALIAS_GENERATED);
+        final byte[] userCert = new byte[] {'a', 'b', 'c'};
+        final byte[] certChain = new byte[] {'d', 'e', 'f'};
+
+        assertThat(mTestSupportService.setKeyPairCertificate(ALIAS_GENERATED, userCert,
+                certChain)).isTrue();
+        mTestSupportService.grantAppPermission(Process.myUid(), ALIAS_GENERATED);
+
+        assertThat(mKeyChainService.getCertificate(ALIAS_GENERATED)).isEqualTo(userCert);
+        assertThat(mKeyChainService.getCaCertificates(ALIAS_GENERATED)).isEqualTo(certChain);
+
+        final byte[] newUserCert = new byte[] {'x', 'y', 'z'};
+        assertThat(mTestSupportService.setKeyPairCertificate(ALIAS_GENERATED, newUserCert,
+                null)).isTrue();
+        assertThat(mKeyChainService.getCertificate(ALIAS_GENERATED)).isEqualTo(newUserCert);
+        assertThat(mKeyChainService.getCaCertificates(ALIAS_GENERATED)).isNull();
+    }
+
+    @Test
+    public void testInstallKeyPairErrorOnAliasSelectionDeniedKey() throws RemoteException,
+            IOException, CertificateException {
+        PrivateKeyEntry privateKeyEntry =
+                TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
+        assertThrows(IllegalArgumentException.class, () -> {
+                mTestSupportService.installKeyPair(
+                        privateKeyEntry.getPrivateKey().getEncoded(),
+                        privateKeyEntry.getCertificate().getEncoded(),
+                        Credentials.convertToPem(privateKeyEntry.getCertificateChain()),
+                        KeyChain.KEY_ALIAS_SELECTION_DENIED);
+        });
+    }
+
+    @Test
+    public void testGenerateKeyPairErrorOnAliasSelectionDeniedKey() throws RemoteException {
+        ParcelableKeyGenParameterSpec parcelableSpec =
+                new ParcelableKeyGenParameterSpec(buildRsaKeySpec(
+                        KeyChain.KEY_ALIAS_SELECTION_DENIED));
+        assertThrows(IllegalArgumentException.class, () -> {
+                mTestSupportService.generateKeyPair("RSA", parcelableSpec);
+        });
+    }
+
+    void generateRsaKey(String alias) throws RemoteException {
+        ParcelableKeyGenParameterSpec parcelableSpec = new ParcelableKeyGenParameterSpec(
+                buildRsaKeySpec(alias));
+        assertThat(mTestSupportService.generateKeyPair("RSA", parcelableSpec)).isEqualTo(
+                KeyChain.KEY_GEN_SUCCESS);
+    }
+
+    void removeKeyPair(String alias) throws RemoteException {
+        assertThat(mTestSupportService.removeKeyPair(alias)).isTrue();
+    }
+
+    void bindTestSupportService() {
+        Intent serviceIntent = new Intent(mContext, IKeyChainServiceTestSupport.class);
+        serviceIntent.setComponent(
+                new ComponentName(
+                        "com.android.keychain.tests.support",
+                        "com.android.keychain.tests.support.KeyChainServiceTestSupport"));
+        Log.d(TAG, String.format("Binding intent: %s", serviceIntent));
+        mIsSupportServiceBound =
+                mContext.bindService(serviceIntent, mSupportConnection, Context.BIND_AUTO_CREATE);
+        Log.d(TAG, String.format("Support service binding result: %b", mIsSupportServiceBound));
+    }
+
+    void unbindTestSupportService() {
+        if (mIsSupportServiceBound) {
+            mContext.unbindService(mSupportConnection);
+            mIsSupportServiceBound = false;
+        }
+    }
+
+    void bindKeyChainService() {
+        Context appContext = mContext.getApplicationContext();
+        Intent intent = new Intent(IKeyChainService.class.getName());
+        ComponentName comp = intent.resolveSystemService(appContext.getPackageManager(), 0);
+        intent.setComponent(comp);
+
+        Log.d(TAG, String.format("Binding to KeyChain: %s", intent));
+        mIsKeyChainServiceBound =
+                appContext.bindServiceAsUser(
+                        intent,
+                        mServiceConnection,
+                        Context.BIND_AUTO_CREATE,
+                        Process.myUserHandle());
+        Log.d(TAG, String.format("KeyChain service binding result: %b", mIsKeyChainServiceBound));
+    }
+
+    void unbindKeyChainService() {
+        if (mIsKeyChainServiceBound) {
+            mContext.getApplicationContext().unbindService(mServiceConnection);
+            mIsKeyChainServiceBound = false;
+        }
+    }
+
+    void installFirstKey() throws RemoteException, IOException, CertificateException {
+        String intermediate = "-intermediate";
+        String root = "-root";
+
+        String alias1PrivateKey = Credentials.USER_PRIVATE_KEY + ALIAS_1;
+        String alias1ClientCert = Credentials.USER_CERTIFICATE + ALIAS_1;
+        String alias1IntermediateCert = (Credentials.CA_CERTIFICATE + ALIAS_1 + intermediate);
+        String alias1RootCert = (Credentials.CA_CERTIFICATE + ALIAS_1 + root);
+        PrivateKeyEntry privateKeyEntry =
+                TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
+        Certificate intermediate1 = privateKeyEntry.getCertificateChain()[1];
+        Certificate root1 = TestKeyStore.getClientCertificate().getRootCertificate("RSA");
+
+        assertThat(
+                mTestSupportService.keystoreImportKey(
+                    alias1PrivateKey, privateKeyEntry.getPrivateKey().getEncoded()))
+            .isTrue();
+        assertThat(
+                mTestSupportService.keystorePut(
+                    alias1ClientCert,
+                    Credentials.convertToPem(privateKeyEntry.getCertificate())))
+            .isTrue();
+        assertThat(
+                mTestSupportService.keystorePut(
+                    alias1IntermediateCert, Credentials.convertToPem(intermediate1)))
+            .isTrue();
+        assertThat(
+                mTestSupportService.keystorePut(alias1RootCert, Credentials.convertToPem(root1)))
+            .isTrue();
+    }
+
+    void waitForSupportService() {
+        Log.d(TAG, "Waiting for support service.");
+        assertThat(mSupportServiceAvailable.block(10000)).isTrue();;
+        assertThat(mTestSupportService).isNotNull();
+    }
+
+    void waitForKeyChainService() {
+        Log.d(TAG, "Waiting for keychain service.");
+        assertThat(mKeyChainAvailable.block(10000)).isTrue();;
+        assertThat(mKeyChainService).isNotNull();
+    }
+
+    private KeyGenParameterSpec buildRsaKeySpec(String alias) {
+        return new KeyGenParameterSpec.Builder(
+                alias,
+                KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+                .setKeySize(2048)
+                .setDigests(KeyProperties.DIGEST_SHA256)
+                .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS,
+                        KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+                .setIsStrongBoxBacked(false)
+                .build();
+    }
+}
diff --git a/tests/src/com/android/keychain/tests/KeyChainActivityTest.java b/tests/src/com/android/keychain/tests/KeyChainActivityTest.java
new file mode 100644
index 0000000..73c3867
--- /dev/null
+++ b/tests/src/com/android/keychain/tests/KeyChainActivityTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 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 com.android.keychain.tests;
+
+import static com.android.keychain.KeyChainActivity.CertificateParametersFilter;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.LargeTest;
+import android.security.Credentials;
+import android.security.KeyStore;
+import android.util.Base64;
+import androidx.test.runner.AndroidJUnit4;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemWriter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public final class KeyChainActivityTest {
+    // Generated with:
+    // openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 3650
+    //   -out root_ca_certificate.pem
+    private static final String ROOT_CA_CERT_RSA =
+            "MIIDazCCAlOgAwIBAgIUWQjj+9olDNtdjcSLzK2RpxI9j6UwDQYJKoZIhvcNAQELBQAwRTELMAkG" +
+            "A1UEBhMCVUsxCzAJBgNVBAgMAk5BMQ8wDQYDVQQHDAZMb25kb24xGDAWBgNVBAoMD0FuZHJvaWQg" +
+            "VGVzdCBDQTAeFw0yMDAzMTIxMTUxNDJaFw0zMDAzMTAxMTUxNDJaMEUxCzAJBgNVBAYTAlVLMQsw" +
+            "CQYDVQQIDAJOQTEPMA0GA1UEBwwGTG9uZG9uMRgwFgYDVQQKDA9BbmRyb2lkIFRlc3QgQ0EwggEi" +
+            "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGbsQO+LqPMr5Nr4Lq0B7C0th93phohSY6hb2w" +
+            "MmZs3MRwamlw8FS64KEgmszX5lnyLNqRs91FOFNuq4f2A+TYQhawi9D2bHB7z2ishDM3SxNAqwQl" +
+            "LzVNBJw7DAtimajy3VvXoprescFbsOZx8wPGGb2xMKqAXg4Yw9F6te4Y4BSIiwCWtammiSR8Ev0B" +
+            "lcnMBrWmSZ4yYF+UgNgNiD/TVrTtRmzQlRhBo5n4F61SGeAxb5p0NRRGmAXKtx358HiLANzZSCiM" +
+            "UE5IrgDvW8AKPn5InuYS1G1K2wG5ar1eanQahimtaIEugQxhqG0+/OiKKq2LGRiBpwV1OomXHNFr" +
+            "AgMBAAGjUzBRMB0GA1UdDgQWBBRrxYWzKZpCHDi/NK4keXIU5iGukzAfBgNVHSMEGDAWgBRrxYWz" +
+            "KZpCHDi/NK4keXIU5iGukzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBJTjUY" +
+            "rfi3adJFpF2IFkSnavPRxi+NX6wxfKgQvcScO7sV18gAMG7r2NhjsVeScZ48mxsNkj99lHaVoNm6" +
+            "c+sWmcb3LO7WCqmAgfJcHeQ5VuluwNoJWo+SGuKbh6/yRejNeQFf++uaEwXP3yNydwKJyQDyDwoG" +
+            "vx0jvy8glkVl3fr6u0lGQqmubGU5Q1X6QyA0zJ/sSWBVorLCgk6KvPABQJhjoij+g/GOB1h4g6fb" +
+            "bQ3xnek6TGwjQ1bB7rQlqBF7iP/9iUtuuDf0cR8LwMr1Z2OUEMDjRHQCZQJ3APc0kW1ewJ8nqQ+m" +
+            "NsUKFRuThYtE/OFsV/TfXwMXbc2rMug+";
+
+    // Generated with:
+    // openssl genrsa  -out intermediate_key.pem 2048
+    // openssl req -new -key intermediate_key.pem -out intermediate_ca.csr
+    // openssl x509 -req -days 3650 -in intermediate_ca.csr -CA root_ca_certificate.pem
+    //   -CAkey key.pem -CAcreateserial -out intermediate_ca.pem
+    private static final String INTERMEDIATE_CA_CERT_RSA =
+            "MIIDHjCCAgYCFHVrA0CEKcoc/C8BqKap3a1m0x8CMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYT" +
+            "AlVLMQswCQYDVQQIDAJOQTEPMA0GA1UEBwwGTG9uZG9uMRgwFgYDVQQKDA9BbmRyb2lkIFRlc3Qg" +
+            "Q0EwHhcNMjAwMzEyMTE1NzUwWhcNMzAwMzEwMTE1NzUwWjBSMQswCQYDVQQGEwJVSzELMAkGA1UE" +
+            "CAwCTkExDzANBgNVBAcMBkxvbmRvbjElMCMGA1UECgwcQW5kcm9pZCBJbnRlcm1lZGlhdGUgVGVz" +
+            "dCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPlEIzApeTyKzQWTvv25z/KVEbsr" +
+            "alrcto7mX56HV1VQ53cGqi4I7dso3cvpg0CYfcZ+mZh6Evkd+njkcc7Dh/nI0KBJIzGZuo2LB+0r" +
+            "qT0RfvI/Xv7CqLO9KOjWJ3+HK3EhSXGvnLvSTsQD1LnE9HXKVdhdOUgLFjbcZrzH62mvTRAO6nhg" +
+            "agWTzprTXOX8okaMJtJl9QGMG63Z/m5DONPPrgASW6X6wksGjyorEaakQTUGuPimapP5mk+Y31Se" +
+            "pLDDumqRavLT2CpfjHfFq0iDmnnJjG5nz6oKlirhg9JjxHwuKm5jIdsO4dIgi1fJ8Goz2ODG6R6E" +
+            "6CjitsQjoMMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdud6PahLSmDLOTr9t0Jqq1HKpeRsjSqn" +
+            "JTpd/GkNUrxcctKLTzpAruMq6en8OfcSEa2s3HUMJ1LVMoO9pp4aQaadH7626Q4uqHbNGHWngVNX" +
+            "lfBioxrH2QJ2wuKBjUipEGWaM3LY0wqNuBFd5qAVuBwQtZ1x/XH7/Y4l38Y5EGGEi4jSw8eCqiiQ" +
+            "2UKItmK8byl3/T5SVgAMbFYz1WJN37EgETEcEgPlosSQ4pha6fVB3Oz6mSfzjGXqHKpHBPUn/N2d" +
+            "Z2kxJG8IuwhhhyemhqJdCfOxT2WpemgLQQCCgqtM9O89peWL8AJUVT9cF9KySOvh/P9lTtSf5bJf" +
+            "sAzfSg==";
+
+    // Generated with:
+    // openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr
+    // openssl x509 -req -days 3650 -in server.csr -CA intermediate_ca.pem
+    //   -CAkey intermediate_key.pem -CAcreateserial -out server.pem
+    private static final String LEAF_SERVER_CERT_RSA =
+            "MIIDIjCCAgoCFGNstHCN7uzPtFlxnbu2FQ3+sc7rMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYT" +
+            "AlVLMQswCQYDVQQIDAJOQTEPMA0GA1UEBwwGTG9uZG9uMSUwIwYDVQQKDBxBbmRyb2lkIEludGVy" +
+            "bWVkaWF0ZSBUZXN0IENBMB4XDTIwMDMxMjEyMTQwMloXDTMwMDMxMDEyMTQwMlowSTELMAkGA1UE" +
+            "BhMCVUsxCzAJBgNVBAgMAk5BMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAoME0FuZHJvaWQgU2Vy" +
+            "dmVyIFRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCBdt0uV01nG2ULMThfnEx" +
+            "CDoVvOSJJLcqKSMsoRUcEYvvqA70x7O2BR6+FZLilr09qAyK1ygyTh+Q6NcBxiB137khrygd/R3S" +
+            "U9nNWI6Xj882EospCiPaewR3j49F6F45PvviAacx7v0mZpU1dVMP7ZvhJWb1AB675/379EFztYyU" +
+            "ghM5ub8zuAgBuvH/O/dJpnmKUmO43n/qSHDM+Q+oDmLAkWD4gd3SOBZKcLgTyO/pD0pghT8m2t0t" +
+            "9X/3KX+tQhif1pAemCOqvxr/HHjlLWxY+QWmcRIAMzTg6+h7sNSKAwwfulMomzMwzkV5sJAYUJOm" +
+            "suhhyAXCNX5rWuqXAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACdFD/uV4iT4jg3/rEtszPHkyP4b" +
+            "zHaYJYpExoWNbJIz7djhycptdM7wjSoesWQMfpJz95aNWoVHrW85DSdGT+7HwZEsW1zUx3KkXURA" +
+            "CdlbVBn1CS4vm0Xk7Rr3LayfhqdFALQVItvBr+LkJPiG/R1jQySp1qaw+NrwukFoepukeZxHH1bF" +
+            "9zjGCLwOcfuRB9g8Gm45wRgoSUTDKbD1SMSLuPyllKeHLKE+chhYjm51Evy2xR0DLm0yaFmeyPPM" +
+            "KQaZJmtkeAzk2uYYb1HsVDlnEoDoXpTKKN1j39qckpCHxF0X0KqbN7D0grDWIDIee5mnSrg3+Xq5" +
+            "aCEDTUn6uU4=";
+
+    private static final X500Principal LEAF_SUBJECT =
+            new X500Principal("O=Android Server Test, L=London, ST=NA, C=UK");
+
+    private static final X500Principal INTERMEDIATE_SUBJECT =
+            new X500Principal("O=Android Intermediate Test CA, L=London, ST=NA, C=UK");
+
+    private static final X500Principal ROOT_SUBJECT =
+            new X500Principal("O=Android Test CA, L=London, ST=NA, C=UK");
+
+    private byte[] mLeafRsaCertificate;
+    private byte[] mIntermediateRsaCertificate;
+    private byte[] mRootRsaCertificate;
+
+    @Before
+    public void setUp() {
+        mLeafRsaCertificate = Base64.decode(LEAF_SERVER_CERT_RSA, Base64.DEFAULT);
+        mIntermediateRsaCertificate = Base64.decode(INTERMEDIATE_CA_CERT_RSA, Base64.DEFAULT);
+        mRootRsaCertificate = Base64.decode(ROOT_CA_CERT_RSA, Base64.DEFAULT);
+    }
+
+    @Test
+    public void testCertificateParametersFilter_filtersByIntermediateIssuer()
+            throws InterruptedException, ExecutionException, CancellationException,
+            TimeoutException, IOException, CertificateEncodingException {
+        KeyStore keyStore = prepareKeyStoreWithLongChainCertificates();
+
+        assertThat(createCheckerForIssuer(keyStore, ROOT_SUBJECT)
+                .shouldPresentCertificate("client")).isTrue();
+
+        assertThat(createCheckerForIssuer(keyStore, INTERMEDIATE_SUBJECT)
+                .shouldPresentCertificate("client")).isTrue();
+
+        assertThat(createCheckerForIssuer(keyStore, LEAF_SUBJECT)
+                .shouldPresentCertificate("client")).isFalse();
+    }
+
+    // Return a KeyStore instance that has both a client certificate as well as a certificate
+    // chain associated with it.
+    private KeyStore prepareKeyStoreWithLongChainCertificates()
+            throws IOException, CertificateEncodingException {
+        KeyStore keyStore = mock(KeyStore.class);
+        when(keyStore.get(Credentials.USER_CERTIFICATE + "client")).thenReturn(mLeafRsaCertificate);
+        Certificate[] intermediates = new Certificate[] {
+            parseCertificate(mRootRsaCertificate), parseCertificate(mIntermediateRsaCertificate)};
+        byte[] intermediatesPem = convertToPem(intermediates);
+        assertThat(intermediatesPem).isNotNull();
+        when(keyStore.get(Credentials.CA_CERTIFICATE + "client")).thenReturn(intermediatesPem);
+
+        return keyStore;
+    }
+
+    // Create a CertificateParametersFilter instance that has the specified issuer as a requested
+    // issuer.
+    private static CertificateParametersFilter createCheckerForIssuer(
+            KeyStore keyStore, X500Principal issuer) {
+        return new CertificateParametersFilter(
+                keyStore, new String[] {},
+                new ArrayList<byte[]>(Arrays.asList(issuer.getEncoded())));
+    }
+
+    private static X509Certificate parseCertificate(byte[] certificateBytes) {
+        InputStream in = new ByteArrayInputStream(certificateBytes);
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            return (X509Certificate)cf.generateCertificate(in);
+        } catch (CertificateException e) {
+            fail(String.format("Could not parse certificate: %s", e));
+            return null;
+        }
+    }
+
+    // Copied from android.security.Credentials, as that is a framework class.
+    public static byte[] convertToPem(Certificate... objects)
+            throws IOException, CertificateEncodingException {
+        ByteArrayOutputStream bao = new ByteArrayOutputStream();
+        Writer writer = new OutputStreamWriter(bao, StandardCharsets.US_ASCII);
+        PemWriter pw = new PemWriter(writer);
+        for (Certificate o : objects) {
+            pw.writeObject(new PemObject("CERTIFICATE", o.getEncoded()));
+        }
+        pw.close();
+        return bao.toByteArray();
+    }
+}
diff --git a/tests/src/com/android/keychain/tests/KeyChainServiceTest.java b/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
deleted file mode 100644
index 7e4008a..0000000
--- a/tests/src/com/android/keychain/tests/KeyChainServiceTest.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright (C) 2011 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 com.android.keychain.tests;
-
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.IBinder;
-import android.security.Credentials;
-import android.security.IKeyChainService;
-import android.security.KeyStore;
-import android.util.Log;
-import com.android.keychain.tests.support.IKeyChainServiceTestSupport;
-import java.security.KeyStore.PrivateKeyEntry;
-import java.security.cert.Certificate;
-import java.util.Arrays;
-import junit.framework.Assert;
-import libcore.java.security.TestKeyStore;
-
-public class KeyChainServiceTest extends Service {
-
-    private static final String TAG = "KeyChainServiceTest";
-
-    private final Object mSupportLock = new Object();
-    private IKeyChainServiceTestSupport mSupport;
-    private boolean mIsBoundSupport;
-
-    private final Object mServiceLock = new Object();
-    private IKeyChainService mService;
-    private boolean mIsBoundService;
-
-    private ServiceConnection mSupportConnection = new ServiceConnection() {
-        @Override public void onServiceConnected(ComponentName name, IBinder service) {
-            synchronized (mSupportLock) {
-                mSupport = IKeyChainServiceTestSupport.Stub.asInterface(service);
-                mSupportLock.notifyAll();
-            }
-        }
-
-        @Override public void onServiceDisconnected(ComponentName name) {
-            synchronized (mSupportLock) {
-                mSupport = null;
-            }
-        }
-    };
-
-    private ServiceConnection mServiceConnection = new ServiceConnection() {
-        @Override public void onServiceConnected(ComponentName name, IBinder service) {
-            synchronized (mServiceLock) {
-                mService = IKeyChainService.Stub.asInterface(service);
-                mServiceLock.notifyAll();
-            }
-        }
-
-        @Override public void onServiceDisconnected(ComponentName name) {
-            synchronized (mServiceLock) {
-                mService = null;
-            }
-        }
-    };
-
-    private static void addComponentToIntent(PackageManager pm, Intent intent) {
-        ResolveInfo service = pm.resolveService(intent, 0);
-        if (service == null) {
-            Log.w(TAG, String.format("No service found for intent: %s", intent.getAction()));
-        } else {
-            Log.d(TAG, String.format("Found service: %s %s for action %s",
-                        service.serviceInfo.packageName, service.serviceInfo.name,
-                        intent.getAction()));
-            ComponentName comp = new ComponentName(
-                    service.serviceInfo.packageName, service.serviceInfo.name);
-            intent.setComponent(comp);
-        }
-    }
-
-    private void bindSupport() {
-        Intent serviceIntent = new Intent(IKeyChainServiceTestSupport.class.getName());
-        addComponentToIntent(getPackageManager(), serviceIntent);
-        mIsBoundSupport = bindService(serviceIntent,
-                                      mSupportConnection,
-                                      Context.BIND_AUTO_CREATE);
-        Log.d(TAG, String.format("Finished bindSupport with result: %b", mIsBoundSupport));
-    }
-
-    private void bindService() {
-        Intent serviceIntent = new Intent(IKeyChainService.class.getName());
-        addComponentToIntent(getPackageManager(), serviceIntent);
-        mIsBoundService = bindService(serviceIntent,
-                                      mServiceConnection,
-                                      Context.BIND_AUTO_CREATE);
-        Log.d(TAG, String.format("Finished bindService with result: %b", mIsBoundService));
-    }
-
-    private void unbindServices() {
-        if (mIsBoundSupport) {
-            unbindService(mSupportConnection);
-            mIsBoundSupport = false;
-        }
-        if (mIsBoundService) {
-            unbindService(mServiceConnection);
-            mIsBoundService = false;
-        }
-    }
-
-    @Override public IBinder onBind(Intent intent) {
-        Log.d(TAG, "onBind");
-        return null;
-    }
-
-    @Override public int onStartCommand(Intent intent, int flags, int startId) {
-        Log.d(TAG, "onStartCommand");
-        new Thread(new Test(), TAG).start();
-        return START_STICKY;
-    }
-
-    @Override public void onDestroy () {
-        Log.d(TAG, "onDestroy");
-        unbindServices();
-    }
-
-    private final class Test extends Assert implements Runnable {
-
-        @Override public void run() {
-            try {
-                test_KeyChainService();
-            } catch (RuntimeException e) {
-                // rethrow RuntimeException without wrapping
-                throw e;
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            } finally {
-                stopSelf();
-            }
-        }
-
-        public void test_KeyChainService() throws Exception {
-            Log.d(TAG, "test_KeyChainService uid=" + getApplicationInfo().uid);
-
-            Log.d(TAG, "test_KeyChainService bind support");
-            bindSupport();
-            assertTrue(mIsBoundSupport);
-            synchronized (mSupportLock) {
-                if (mSupport == null) {
-                    mSupportLock.wait(10 * 1000);
-                }
-            }
-            assertNotNull(mSupport);
-
-            Log.d(TAG, "test_KeyChainService setup keystore");
-            KeyStore keyStore = KeyStore.getInstance();
-            assertTrue(mSupport.keystoreReset());
-            assertTrue(mSupport.keystoreSetPassword("newpasswd"));
-
-            String intermediate = "-intermediate";
-            String root = "-root";
-
-            String alias1 = "client";
-            String alias1Intermediate = alias1 + intermediate;
-            String alias1Root = alias1 + root;
-            String alias1Pkey = (Credentials.USER_PRIVATE_KEY + alias1);
-            String alias1Cert = (Credentials.USER_CERTIFICATE + alias1);
-            String alias1ICert = (Credentials.CA_CERTIFICATE + alias1Intermediate);
-            String alias1RCert = (Credentials.CA_CERTIFICATE + alias1Root);
-            PrivateKeyEntry pke1 = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA");
-            Certificate intermediate1 = pke1.getCertificateChain()[1];
-            Certificate root1 = TestKeyStore.getClientCertificate().getRootCertificate("RSA");
-
-            final String alias2 = "server";
-            String alias2Intermediate = alias2 + intermediate;
-            String alias2Root = alias2 + root;
-            String alias2Pkey = (Credentials.USER_PRIVATE_KEY + alias2);
-            String alias2Cert = (Credentials.USER_CERTIFICATE + alias2);
-            String alias2ICert = (Credentials.CA_CERTIFICATE + alias2Intermediate);
-            String alias2RCert = (Credentials.CA_CERTIFICATE + alias2Root);
-            PrivateKeyEntry pke2 = TestKeyStore.getServer().getPrivateKey("RSA", "RSA");
-            Certificate intermediate2 = pke2.getCertificateChain()[1];
-            Certificate root2 = TestKeyStore.getServer().getRootCertificate("RSA");
-
-            assertTrue(mSupport.keystoreImportKey(alias1Pkey,
-                                           pke1.getPrivateKey().getEncoded()));
-            assertTrue(mSupport.keystorePut(alias1Cert,
-                                            Credentials.convertToPem(pke1.getCertificate())));
-            assertTrue(mSupport.keystorePut(alias1ICert,
-                                            Credentials.convertToPem(intermediate1)));
-            assertTrue(mSupport.keystorePut(alias1RCert,
-                                            Credentials.convertToPem(root1)));
-            assertTrue(mSupport.keystoreImportKey(alias2Pkey,
-                                            pke2.getPrivateKey().getEncoded()));
-            assertTrue(mSupport.keystorePut(alias2Cert,
-                                            Credentials.convertToPem(pke2.getCertificate())));
-            assertTrue(mSupport.keystorePut(alias2ICert,
-                                            Credentials.convertToPem(intermediate2)));
-            assertTrue(mSupport.keystorePut(alias2RCert,
-                                            Credentials.convertToPem(root2)));
-
-            assertEquals(KeyStore.State.UNLOCKED, keyStore.state());
-
-            Log.d(TAG, "test_KeyChainService bind service");
-            bindService();
-            assertTrue(mIsBoundService);
-            synchronized (mServiceLock) {
-                if (mService == null) {
-                    mServiceLock.wait(10 * 1000);
-                }
-            }
-            assertNotNull(mService);
-
-            mSupport.grantAppPermission(getApplicationInfo().uid, alias1);
-            // don't grant alias2, so it can be done manually with KeyChainTestActivity
-            Log.d(TAG, "test_KeyChainService positive testing");
-            assertNotNull("Requesting private key should succeed",
-                    mService.requestPrivateKey(alias1));
-
-            byte[] certificate = mService.getCertificate(alias1);
-            assertNotNull(certificate);
-            assertEquals(Arrays.toString(Credentials.convertToPem(pke1.getCertificate())),
-                         Arrays.toString(certificate));
-
-            Log.d(TAG, "test_KeyChainService negative testing");
-            mSupport.revokeAppPermission(getApplicationInfo().uid, alias2);
-            try {
-                mService.requestPrivateKey(alias2);
-                fail();
-            } catch (IllegalStateException expected) {
-            }
-
-            try {
-                mService.getCertificate(alias2);
-                fail();
-            } catch (IllegalStateException expected) {
-            }
-
-            Log.d(TAG, "test_KeyChainService unbind");
-            unbindServices();
-            assertFalse(mIsBoundSupport);
-            assertFalse(mIsBoundService);
-
-            Log.d(TAG, "test_KeyChainService end");
-        }
-    }
-}