Snap for 7631994 from 27ab6419029734e396f30848daa704d8dfefec2d to mainline-resolv-release

Change-Id: I0ace7f3a5c303ddf22342481b1ac52eb73903f3b
diff --git a/src/com/android/remoteprovisioner/BootReceiver.java b/src/com/android/remoteprovisioner/BootReceiver.java
index f39d155..bf58b9c 100644
--- a/src/com/android/remoteprovisioner/BootReceiver.java
+++ b/src/com/android/remoteprovisioner/BootReceiver.java
@@ -16,14 +16,23 @@
 
 package com.android.remoteprovisioner;
 
+import static java.lang.Math.max;
+
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
+import android.security.remoteprovisioning.IRemoteProvisioning;
 import android.util.Log;
 
+import java.time.Duration;
+
 /**
  * A receiver class that listens for boot to be completed and then starts a recurring job that will
  * monitor the status of the attestation key pool on device, purging old certificates and requesting
@@ -31,14 +40,34 @@
  */
 public class BootReceiver extends BroadcastReceiver {
     private static final String TAG = "RemoteProvisioningBootReceiver";
+    private static final String SERVICE = "android.security.remoteprovisioning";
+
+    private static final Duration SCHEDULER_PERIOD = Duration.ofDays(1);
+
+    private static final int ESTIMATED_DOWNLOAD_BYTES_STATIC = 2300;
+    private static final int ESTIMATED_X509_CERT_BYTES = 540;
+    private static final int ESTIMATED_UPLOAD_BYTES_STATIC = 600;
+    private static final int ESTIMATED_CSR_KEY_BYTES = 44;
+
     @Override
     public void onReceive(Context context, Intent intent) {
-        Log.d(TAG, "Caught boot intent, waking up.");
+        Log.i(TAG, "Caught boot intent, waking up.");
+        SettingsManager.generateAndSetId(context);
+        // An average call transmits about 500 bytes total. These calculations are for the
+        // once a month wake-up where provisioning occurs, where the expected bytes sent is closer
+        // to 8-10KB.
+        int numKeysNeeded = max(SettingsManager.getExtraSignedKeysAvailable(context),
+                                calcNumPotentialKeysToDownload());
+        int estimatedDlBytes =
+                ESTIMATED_DOWNLOAD_BYTES_STATIC + (ESTIMATED_X509_CERT_BYTES * numKeysNeeded);
+        int estimatedUploadBytes =
+                ESTIMATED_UPLOAD_BYTES_STATIC + (ESTIMATED_CSR_KEY_BYTES * numKeysNeeded);
+
         JobInfo info = new JobInfo
                 .Builder(1, new ComponentName(context, PeriodicProvisioner.class))
                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
-                .setEstimatedNetworkBytes(1000, 1000)
-                .setPeriodic(1000 * 60 * 60 * 24)
+                .setEstimatedNetworkBytes(estimatedDlBytes, estimatedUploadBytes)
+                .setPeriodic(SCHEDULER_PERIOD.toMillis())
                 .build();
         if (((JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(info)
                 != JobScheduler.RESULT_SUCCESS) {
@@ -46,4 +75,31 @@
         }
     }
 
+    private int calcNumPotentialKeysToDownload() {
+        try {
+            IRemoteProvisioning binder =
+                IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+            int totalKeysAssigned = 0;
+            if (binder == null) {
+                Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
+                return totalKeysAssigned;
+            }
+            ImplInfo[] implInfos = binder.getImplementationInfo();
+            if (implInfos == null) {
+                Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
+                           + SERVICE);
+                return totalKeysAssigned;
+            }
+            for (int i = 0; i < implInfos.length; i++) {
+                AttestationPoolStatus pool = binder.getPoolStatus(0, implInfos[i].secLevel);
+                if (pool != null) {
+                    totalKeysAssigned += pool.attested - pool.unassigned;
+                }
+            }
+            return totalKeysAssigned;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failure on the RemoteProvisioning backend.", e);
+            return 0;
+        }
+    }
 }
diff --git a/src/com/android/remoteprovisioner/CborUtils.java b/src/com/android/remoteprovisioner/CborUtils.java
index d30f8f5..d3fc3d7 100644
--- a/src/com/android/remoteprovisioner/CborUtils.java
+++ b/src/com/android/remoteprovisioner/CborUtils.java
@@ -16,12 +16,14 @@
 
 package com.android.remoteprovisioner;
 
+import android.content.Context;
 import android.os.Build;
 import android.util.Log;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -35,8 +37,16 @@
 import co.nstant.in.cbor.model.MajorType;
 import co.nstant.in.cbor.model.Map;
 import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
 
 public class CborUtils {
+    public static final int EC_CURVE_P256 = 1;
+    public static final int EC_CURVE_25519 = 2;
+
+    public static final String EXTRA_KEYS = "num_extra_attestation_keys";
+    public static final String TIME_TO_REFRESH = "time_to_refresh_hours";
+    public static final String PROVISIONING_URL = "provisioning_url";
+
     private static final int RESPONSE_CERT_ARRAY_INDEX = 0;
     private static final int RESPONSE_ARRAY_SIZE = 1;
 
@@ -44,11 +54,18 @@
     private static final int UNIQUE_CERTIFICATES_INDEX = 1;
     private static final int CERT_ARRAY_ENTRIES = 2;
 
-    private static final int EEK_INDEX = 0;
+    private static final int EEK_AND_CURVE_INDEX = 0;
     private static final int CHALLENGE_INDEX = 1;
-    private static final int EEK_ARRAY_ENTRIES = 2;
+    private static final int CONFIG_INDEX = 2;
 
+    private static final int CURVE_AND_EEK_CHAIN_LENGTH = 2;
+    private static final int CURVE_INDEX = 0;
+    private static final int EEK_CERT_CHAIN_INDEX = 1;
+
+    private static final int EEK_ARRAY_ENTRIES_NO_CONFIG = 2;
+    private static final int EEK_ARRAY_ENTRIES_WITH_CONFIG = 3;
     private static final String TAG = "RemoteProvisioningService";
+    private static final byte[] EMPTY_MAP = new byte[] {(byte) 0xA0};
 
     /**
      * Parses the signed certificate chains returned by the server. In order to reduce data use over
@@ -67,10 +84,10 @@
             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
             List<DataItem> dataItems = new CborDecoder(bais).decode();
             if (dataItems.size() != RESPONSE_ARRAY_SIZE
-                    || dataItems.get(RESPONSE_CERT_ARRAY_INDEX).getMajorType() != MajorType.ARRAY) {
+                    || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
+                                  MajorType.ARRAY, "CborResponse")) {
                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
-                            + dataItems.size() + "\nExpected major type: Array. Actual: "
-                            + dataItems.get(0).getMajorType().name());
+                            + dataItems.size());
                 return null;
             }
             dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
@@ -79,12 +96,10 @@
                             + dataItems.size());
                 return null;
             }
-            if (dataItems.get(SHARED_CERTIFICATES_INDEX).getMajorType() != MajorType.BYTE_STRING
-                    || dataItems.get(UNIQUE_CERTIFICATES_INDEX).getMajorType() != MajorType.ARRAY) {
-                Log.e(TAG, "Incorrect CBOR types. Expected 'Byte String' and 'Array'. Got: "
-                            + dataItems.get(SHARED_CERTIFICATES_INDEX).getMajorType().name()
-                            + " and "
-                            + dataItems.get(UNIQUE_CERTIFICATES_INDEX).getMajorType().name());
+            if (!checkType(dataItems.get(SHARED_CERTIFICATES_INDEX),
+                           MajorType.BYTE_STRING, "SharedCertificates")
+                    || !checkType(dataItems.get(UNIQUE_CERTIFICATES_INDEX),
+                                  MajorType.ARRAY, "UniqueCertificates")) {
                 return null;
             }
             byte[] sharedCertificates =
@@ -92,9 +107,7 @@
             Array uniqueCertificates = (Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX);
             List<byte[]> uniqueCertificateChains = new ArrayList<byte[]>();
             for (DataItem entry : uniqueCertificates.getDataItems()) {
-                if (entry.getMajorType() != MajorType.BYTE_STRING) {
-                    Log.e(TAG, "Incorrect CBOR type. Expected: 'Byte String'. Actual:"
-                                + entry.getMajorType().name());
+                if (!checkType(entry, MajorType.BYTE_STRING, "UniqueCertificate")) {
                     return null;
                 }
                 ByteArrayOutputStream concat = new ByteArrayOutputStream();
@@ -112,39 +125,109 @@
         return null;
     }
 
+    private static boolean checkType(DataItem item, MajorType majorType, String field) {
+        if (item.getMajorType() != majorType) {
+            Log.e(TAG, "Incorrect CBOR type for field: " + field + ". Expected " + majorType.name()
+                        + ". Actual: " + item.getMajorType().name());
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean parseDeviceConfig(GeekResponse resp, DataItem deviceConfig) {
+        if (!checkType(deviceConfig, MajorType.MAP, "DeviceConfig")) {
+            return false;
+        }
+        Map deviceConfiguration = (Map) deviceConfig;
+        DataItem extraKeys =
+                deviceConfiguration.get(new UnicodeString(EXTRA_KEYS));
+        DataItem timeToRefreshHours =
+                deviceConfiguration.get(new UnicodeString(TIME_TO_REFRESH));
+        DataItem newUrl =
+                deviceConfiguration.get(new UnicodeString(PROVISIONING_URL));
+        if (extraKeys != null) {
+            if (!checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys")) {
+                return false;
+            }
+            resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue();
+        }
+        if (timeToRefreshHours != null) {
+            if (!checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh")) {
+                return false;
+            }
+            resp.timeToRefresh =
+                    Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue());
+        }
+        if (newUrl != null) {
+            if (!checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL")) {
+                return false;
+            }
+            resp.provisioningUrl = ((UnicodeString) newUrl).getString();
+        }
+        return true;
+    }
+
     /**
      * Parses the Google Endpoint Encryption Key response provided by the server which contains a
      * Google signed EEK and a challenge for use by the underlying IRemotelyProvisionedComponent HAL
      */
     public static GeekResponse parseGeekResponse(byte[] serverResp) {
         try {
+            GeekResponse resp = new GeekResponse();
             ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
             List<DataItem> dataItems = new CborDecoder(bais).decode();
             if (dataItems.size() != RESPONSE_ARRAY_SIZE
-                    || dataItems.get(RESPONSE_CERT_ARRAY_INDEX).getMajorType() != MajorType.ARRAY) {
+                    || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
+                                  MajorType.ARRAY, "CborResponse")) {
                 Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
-                            + dataItems.size() + "\nExpected major type: Array. Actual: "
-                            + dataItems.get(0).getMajorType().name());
-                return null;
-            }
-            dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
-            if (dataItems.size() != EEK_ARRAY_ENTRIES) {
-                Log.e(TAG, "Incorrect number of certificate array entries. Expected: 2. Actual: "
                             + dataItems.size());
                 return null;
             }
-            if (dataItems.get(EEK_INDEX).getMajorType() != MajorType.ARRAY
-                    || dataItems.get(CHALLENGE_INDEX).getMajorType() != MajorType.BYTE_STRING) {
-                Log.e(TAG, "Incorrect CBOR types. Expected 'Array' and 'Byte String'. Got: "
-                            + dataItems.get(EEK_INDEX).getMajorType().name()
-                            + " and "
-                            + dataItems.get(CHALLENGE_INDEX).getMajorType().name());
+            List<DataItem> respItems =
+                    ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
+            if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG
+                    && respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) {
+                Log.e(TAG, "Incorrect number of certificate array entries. Expected: "
+                            + EEK_ARRAY_ENTRIES_NO_CONFIG + " or " + EEK_ARRAY_ENTRIES_WITH_CONFIG
+                            + ". Actual: " + respItems.size());
                 return null;
             }
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            new CborEncoder(baos).encode(dataItems.get(EEK_INDEX));
-            return new GeekResponse(baos.toByteArray(),
-                                    ((ByteString) dataItems.get(CHALLENGE_INDEX)).getBytes());
+            if (!checkType(respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr")) {
+                return null;
+            }
+            List<DataItem> curveAndEekChains =
+                    ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
+            for (int i = 0; i < curveAndEekChains.size(); i++) {
+                if (!checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve")) {
+                    return null;
+                }
+                List<DataItem> curveAndEekChain =
+                        ((Array) curveAndEekChains.get(i)).getDataItems();
+                if (curveAndEekChain.size() != CURVE_AND_EEK_CHAIN_LENGTH) {
+                    Log.e(TAG, "Wrong size. Expected: " + CURVE_AND_EEK_CHAIN_LENGTH + ". Actual: "
+                               + curveAndEekChain.size());
+                    return null;
+                }
+                if (!checkType(curveAndEekChain.get(CURVE_INDEX),
+                               MajorType.UNSIGNED_INTEGER, "Curve")
+                        || !checkType(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX),
+                                                           MajorType.ARRAY, "EekCertChain")) {
+                    return null;
+                }
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                new CborEncoder(baos).encode(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX));
+                UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX);
+                resp.addGeek(curve.getValue().intValue(), baos.toByteArray());
+            }
+            if (!checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge")) {
+                return null;
+            }
+            resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes());
+            if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG
+                    && !parseDeviceConfig(resp, respItems.get(CONFIG_INDEX))) {
+                return null;
+            }
+            return resp;
         } catch (CborException e) {
             Log.e(TAG, "CBOR parsing/serializing failed.", e);
             return null;
@@ -152,22 +235,46 @@
     }
 
     /**
+     * Creates the bundle of data that the server needs in order to make a decision over what
+     * device configuration values to return. In general, this boils down to if remote provisioning
+     * is turned on at all or not.
+     *
+     * @return the CBOR encoded provisioning information relevant to the server.
+     */
+    public static byte[] buildProvisioningInfo(Context context) {
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            new CborEncoder(baos).encode(new CborBuilder()
+                    .addMap()
+                        .put("fingerprint", Build.FINGERPRINT)
+                        .put(new UnicodeString("id"),
+                             new UnsignedInteger(SettingsManager.getId(context)))
+                        .end()
+                    .build());
+            return baos.toByteArray();
+        } catch (CborException e) {
+            Log.e(TAG, "CBOR serialization failed.", e);
+            return EMPTY_MAP;
+        }
+    }
+
+    /**
      * Takes the various fields fetched from the server and the remote provisioning service and
      * formats them in the CBOR blob the server is expecting as defined by the
      * IRemotelyProvisionedComponent HAL AIDL files.
      */
     public static byte[] buildCertificateRequest(byte[] deviceInfo, byte[] challenge,
                                                  byte[] protectedData, byte[] macedKeysToSign) {
-            // This CBOR library doesn't support adding already serialized CBOR structures into a
-            // CBOR builder. Because of this, we have to first deserialize the provided parameters
-            // back into the library's CBOR object types, and then reserialize them into the
-            // desired structure.
+        // This CBOR library doesn't support adding already serialized CBOR structures into a
+        // CBOR builder. Because of this, we have to first deserialize the provided parameters
+        // back into the library's CBOR object types, and then reserialize them into the
+        // desired structure.
         try {
             // Deserialize the protectedData blob
             ByteArrayInputStream bais = new ByteArrayInputStream(protectedData);
             List<DataItem> dataItems = new CborDecoder(bais).decode();
-            if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.ARRAY) {
-                Log.e(TAG, "protectedData is carrying unexpected data.");
+            if (dataItems.size() != 1
+                    || !checkType(dataItems.get(0), MajorType.ARRAY, "ProtectedData")) {
                 return null;
             }
             Array protectedDataArray = (Array) dataItems.get(0);
@@ -175,8 +282,8 @@
             // Deserialize macedKeysToSign
             bais = new ByteArrayInputStream(macedKeysToSign);
             dataItems = new CborDecoder(bais).decode();
-            if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.ARRAY) {
-                Log.e(TAG, "macedKeysToSign is carrying unexpected data.");
+            if (dataItems.size() != 1
+                    || !checkType(dataItems.get(0), MajorType.ARRAY, "MacedKeysToSign")) {
                 return null;
             }
             Array macedKeysToSignArray = (Array) dataItems.get(0);
@@ -184,8 +291,8 @@
             // Deserialize deviceInfo
             bais = new ByteArrayInputStream(deviceInfo);
             dataItems = new CborDecoder(bais).decode();
-            if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.MAP) {
-                Log.e(TAG, "macedKeysToSign is carrying unexpected data.");
+            if (dataItems.size() != 1
+                    || !checkType(dataItems.get(0), MajorType.MAP, "DeviceInfo")) {
                 return null;
             }
             Map verifiedDeviceInfoMap = (Map) dataItems.get(0);
diff --git a/src/com/android/remoteprovisioner/GeekResponse.java b/src/com/android/remoteprovisioner/GeekResponse.java
index 0a1c798..dcfe040 100644
--- a/src/com/android/remoteprovisioner/GeekResponse.java
+++ b/src/com/android/remoteprovisioner/GeekResponse.java
@@ -16,24 +16,83 @@
 
 package com.android.remoteprovisioner;
 
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Convenience class for packaging up the values returned by the server when initially requesting
  * an Endpoint Encryption Key for remote provisioning. Those values are described by the following
  * CDDL Schema:
  *    GeekResponse = [
- *        EekChain,
+ *        [+CurveAndEek],
  *        challenge : bstr,
+ *        ? Config,
  *    ]
+ *    CurveAndEek = [
+ *        curve: uint,
+ *        EekChain
+ *    ]
+ *    Config = {
+ *        ? "num_extra_attestation_keys": uint,
+ *        ? "time_to_refresh_hours" : uint,
+ *        ? "provisioning_url": tstr,
+ *    }
  *
  * The CDDL that defines EekChain is defined in the RemoteProvisioning HAL, but this app does not
  * require any semantic understanding of the format to perform its function.
  */
 public class GeekResponse {
-    public byte[] challenge;
-    public byte[] geek;
+    public static final int NO_EXTRA_KEY_UPDATE = -1;
+    private byte[] mChallenge;
+    private Map<Integer, byte[]> mCurveToGeek;
+    public int numExtraAttestationKeys;
+    public Duration timeToRefresh;
+    public String provisioningUrl;
 
-    public GeekResponse(byte[] geek, byte[] challenge) {
-        this.geek = geek;
-        this.challenge = challenge;
+    /**
+     * Default initializer.
+     */
+    public GeekResponse() {
+        mCurveToGeek = new HashMap();
+        numExtraAttestationKeys = NO_EXTRA_KEY_UPDATE;
+    }
+
+    /**
+     * Add a CBOR encoded array containing a GEEK and the corresponding certificate chain, keyed
+     * on the EC {@code curve}.
+     *
+     * @param curve an integer which represents an EC curve.
+     * @param geekChain the encoded CBOR array containing an ECDH key and corresponding certificate
+     *                  chain.
+     */
+    public void addGeek(int curve, byte[] geekChain) {
+        mCurveToGeek.put(curve, geekChain);
+    }
+
+    /**
+     * Returns the encoded CBOR array with an ECDH key corresponding to the provided {@code curve}.
+     *
+     * @param curve an integer which represents an EC curve.
+     * @return the corresponding encoded CBOR array.
+     */
+    public byte[] getGeekChain(int curve) {
+        return mCurveToGeek.get(curve);
+    }
+
+    /**
+     * Sets the {@code challenge}.
+     */
+    public void setChallenge(byte[] challenge) {
+        mChallenge = challenge;
+    }
+
+    /**
+     * Returns the {@code challenge}.
+     *
+     * @return the challenge that will be embedded in the CSR sent to the server.
+     */
+    public byte[] getChallenge() {
+        return mChallenge;
     }
 }
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index 397ea9a..fd5e69c 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -16,36 +16,48 @@
 
 package com.android.remoteprovisioner;
 
+import static java.lang.Math.min;
+
 import android.app.job.JobParameters;
 import android.app.job.JobService;
+import android.content.Context;
+import android.net.ConnectivityManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
 import android.security.remoteprovisioning.IRemoteProvisioning;
 import android.util.Log;
 
+import java.time.Duration;
+
 /**
  * A class that extends JobService in order to be scheduled to check the status of the attestation
  * key pool at regular intervals. If the job determines that more keys need to be generated and
  * signed, it drives that process.
  */
 public class PeriodicProvisioner extends JobService {
-    //TODO(b/176249146): Replace default values here with values fetched from the server.
-    private static int sTotalSignedKeys = 0;
-    // Check for expiring certs in the next 3 days
-    private static int sExpiringBy = 1000 * 60 * 60 * 24 * 3;
+
+    private static final int FAILURE_MAXIMUM = 5;
+    private static final int SAFE_CSR_BATCH_SIZE = 20;
+
+    // How long to wait in between key pair generations to avoid flooding keystore with requests.
+    private static final Duration KEY_GENERATION_PAUSE = Duration.ofMillis(1000);
+
+    // If the connection is metered when the job service is started, try to avoid provisioning.
+    private static final long METERED_CONNECTION_EXPIRATION_CHECK = Duration.ofDays(1).toMillis();
 
     private static final String SERVICE = "android.security.remoteprovisioning";
     private static final String TAG = "RemoteProvisioningService";
     private ProvisionerThread mProvisionerThread;
 
     /**
-     * Starts the periodic provisioning job, which will occasionally check the attestation key pool
+     * Starts the periodic provisioning job, which will check the attestation key pool
      * and provision it as necessary.
      */
     public boolean onStartJob(JobParameters params) {
-        Log.d(TAG, "Starting provisioning job");
-        mProvisionerThread = new ProvisionerThread(params);
+        Log.i(TAG, "Starting provisioning job");
+        mProvisionerThread = new ProvisionerThread(params, this);
         mProvisionerThread.start();
         return true;
     }
@@ -54,15 +66,16 @@
      * Allows the job to be stopped if need be.
      */
     public boolean onStopJob(JobParameters params) {
-        mProvisionerThread.stop();
         return false;
     }
 
     private class ProvisionerThread extends Thread {
+        private Context mContext;
         private JobParameters mParams;
 
-        ProvisionerThread(JobParameters params) {
+        ProvisionerThread(JobParameters params, Context context) {
             mParams = params;
+            mContext = context;
         }
 
         public void run() {
@@ -71,45 +84,171 @@
                         IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
                 if (binder == null) {
                     Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
-                    jobFinished(mParams, true /* wantsReschedule */);
+                    jobFinished(mParams, false /* wantsReschedule */);
                     return;
                 }
-                int[] securityLevels = binder.getSecurityLevels();
-                if (securityLevels == null) {
+
+                ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+                        Context.CONNECTIVITY_SERVICE);
+                boolean isMetered = cm.isActiveNetworkMetered();
+                long expiringBy;
+                if (isMetered) {
+                    // Check a shortened duration to attempt to avoid metered connection
+                    // provisioning.
+                    expiringBy = System.currentTimeMillis() + METERED_CONNECTION_EXPIRATION_CHECK;
+                } else {
+                    expiringBy = SettingsManager.getExpiringBy(mContext)
+                                                      .plusMillis(System.currentTimeMillis())
+                                                      .toMillis();
+                }
+                ImplInfo[] implInfos = binder.getImplementationInfo();
+                if (implInfos == null) {
                     Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
                                + SERVICE);
-                    jobFinished(mParams, true /* wantsReschedule */);
+                    jobFinished(mParams, false /* wantsReschedule */);
                     return;
                 }
-                for (int i = 0; i < securityLevels.length; i++) {
-                    // TODO(b/176249146): Replace expiration date parameter with value fetched from
-                    //                    server
-                    checkAndProvision(binder, System.currentTimeMillis() + sExpiringBy,
-                            securityLevels[i]);
+                int[] keysNeededForSecLevel = new int[implInfos.length];
+                boolean provisioningNeeded =
+                        isProvisioningNeeded(binder, expiringBy, implInfos, keysNeededForSecLevel);
+                GeekResponse resp = null;
+                if (!provisioningNeeded) {
+                    if (!isMetered) {
+                        // So long as the connection is unmetered, go ahead and grab an updated
+                        // device configuration file.
+                        resp = ServerInterface.fetchGeek(mContext);
+                        if (!checkGeekResp(resp)) {
+                            jobFinished(mParams, false /* wantsReschedule */);
+                            return;
+                        }
+                        SettingsManager.setDeviceConfig(mContext,
+                                resp.numExtraAttestationKeys,
+                                resp.timeToRefresh,
+                                resp.provisioningUrl);
+                        if (resp.numExtraAttestationKeys == 0) {
+                            binder.deleteAllKeys();
+                        }
+                    }
+                    jobFinished(mParams, false /* wantsReschedule */);
+                    return;
+                }
+                resp = ServerInterface.fetchGeek(mContext);
+                if (!checkGeekResp(resp)) {
+                    jobFinished(mParams, false /* wantsReschedule */);
+                    return;
+                }
+                SettingsManager.setDeviceConfig(mContext,
+                            resp.numExtraAttestationKeys,
+                            resp.timeToRefresh,
+                            resp.provisioningUrl);
+
+                if (resp.numExtraAttestationKeys == 0) {
+                    // Provisioning is disabled. Check with the server if it's time to turn it back
+                    // on. If not, quit. Avoid checking if the connection is metered. Opt instead
+                    // to just continue using the fallback factory provisioned key.
+                    binder.deleteAllKeys();
+                    jobFinished(mParams, false /* wantsReschedule */);
+                    return;
+                }
+                for (int i = 0; i < implInfos.length; i++) {
+                    // Break very large CSR requests into chunks, so as not to overwhelm the
+                    // backend.
+                    int keysToCertify = keysNeededForSecLevel[i];
+                    while (keysToCertify != 0) {
+                        int batchSize = min(keysToCertify, SAFE_CSR_BATCH_SIZE);
+                        Provisioner.provisionCerts(batchSize,
+                                                   implInfos[i].secLevel,
+                                                   resp.getGeekChain(implInfos[i].supportedCurve),
+                                                   resp.getChallenge(),
+                                                   binder,
+                                                   mContext);
+                        keysToCertify -= batchSize;
+                    }
                 }
                 jobFinished(mParams, false /* wantsReschedule */);
             } catch (RemoteException e) {
-                jobFinished(mParams, true /* wantsReschedule */);
+                jobFinished(mParams, false /* wantsReschedule */);
                 Log.e(TAG, "Error on the binder side during provisioning.", e);
             } catch (InterruptedException e) {
-                jobFinished(mParams, true /* wantsReschedule */);
+                jobFinished(mParams, false /* wantsReschedule */);
                 Log.e(TAG, "Provisioner thread interrupted.", e);
             }
         }
 
-        private void checkAndProvision(IRemoteProvisioning binder, long expiringBy, int secLevel)
+        private boolean checkGeekResp(GeekResponse resp) {
+            if (resp == null) {
+                Log.e(TAG, "Failed to get a response from the server.");
+                if (SettingsManager.getFailureCounter(mContext) > FAILURE_MAXIMUM) {
+                    Log.e(TAG, "Too many failures, resetting defaults.");
+                    SettingsManager.clearPreferences(mContext);
+                }
+                jobFinished(mParams, false /* wantsReschedule */);
+                return false;
+            }
+            return true;
+        }
+
+        private boolean isProvisioningNeeded(
+                IRemoteProvisioning binder, long expiringBy, ImplInfo[] implInfos,
+                int[] keysNeededForSecLevel)
+                throws InterruptedException, RemoteException {
+            if (implInfos == null || keysNeededForSecLevel == null
+                || keysNeededForSecLevel.length != implInfos.length) {
+                Log.e(TAG, "Invalid argument.");
+                return false;
+            }
+            boolean provisioningNeeded = false;
+            for (int i = 0; i < implInfos.length; i++) {
+                keysNeededForSecLevel[i] =
+                        generateNumKeysNeeded(binder,
+                                   expiringBy,
+                                   implInfos[i].secLevel);
+                if (keysNeededForSecLevel[i] > 0) {
+                    provisioningNeeded = true;
+                }
+            }
+            return provisioningNeeded;
+        }
+
+        /**
+         * This method will generate and bundle up keys for signing to make sure that there will be
+         * enough keys available for use by the system when current keys expire.
+         *
+         * Enough keys is defined by checking how many keys are currently assigned to apps and
+         * generating enough keys to cover any expiring certificates plus a bit of buffer room
+         * defined by {@code sExtraSignedKeysAvailable}.
+         *
+         * This allows devices to dynamically resize their key pools as the user downloads and
+         * removes apps that may also use attestation.
+         */
+        private int generateNumKeysNeeded(IRemoteProvisioning binder, long expiringBy, int secLevel)
                 throws InterruptedException, RemoteException {
             AttestationPoolStatus pool = binder.getPoolStatus(expiringBy, secLevel);
-            int generated = 0;
-            while (generated + pool.total - pool.expiring < sTotalSignedKeys) {
-                generated++;
+            int unattestedKeys = pool.total - pool.attested;
+            int keysInUse = pool.attested - pool.unassigned;
+            int totalSignedKeys = keysInUse + SettingsManager.getExtraSignedKeysAvailable(mContext);
+            int generated;
+            // If nothing is expiring, and the amount of available unassigned keys is sufficient,
+            // then do nothing. Otherwise, generate the complete amount of totalSignedKeys. It will
+            // reduce network usage if the app just provisions an entire new batch in one go, rather
+            // than consistently grabbing just a few at a time as the expiration dates become
+            // misaligned.
+            if (pool.expiring > pool.unassigned && pool.attested == totalSignedKeys) {
+                return 0;
+            }
+            for (generated = 0;
+                    generated + unattestedKeys < totalSignedKeys; generated++) {
                 binder.generateKeyPair(false /* isTestMode */, secLevel);
-                Thread.sleep(5000);
+                // Prioritize provisioning if there are no keys available. No keys being available
+                // indicates that this is the first time a device is being brought online.
+                if (pool.total != 0) {
+                    Thread.sleep(KEY_GENERATION_PAUSE.toMillis());
+                }
             }
-            if (generated > 0) {
-                Log.d(TAG, "Keys generated, moving to provisioning process.");
-                Provisioner.provisionCerts(generated, secLevel, binder);
+            if (totalSignedKeys > 0) {
+                return generated + unattestedKeys;
             }
+            return 0;
         }
     }
 }
diff --git a/src/com/android/remoteprovisioner/Provisioner.java b/src/com/android/remoteprovisioner/Provisioner.java
index eabe7d3..06a7f4d 100644
--- a/src/com/android/remoteprovisioner/Provisioner.java
+++ b/src/com/android/remoteprovisioner/Provisioner.java
@@ -17,6 +17,7 @@
 package com.android.remoteprovisioner;
 
 import android.annotation.NonNull;
+import android.content.Context;
 import android.hardware.security.keymint.DeviceInfo;
 import android.hardware.security.keymint.ProtectedData;
 import android.security.remoteprovisioning.IRemoteProvisioning;
@@ -25,7 +26,7 @@
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Provides an easy package to run the provisioning process from start to finish, interfacing
@@ -36,57 +37,61 @@
     private static final String TAG = "RemoteProvisioningService";
 
     /**
-     * Drives the process of provisioning certs. The method first contacts the provided backend
-     * server to retrieve an Endpoing Encryption Key with an accompanying certificate chain and a
-     * challenge. It passes this data and the requested number of keys to the remote provisioning
-     * system backend, which then works with KeyMint in order to get a CSR bundle generated, along
-     * with an encrypted package containing metadata that the server needs in order to make
-     * decisions about provisioning.
+     * Drives the process of provisioning certs. The method passes the data fetched from the
+     * provisioning server along with the requested number of keys to the remote provisioning
+     * system backend. The backend will talk to the underlying IRemotelyProvisionedComponent
+     * interface in order to get a CSR bundle generated, along with an encrypted package containing
+     * metadata that the server needs in order to make decisions about provisioning.
      *
      * This method then passes that bundle back out to the server backend, waits for the response,
      * and, if successful, passes the certificate chains back to the remote provisioning service to
      * be stored and later assigned to apps requesting a key attestation.
      *
      * @param numKeys The number of keys to be signed. The service will do a best-effort to
-     *                     provision the number requested, but if the number requested is larger
-     *                     than the number of unsigned attestation key pairs available, it will
-     *                     only sign the number that is available at time of calling.
-     *
+     *                provision the number requested, but if the number requested is larger
+     *                than the number of unsigned attestation key pairs available, it will
+     *                only sign the number that is available at time of calling.
      * @param secLevel Which KM instance should be used to provision certs.
+     * @param geekChain The certificate chain that signs the endpoint encryption key.
+     * @param challenge A server provided challenge to ensure freshness of the response.
      * @param binder The IRemoteProvisioning binder interface needed by the method to handle talking
-     *                     to the remote provisioning system component.
-     *
-     * @return True if certificates were successfully provisioned for the signing keys.
+     *               to the remote provisioning system component.
+     * @param context The application context object which enables this method to make use of
+     *                SettingsManager.
+     * @return The number of certificates provisoned. Ideally, this should equal {@code numKeys}.
      */
-    public static boolean provisionCerts(int numKeys, int secLevel,
-            @NonNull IRemoteProvisioning binder) {
+    public static int provisionCerts(int numKeys, int secLevel, byte[] geekChain, byte[] challenge,
+            @NonNull IRemoteProvisioning binder, Context context) {
         if (numKeys < 1) {
             Log.e(TAG, "Request at least 1 key to be signed. Num requested: " + numKeys);
-            return false;
-        }
-        GeekResponse geek = ServerInterface.fetchGeek();
-        if (geek == null) {
-            Log.e(TAG, "The geek is null");
-            return false;
+            return 0;
         }
         DeviceInfo deviceInfo = new DeviceInfo();
         ProtectedData protectedData = new ProtectedData();
         byte[] macedKeysToSign =
-                SystemInterface.generateCsr(false /* testMode */, numKeys, secLevel, geek,
-                                            protectedData, deviceInfo, binder);
+                SystemInterface.generateCsr(false /* testMode */, numKeys, secLevel, geekChain,
+                                            challenge, protectedData, deviceInfo, binder);
         if (macedKeysToSign == null || protectedData.protectedData == null
                 || deviceInfo.deviceInfo == null) {
             Log.e(TAG, "Keystore failed to generate a payload");
-            return false;
+            return 0;
         }
         byte[] certificateRequest =
                 CborUtils.buildCertificateRequest(deviceInfo.deviceInfo,
-                                                  geek.challenge,
+                                                  challenge,
                                                   protectedData.protectedData,
                                                   macedKeysToSign);
-        ArrayList<byte[]> certChains =
-                new ArrayList<byte[]>(ServerInterface.requestSignedCertificates(
-                        certificateRequest, geek.challenge));
+        if (certificateRequest == null) {
+            Log.e(TAG, "Failed to serialize the payload generated by keystore.");
+            return 0;
+        }
+        List<byte[]> certChains = ServerInterface.requestSignedCertificates(context,
+                        certificateRequest, challenge);
+        if (certChains == null) {
+            Log.e(TAG, "Server response failed on provisioning attempt.");
+            return 0;
+        }
+        int provisioned = 0;
         for (byte[] certChain : certChains) {
             // DER encoding specifies leaf to root ordering. Pull the public key and expiration
             // date from the leaf.
@@ -95,21 +100,25 @@
                 cert = X509Utils.formatX509Certs(certChain)[0];
             } catch (CertificateException e) {
                 Log.e(TAG, "Failed to interpret DER encoded certificate chain", e);
-                return false;
+                return 0;
             }
             // getTime returns the time in *milliseconds* since the epoch.
             long expirationDate = cert.getNotAfter().getTime();
             byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(cert);
+            if (rawPublicKey == null) {
+                Log.e(TAG, "Skipping malformed public key.");
+                continue;
+            }
             try {
-                return SystemInterface.provisionCertChain(rawPublicKey, cert.getEncoded(),
-                                                          certChain, expirationDate, secLevel,
-                                                          binder);
+                if (SystemInterface.provisionCertChain(rawPublicKey, cert.getEncoded(), certChain,
+                                                       expirationDate, secLevel, binder)) {
+                    provisioned++;
+                }
             } catch (CertificateEncodingException e) {
                 Log.e(TAG, "Somehow can't re-encode the decoded batch cert...", e);
-                return false;
+                return provisioned;
             }
         }
-        Log.d(TAG, "Reaching this return statement implies the server returned 0 signed certs.");
-        return false;
+        return provisioned;
     }
 }
diff --git a/src/com/android/remoteprovisioner/ServerInterface.java b/src/com/android/remoteprovisioner/ServerInterface.java
index 576e913..0ce70ef 100644
--- a/src/com/android/remoteprovisioner/ServerInterface.java
+++ b/src/com/android/remoteprovisioner/ServerInterface.java
@@ -16,6 +16,7 @@
 
 package com.android.remoteprovisioner;
 
+import android.content.Context;
 import android.util.Base64;
 import android.util.Log;
 
@@ -36,30 +37,28 @@
     private static final int TIMEOUT_MS = 5000;
 
     private static final String TAG = "ServerInterface";
-    private static final String PROVISIONING_URL = "https://remoteprovisioning.googleapis.com";
-    private static final String GEEK_URL = PROVISIONING_URL + "/v1alpha1:fetchEekChain";
-    private static final String CERTIFICATE_SIGNING_URL =
-            PROVISIONING_URL + "/v1alpha1:signCertificates?challenge=";
+    private static final String GEEK_URL = ":fetchEekChain";
+    private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?challenge=";
 
     /**
      * Ferries the CBOR blobs returned by KeyMint to the provisioning server. The data sent to the
      * provisioning server contains the MAC'ed CSRs and encrypted bundle containing the MAC key and
      * the hardware unique public key.
      *
+     * @param context The application context which is required to use SettingsManager.
      * @param csr The CBOR encoded data containing the relevant pieces needed for the server to
      *                    sign the CSRs. The data encoded within comes from Keystore / KeyMint.
-     *
      * @param challenge The challenge that was sent from the server. It is included here even though
      *                    it is also included in `cborBlob` in order to allow the server to more
      *                    easily reject bad requests.
-     *
      * @return A List of byte arrays, where each array contains an entire DER-encoded certificate
      *                    chain for one attestation key pair.
      */
-    public static List<byte[]> requestSignedCertificates(byte[] csr, byte[] challenge) {
+    public static List<byte[]> requestSignedCertificates(Context context, byte[] csr,
+                                                         byte[] challenge) {
         try {
-            URL url = new URL(CERTIFICATE_SIGNING_URL
-                        + Base64.encodeToString(challenge, Base64.URL_SAFE));
+            URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL
+                              + Base64.encodeToString(challenge, Base64.URL_SAFE));
             HttpURLConnection con = (HttpURLConnection) url.openConnection();
             con.setRequestMethod("POST");
             con.setDoOutput(true);
@@ -69,15 +68,15 @@
             // the output stream being automatically closed.
             try (OutputStream os = con.getOutputStream()) {
                 os.write(csr, 0, csr.length);
-            } catch (Exception e) {
-                return null;
             }
 
             if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                int failures = SettingsManager.incrementFailureCounter(context);
                 Log.e(TAG, "Server connection for signing failed, response code: "
-                        + con.getResponseCode());
+                        + con.getResponseCode() + "\nRepeated failure count: " + failures);
                 return null;
             }
+            SettingsManager.clearFailureCounter(context);
             BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
             ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
             byte[] buffer = new byte[1024];
@@ -87,9 +86,11 @@
             }
             return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
         } catch (SocketTimeoutException e) {
+            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Server timed out", e);
             return null;
         } catch (IOException e) {
+            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Failed to request signed certificates from the server", e);
             return null;
         }
@@ -103,19 +104,30 @@
      *
      * A challenge is also returned from the server so that it can check freshness of the follow-up
      * request to get keys signed.
+     *
+     * @param context The application context which is required to use SettingsManager.
+     * @return A GeekResponse object which optionally contains configuration data.
      */
-    public static GeekResponse fetchGeek() {
+    public static GeekResponse fetchGeek(Context context) {
         try {
-            URL url = new URL(GEEK_URL);
+            URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL);
             HttpURLConnection con = (HttpURLConnection) url.openConnection();
-            con.setRequestMethod("GET");
+            con.setRequestMethod("POST");
             con.setConnectTimeout(TIMEOUT_MS);
+            con.setDoOutput(true);
+
+            byte[] config = CborUtils.buildProvisioningInfo(context);
+            try (OutputStream os = con.getOutputStream()) {
+                os.write(config, 0, config.length);
+            }
 
             if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                int failures = SettingsManager.incrementFailureCounter(context);
                 Log.e(TAG, "Server connection for GEEK failed, response code: "
-                        + con.getResponseCode());
+                        + con.getResponseCode() + "\nRepeated failure count: " + failures);
                 return null;
             }
+            SettingsManager.clearFailureCounter(context);
 
             BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
             ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
@@ -127,8 +139,11 @@
             inputStream.close();
             return CborUtils.parseGeekResponse(cborBytes.toByteArray());
         } catch (SocketTimeoutException e) {
+            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Server timed out", e);
         } catch (IOException e) {
+            // This exception will trigger on a completely malformed URL.
+            SettingsManager.incrementFailureCounter(context);
             Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
         }
         return null;
diff --git a/src/com/android/remoteprovisioner/SettingsManager.java b/src/com/android/remoteprovisioner/SettingsManager.java
new file mode 100644
index 0000000..5808475
--- /dev/null
+++ b/src/com/android/remoteprovisioner/SettingsManager.java
@@ -0,0 +1,188 @@
+/**
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.remoteprovisioner;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.time.Duration;
+import java.util.Random;
+
+/**
+ * SettingsManager makes use of SharedPreferences in order to store key/value pairs related to
+ * configuration settings that can be retrieved from the server. In the event that none have yet
+ * been retrieved, or for some reason a reset has occurred, there are reasonable default values.
+ */
+public class SettingsManager {
+
+    public static final int ID_UPPER_BOUND = 1000000;
+    public static final int EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT = 6;
+    // Check for expiring certs in the next 3 days
+    public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3;
+    public static final String URL_DEFAULT = "https://remoteprovisioning.googleapis.com/v1";
+
+    private static final String KEY_EXPIRING_BY = "expiring_by";
+    private static final String KEY_EXTRA_KEYS = "extra_keys";
+    private static final String KEY_ID = "settings_id";
+    private static final String KEY_FAILURE_COUNTER = "failure_counter";
+    private static final String KEY_URL = "url";
+    private static final String PREFERENCES_NAME = "com.android.remoteprovisioner.preferences";
+    private static final String TAG = "RemoteProvisionerSettings";
+
+    /**
+     * Generates a random ID for the use of gradual ramp up of remote provisioning.
+     */
+    public static void generateAndSetId(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        if (sharedPref.contains(KEY_ID)) {
+            // ID is already set, don't rotate it.
+            return;
+        }
+        Log.i(TAG, "Setting ID");
+        Random rand = new Random();
+        SharedPreferences.Editor editor = sharedPref.edit();
+        editor.putInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND));
+        editor.apply();
+    }
+
+    /**
+     * Fetches the generated ID.
+     */
+    public static int getId(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        Random rand = new Random();
+        return sharedPref.getInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND) /* defaultValue */);
+    }
+
+    /**
+     * Sets the remote provisioning configuration values based on what was fetched from the server.
+     * The server is not guaranteed to have sent every available parameter in the config that
+     * was returned to the device, so the parameters should be checked for null values.
+     *
+     * @param extraKeys How many server signed remote provisioning key pairs that should be kept
+     *                  available in KeyStore.
+     * @param expiringBy How far in the future the app should check for expiring keys.
+     * @param url The base URL for the provisioning server.
+     * @return {@code true} if any settings were updated.
+     */
+    public static boolean setDeviceConfig(Context context, int extraKeys,
+                                          Duration expiringBy, String url) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sharedPref.edit();
+        boolean wereUpdatesMade = false;
+        if (extraKeys != GeekResponse.NO_EXTRA_KEY_UPDATE
+                && sharedPref.getInt(KEY_EXTRA_KEYS, -5) != extraKeys) {
+            editor.putInt(KEY_EXTRA_KEYS, extraKeys);
+            wereUpdatesMade = true;
+        }
+        if (expiringBy != null
+                && sharedPref.getLong(KEY_EXPIRING_BY, -1) != expiringBy.toMillis()) {
+            editor.putLong(KEY_EXPIRING_BY, expiringBy.toMillis());
+            wereUpdatesMade = true;
+        }
+        if (url != null && !sharedPref.getString(KEY_URL, "").equals(url)) {
+            editor.putString(KEY_URL, url);
+            wereUpdatesMade = true;
+        }
+        if (wereUpdatesMade) {
+            editor.apply();
+        }
+        return wereUpdatesMade;
+    }
+
+    /**
+     * Gets the setting for how many extra keys should be kept signed and available in KeyStore.
+     */
+    public static int getExtraSignedKeysAvailable(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return sharedPref.getInt(KEY_EXTRA_KEYS, EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT);
+    }
+
+    /**
+     * Gets the setting for how far into the future the provisioner should check for expiring keys.
+     */
+    public static Duration getExpiringBy(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return Duration.ofMillis(sharedPref.getLong(KEY_EXPIRING_BY, EXPIRING_BY_MS_DEFAULT));
+    }
+
+    /**
+     * Gets the setting for what base URL the provisioner should use to talk to provisioning
+     * servers.
+     */
+    public static String getUrl(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return sharedPref.getString(KEY_URL, URL_DEFAULT);
+    }
+
+    /**
+     * Increments the failure counter. This is intended to be used when reaching the server fails
+     * for any reason so that the app logic can decide if the preferences should be reset to
+     * defaults in the event that a bad push stored an incorrect URL string.
+     *
+     * @return the current failure counter after incrementing.
+     */
+    public static int incrementFailureCounter(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sharedPref.edit();
+        int failures = sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */);
+        editor.putInt(KEY_FAILURE_COUNTER, ++failures);
+        editor.apply();
+        return failures;
+    }
+
+    /**
+     * Gets the current failure counter.
+     */
+    public static int getFailureCounter(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        return sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */);
+    }
+
+    /**
+     * Resets the failure counter to {@code 0}.
+     */
+    public static void clearFailureCounter(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        if (sharedPref.getInt(KEY_FAILURE_COUNTER, 0) != 0) {
+            SharedPreferences.Editor editor = sharedPref.edit();
+            editor.putInt(KEY_FAILURE_COUNTER, 0);
+            editor.apply();
+        }
+    }
+
+    /**
+     * Clears all preferences, thus restoring the defaults.
+     */
+    public static void clearPreferences(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sharedPref.edit();
+        editor.clear();
+        editor.apply();
+    }
+}
diff --git a/src/com/android/remoteprovisioner/SystemInterface.java b/src/com/android/remoteprovisioner/SystemInterface.java
index 160f185..67ab028 100644
--- a/src/com/android/remoteprovisioner/SystemInterface.java
+++ b/src/com/android/remoteprovisioner/SystemInterface.java
@@ -68,18 +68,22 @@
      * Sends a generateCsr request over the binder interface. `dataBlob` is an out parameter that
      * will be populated by the underlying binder service.
      */
-    public static byte[] generateCsr(boolean testMode, int numKeys, int secLevel, GeekResponse geek,
-            ProtectedData protectedData, DeviceInfo deviceInfo,
+    public static byte[] generateCsr(boolean testMode, int numKeys, int secLevel,
+            byte[] geekChain, byte[] challenge, ProtectedData protectedData, DeviceInfo deviceInfo,
             @NonNull IRemoteProvisioning binder) {
         try {
             ProtectedData dataBundle = new ProtectedData();
             byte[] macedPublicKeys = binder.generateCsr(testMode,
                                                         numKeys,
-                                                        geek.geek,
-                                                        geek.challenge,
+                                                        geekChain,
+                                                        challenge,
                                                         secLevel,
                                                         protectedData,
                                                         deviceInfo);
+            if (macedPublicKeys == null) {
+                Log.e(TAG, "Keystore didn't generate a CSR successfully.");
+                return null;
+            }
             ByteArrayInputStream bais = new ByteArrayInputStream(macedPublicKeys);
             List<DataItem> dataItems = new CborDecoder(bais).decode();
             List<DataItem> macInfo = ((Array) dataItems.get(0)).getDataItems();
diff --git a/src/com/android/remoteprovisioner/X509Utils.java b/src/com/android/remoteprovisioner/X509Utils.java
index 60fee4a..d33d573 100644
--- a/src/com/android/remoteprovisioner/X509Utils.java
+++ b/src/com/android/remoteprovisioner/X509Utils.java
@@ -16,8 +16,11 @@
 
 package com.android.remoteprovisioner;
 
+import android.util.Log;
+
 import java.io.ByteArrayInputStream;
 import java.math.BigInteger;
+import java.security.PublicKey;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
@@ -30,6 +33,8 @@
  */
 public class X509Utils {
 
+    private static final String TAG = "RemoteProvisionerX509Utils";
+
     /**
      * Takes a byte array composed of DER encoded certificates and returns the X.509 certificates
      * contained within as an X509Certificate array.
@@ -47,6 +52,11 @@
      * the certificate chain to the proper key when passed into the keystore database.
      */
     public static byte[] getAndFormatRawPublicKey(X509Certificate cert) {
+        PublicKey pubKey = cert.getPublicKey();
+        if (!(pubKey instanceof ECPublicKey)) {
+            Log.e(TAG, "Certificate public key is not an instance of ECPublicKey");
+            return null;
+        }
         ECPublicKey key = (ECPublicKey) cert.getPublicKey();
         // Remote key provisioning internally supports the default, uncompressed public key
         // format for ECDSA. This defines the format as (s | x | y), where s is the byte
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index f505d28..73c83b1 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -17,21 +17,27 @@
 package com.android.remoteprovisioner.service;
 
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.security.IGenerateRkpKeyService;
 import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
 import android.security.remoteprovisioning.IRemoteProvisioning;
 import android.util.Log;
 
+import com.android.remoteprovisioner.GeekResponse;
 import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
 
 /**
  * Provides the implementation for IGenerateKeyService.aidl
  */
 public class GenerateRkpKeyService extends Service {
+    private static final int KEY_GENERATION_PAUSE_MS = 1000;
     private static final String SERVICE = "android.security.remoteprovisioning";
     private static final String TAG = "RemoteProvisioningService";
 
@@ -51,7 +57,6 @@
             try {
                 IRemoteProvisioning binder =
                         IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
-                // Iterate through each security level backend
                 checkAndFillPool(binder, securityLevel);
             } catch (RemoteException e) {
                 Log.e(TAG, "Remote Exception: ", e);
@@ -63,7 +68,6 @@
             try {
                 IRemoteProvisioning binder =
                         IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
-                // Iterate through each security level backend
                 checkAndFillPool(binder, securityLevel);
             } catch (RemoteException e) {
                 Log.e(TAG, "Remote Exception: ", e);
@@ -74,13 +78,39 @@
                 throws RemoteException {
             AttestationPoolStatus pool =
                     binder.getPoolStatus(System.currentTimeMillis(), secLevel);
-            // If there are no unassigned keys, go ahead and provision some. If there are no keys
-            // at all on system, this implies that it is a hybrid rkp/factory-provisioned system
-            // that has turned off RKP. In that case, do not provision.
-            if (pool.unassigned == 0 && pool.total != 0) {
-                Log.d(TAG, "All signed keys are currently in use, provisioning more.");
-                binder.generateKeyPair(false /* isTestMode */, secLevel);
-                Provisioner.provisionCerts(1 /* numCsr */, secLevel, binder);
+            ImplInfo[] implInfos = binder.getImplementationInfo();
+            int curve = 0;
+            for (int i = 0; i < implInfos.length; i++) {
+                if (implInfos[i].secLevel == secLevel) {
+                    curve = implInfos[i].supportedCurve;
+                    break;
+                }
+            }
+            // If there are no unassigned keys, go ahead and provision some. If there are no
+            // attested keys at all on the system, this implies that it is a hybrid
+            // rkp/factory-provisioned system that has turned off RKP. In that case, do
+            // not provision.
+            if (pool.unassigned == 0 && pool.attested != 0) {
+                Log.i(TAG, "All signed keys are currently in use, provisioning more.");
+                Context context = getApplicationContext();
+                int keysToProvision = SettingsManager.getExtraSignedKeysAvailable(context);
+                int existingUnsignedKeys = pool.total - pool.attested;
+                int keysToGenerate = keysToProvision - existingUnsignedKeys;
+                try {
+                    for (int i = 0; i < keysToGenerate; i++) {
+                        binder.generateKeyPair(false /* isTestMode */, secLevel);
+                        Thread.sleep(KEY_GENERATION_PAUSE_MS);
+                    }
+                } catch (InterruptedException e) {
+                    Log.i(TAG, "Thread interrupted", e);
+                }
+                GeekResponse resp = ServerInterface.fetchGeek(context);
+                if (resp == null) {
+                    Log.e(TAG, "Server unavailable");
+                    return;
+                }
+                Provisioner.provisionCerts(keysToProvision, secLevel, resp.getGeekChain(curve),
+                                           resp.getChallenge(), binder, context);
             }
         }
     };
diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp
index c2ed374..2aafdc7 100644
--- a/tests/unittests/Android.bp
+++ b/tests/unittests/Android.bp
@@ -19,6 +19,7 @@
     name: "RemoteProvisionerUnitTests",
     srcs: ["src/**/*.java"],
     static_libs: [
+        "androidx.test.core",
         "androidx.test.rules",
         "android.security.remoteprovisioning-java",
         "platform-test-annotations",
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
index 4c30a6e..cc561a8 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
@@ -31,8 +31,12 @@
 import co.nstant.in.cbor.CborDecoder;
 import co.nstant.in.cbor.CborEncoder;
 import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.ByteString;
 import co.nstant.in.cbor.model.DataItem;
 import co.nstant.in.cbor.model.MajorType;
+import co.nstant.in.cbor.model.Map;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -47,10 +51,48 @@
 public class CborUtilsTest {
 
     private ByteArrayOutputStream mBaos;
+    private Array mGeekChain1;
+    private byte[] mEncodedmGeekChain1;
+    private Array mGeekChain2;
+    private byte[] mEncodedGeekChain2;
+    private Map mDeviceConfig;
+    private static final byte[] CHALLENGE = new byte[]{0x0a, 0x0b, 0x0c};
+    private static final int TEST_EXTRA_KEYS = 18;
+    private static final int TEST_TIME_TO_REFRESH_HOURS = 42;
+    private static final String TEST_URL = "https://www.wonderifthisisvalid.combutjustincase";
+
+    private byte[] encodeDataItem(DataItem toEncode) throws Exception {
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                        .add(toEncode)
+                        .build());
+        byte[] encoded = mBaos.toByteArray();
+        mBaos.reset();
+        return encoded;
+    }
 
     @Before
     public void setUp() throws Exception {
         mBaos = new ByteArrayOutputStream();
+
+        mGeekChain1 = new Array();
+        mGeekChain1.add(new ByteString(new byte[] {0x01, 0x02, 0x03}))
+                   .add(new ByteString(new byte[] {0x04, 0x05, 0x06}))
+                   .add(new ByteString(new byte[] {0x07, 0x08, 0x09}));
+        mEncodedmGeekChain1 = encodeDataItem(mGeekChain1);
+
+        mGeekChain2 = new Array();
+        mGeekChain2.add(new ByteString(new byte[] {0x09, 0x08, 0x07}))
+                   .add(new ByteString(new byte[] {0x06, 0x05, 0x04}))
+                   .add(new ByteString(new byte[] {0x03, 0x02, 0x01}));
+        mEncodedGeekChain2 = encodeDataItem(mGeekChain2);
+
+        mDeviceConfig = new Map();
+        mDeviceConfig.put(new UnicodeString(CborUtils.EXTRA_KEYS),
+                          new UnsignedInteger(TEST_EXTRA_KEYS))
+                     .put(new UnicodeString(CborUtils.TIME_TO_REFRESH),
+                          new UnsignedInteger(TEST_TIME_TO_REFRESH_HOURS))
+                     .put(new UnicodeString(CborUtils.PROVISIONING_URL),
+                          new UnicodeString(TEST_URL));
     }
 
     @Presubmit
@@ -116,39 +158,182 @@
     public void testParseGeekResponseFakeData() throws Exception {
         new CborEncoder(mBaos).encode(new CborBuilder()
                 .addArray()
-                    .addArray()                                       // GEEK Chain
-                        .add(new byte[] {0x01, 0x02, 0x03})
-                        .add(new byte[] {0x04, 0x05, 0x06})
-                        .add(new byte[] {0x07, 0x08, 0x09})
+                    .addArray()                                       // GEEK Curve to Chains
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+                            .add(mGeekChain2)
+                            .end()
                         .end()
-                    .add(new byte[] {0x0a, 0x0b, 0x0c})               // Challenge
+                    .add(CHALLENGE)
+                    .add(mDeviceConfig)
                     .end()
                 .build());
         GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
         mBaos.reset();
+        assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+        assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+        assertArrayEquals(CHALLENGE, resp.getChallenge());
+        assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+        assertEquals(TEST_TIME_TO_REFRESH_HOURS, resp.timeToRefresh.toHours());
+        assertEquals(TEST_URL, resp.provisioningUrl);
+    }
+
+    @Test
+    public void testExtraDeviceConfigEntriesDontFail() throws Exception {
         new CborEncoder(mBaos).encode(new CborBuilder()
                 .addArray()
-                    .add(new byte[] {0x01, 0x02, 0x03})
-                    .add(new byte[] {0x04, 0x05, 0x06})
-                    .add(new byte[] {0x07, 0x08, 0x09})
+                    .addArray()                                       // GEEK Curve to Chains
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+                            .add(mGeekChain2)
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .add(mDeviceConfig.put(new UnicodeString("new_field"),
+                                          new UnsignedInteger(84)))
                     .end()
                 .build());
-        byte[] expectedGeek = mBaos.toByteArray();
-        assertArrayEquals(expectedGeek, resp.geek);
-        assertArrayEquals(new byte[] {0x0a, 0x0b, 0x0c}, resp.challenge);
+        GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+        mBaos.reset();
+        assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+        assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+        assertArrayEquals(CHALLENGE, resp.getChallenge());
+        assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+        assertEquals(TEST_TIME_TO_REFRESH_HOURS, resp.timeToRefresh.toHours());
+        assertEquals(TEST_URL, resp.provisioningUrl);
+    }
+
+    @Test
+    public void testMissingDeviceConfigDoesntFail() throws Exception {
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()                                       // GEEK Curve to Chains
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+                            .add(mGeekChain2)
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .end()
+                .build());
+        GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+        mBaos.reset();
+        assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+        assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+        assertArrayEquals(CHALLENGE, resp.getChallenge());
+        assertEquals(GeekResponse.NO_EXTRA_KEY_UPDATE, resp.numExtraAttestationKeys);
+        assertEquals(null, resp.timeToRefresh);
+        assertEquals(null, resp.provisioningUrl);
+    }
+
+    @Test
+    public void testMissingDeviceConfigEntriesDoesntFail() throws Exception {
+        mDeviceConfig.remove(new UnicodeString(CborUtils.TIME_TO_REFRESH));
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()                                       // GEEK Curve to Chains
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+                            .add(mGeekChain2)
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .add(mDeviceConfig)
+                    .end()
+                .build());
+        GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+        mBaos.reset();
+        assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+        assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+        assertArrayEquals(CHALLENGE, resp.getChallenge());
+        assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+        assertEquals(null, resp.timeToRefresh);
+        assertEquals(TEST_URL, resp.provisioningUrl);
+    }
+
+    @Test
+    public void testParseGeekResponseFailsOnWrongType() throws Exception {
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()
+                        .addArray()
+                            .add("String instead of curve enum")
+                            .add(mGeekChain1)
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .add(mDeviceConfig)
+                    .end()
+                .build());
+        assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+        mBaos.reset();
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(new ByteString(CHALLENGE)) // Must be an array of bstrs
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .add(mDeviceConfig)
+                    .end()
+                .build());
+        assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+        mBaos.reset();
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .end()
+                    .add(new UnicodeString("tstr instead of bstr"))
+                    .add(mDeviceConfig)
+                    .end()
+                .build());
+        assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+        mBaos.reset();
+        new CborEncoder(mBaos).encode(new CborBuilder()
+                .addArray()
+                    .addArray()
+                        .addArray()
+                            .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+                            .add(mGeekChain1)
+                            .end()
+                        .end()
+                    .add(CHALLENGE)
+                    .add(CHALLENGE)
+                    .end()
+                .build());
+        assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
     }
 
     @Test
     public void testParseGeekResponseWrongSize() throws Exception {
         new CborEncoder(mBaos).encode(new CborBuilder()
                 .addArray()
-                    .addArray()
-                        .add(new byte[] {0x01, 0x02, 0x03})
-                        .add(new byte[] {0x04, 0x05, 0x06})
-                        .add(new byte[] {0x07, 0x08, 0x09})
-                        .end()
-                    .add(new byte[] {0x0a, 0x0b, 0x0c})
-                    .add("One more entry than there should be")
+                    .add("one entry")
+                    .add("two entries")
+                    .add("three entries")
+                    .add("whoops")
                     .end()
                 .build());
         assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
new file mode 100644
index 0000000..94206af
--- /dev/null
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.remoteprovisioner.unittest;
+
+import static android.hardware.security.keymint.SecurityLevel.TRUSTED_ENVIRONMENT;
+import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC;
+import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.ServiceManager;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
+import android.security.remoteprovisioning.IRemoteProvisioning;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.remoteprovisioner.GeekResponse;
+import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class ServerToSystemTest {
+
+    private static final boolean IS_TEST_MODE = false;
+    private static final String SERVICE = "android.security.remoteprovisioning";
+
+    private static Context sContext;
+    private static IRemoteProvisioning sBinder;
+    private static int sCurve = 0;
+
+    private Duration mDuration;
+
+    private void assertPoolStatus(int total, int attested,
+                                  int unassigned, int expiring, Duration time) throws Exception {
+        AttestationPoolStatus pool = sBinder.getPoolStatus(time.toMillis(), TRUSTED_ENVIRONMENT);
+        assertEquals(total, pool.total);
+        assertEquals(attested, pool.attested);
+        assertEquals(unassigned, pool.unassigned);
+        assertEquals(expiring, pool.expiring);
+    }
+
+    private static Certificate[] generateKeyStoreKey(String alias) throws Exception {
+        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+        keyStore.load(null);
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_EC,
+                "AndroidKeyStore");
+        KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, PURPOSE_SIGN)
+                .setAttestationChallenge("challenge".getBytes())
+                .build();
+        keyPairGenerator.initialize(spec);
+        keyPairGenerator.generateKeyPair();
+        Certificate[] certs = keyStore.getCertificateChain(spec.getKeystoreAlias());
+        keyStore.deleteEntry(alias);
+        return certs;
+    }
+
+    @BeforeClass
+    public static void init() throws Exception {
+        sContext = ApplicationProvider.getApplicationContext();
+        sBinder =
+              IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+        assertNotNull(sBinder);
+        ImplInfo[] info = sBinder.getImplementationInfo();
+        for (int i = 0; i < info.length; i++) {
+            if (info[i].secLevel == TRUSTED_ENVIRONMENT) {
+                sCurve = info[i].supportedCurve;
+                break;
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        SettingsManager.clearPreferences(sContext);
+        sBinder.deleteAllKeys();
+        mDuration = Duration.ofMillis(System.currentTimeMillis());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        SettingsManager.clearPreferences(sContext);
+        sBinder.deleteAllKeys();
+    }
+
+    @Test
+    public void testFullRoundTrip() throws Exception {
+        int numTestKeys = 1;
+        assertPoolStatus(0, 0, 0, 0, mDuration);
+        sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+        assertPoolStatus(numTestKeys, 0, 0, 0, mDuration);
+        GeekResponse geek = ServerInterface.fetchGeek(sContext);
+        assertNotNull(geek);
+        int numProvisioned =
+                Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
+                                           geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
+                                           sContext);
+        assertEquals(numTestKeys, numProvisioned);
+        assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
+        // Certificate duration sent back from the server may change, however ~6 months should be
+        // pretty safe.
+        assertPoolStatus(numTestKeys, numTestKeys, numTestKeys,
+                         numTestKeys, mDuration.plusDays(180));
+    }
+
+    @Test
+    public void testFallback() throws Exception {
+        // Feed a fake URL into the device config to ensure that remote provisioning fails.
+        SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */,
+                                        "Not even a URL" /* url */);
+        int numTestKeys = 1;
+        assertPoolStatus(0, 0, 0, 0, mDuration);
+        Certificate[] fallbackKeyCerts1 = generateKeyStoreKey("test1");
+
+        SettingsManager.clearPreferences(sContext);
+        sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+        GeekResponse geek = ServerInterface.fetchGeek(sContext);
+        int numProvisioned =
+                Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
+                                           geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
+                                           sContext);
+        assertEquals(numTestKeys, numProvisioned);
+        assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
+        Certificate[] provisionedKeyCerts = generateKeyStoreKey("test2");
+        sBinder.deleteAllKeys();
+        sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+
+        SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */,
+                                        "Not even a URL" /* url */);
+        // Even if there is an unsigned key hanging around, fallback should still occur.
+        Certificate[] fallbackKeyCerts2 = generateKeyStoreKey("test3");
+        // Due to there being no attested keys in the pool, the provisioning service should not
+        // have even attempted to provision more certificates.
+        assertEquals(0, SettingsManager.getFailureCounter(sContext));
+        assertTrue(fallbackKeyCerts1.length == fallbackKeyCerts2.length);
+        for (int i = 1; i < fallbackKeyCerts1.length; i++) {
+            assertArrayEquals("Cert: " + i, fallbackKeyCerts1[i].getEncoded(),
+                              fallbackKeyCerts2[i].getEncoded());
+        }
+        assertTrue(provisionedKeyCerts.length > 0);
+        // The root certificates should not match.
+        assertFalse("Provisioned and fallback attestation key root certificates match.",
+                    Arrays.equals(fallbackKeyCerts1[fallbackKeyCerts1.length - 1].getEncoded(),
+                              provisionedKeyCerts[provisionedKeyCerts.length - 1].getEncoded()));
+    }
+}
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
new file mode 100644
index 0000000..7db20a9
--- /dev/null
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.remoteprovisioner.unittest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.remoteprovisioner.SettingsManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+
+@RunWith(AndroidJUnit4.class)
+public class SettingsManagerTest {
+
+    private static Context sContext;
+
+    @BeforeClass
+    public static void init() {
+        sContext = ApplicationProvider.getApplicationContext();
+    }
+
+    @Before
+    public void setUp() {
+        SettingsManager.clearPreferences(sContext);
+    }
+
+    @After
+    public void tearDown() {
+        SettingsManager.clearPreferences(sContext);
+    }
+
+    @Test
+    public void testCheckDefaults() throws Exception {
+        assertEquals(SettingsManager.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+                     SettingsManager.getExtraSignedKeysAvailable(sContext));
+        assertEquals(SettingsManager.EXPIRING_BY_MS_DEFAULT,
+                     SettingsManager.getExpiringBy(sContext).toMillis());
+        assertEquals(SettingsManager.URL_DEFAULT,
+                     SettingsManager.getUrl(sContext));
+        assertEquals(0, SettingsManager.getFailureCounter(sContext));
+    }
+
+    @Test
+    public void testCheckIdSettings() throws Exception {
+        int defaultRandom = SettingsManager.getId(sContext);
+        assertTrue("Default ID out of bounds.",
+                defaultRandom < SettingsManager.ID_UPPER_BOUND && defaultRandom >= 0);
+        SettingsManager.generateAndSetId(sContext);
+        int setId = SettingsManager.getId(sContext);
+        assertTrue("Stored ID out of bounds.",
+                setId < SettingsManager.ID_UPPER_BOUND && setId >= 0);
+        SettingsManager.generateAndSetId(sContext);
+        assertEquals("ID should not be updated by a repeated call",
+                     setId, SettingsManager.getId(sContext));
+    }
+
+    @Test
+    public void testSetDeviceConfig() {
+        int extraKeys = 12;
+        Duration expiringBy = Duration.ofMillis(1000);
+        String url = "https://www.remoteprovisionalot";
+        assertTrue("Method did not return true on write.",
+                   SettingsManager.setDeviceConfig(sContext, extraKeys, expiringBy, url));
+        assertEquals(extraKeys, SettingsManager.getExtraSignedKeysAvailable(sContext));
+        assertEquals(expiringBy.toMillis(), SettingsManager.getExpiringBy(sContext).toMillis());
+        assertEquals(url, SettingsManager.getUrl(sContext));
+    }
+
+    @Test
+    public void testFailureCounter() {
+        assertEquals(1, SettingsManager.incrementFailureCounter(sContext));
+        assertEquals(1, SettingsManager.getFailureCounter(sContext));
+        for (int i = 1; i < 10; i++) {
+            assertEquals(i + 1, SettingsManager.incrementFailureCounter(sContext));
+        }
+        SettingsManager.clearFailureCounter(sContext);
+        assertEquals(0, SettingsManager.getFailureCounter(sContext));
+        SettingsManager.incrementFailureCounter(sContext);
+        assertEquals(1, SettingsManager.getFailureCounter(sContext));
+    }
+}
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
index 2a2adc4..44be452 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
@@ -38,7 +38,7 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.remoteprovisioner.GeekResponse;
+import com.android.remoteprovisioner.CborUtils;
 import com.android.remoteprovisioner.SystemInterface;
 import com.android.remoteprovisioner.X509Utils;
 
@@ -119,11 +119,11 @@
         ProtectedData encryptedBundle = new ProtectedData();
         byte[] eek = new byte[32];
         new Random().nextBytes(eek);
-        GeekResponse geek = new GeekResponse(generateEekChain(eek), new byte[] {0x02});
         byte[] bundle =
             SystemInterface.generateCsr(true /* testMode */, 0 /* numKeys */,
                                         SecurityLevel.TRUSTED_ENVIRONMENT,
-                                        geek, encryptedBundle, deviceInfo, mBinder);
+                                        generateEekChain(eek),
+                                        new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
         // encryptedBundle should contain a COSE_Encrypt message
         ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
         List<DataItem> dataItems = new CborDecoder(bais).decode();
@@ -156,14 +156,14 @@
         int numKeys = 10;
         byte[] eek = new byte[32];
         new Random().nextBytes(eek);
-        GeekResponse geek = new GeekResponse(generateEekChain(eek), new byte[] {0x02});
         for (int i = 0; i < numKeys; i++) {
             mBinder.generateKeyPair(true /* testMode */, SecurityLevel.TRUSTED_ENVIRONMENT);
         }
         byte[] bundle =
             SystemInterface.generateCsr(true /* testMode */, numKeys,
                                         SecurityLevel.TRUSTED_ENVIRONMENT,
-                                        geek, encryptedBundle, deviceInfo, mBinder);
+                                        generateEekChain(eek),
+                                        new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
         assertNotNull(bundle);
         // The return value of generateCsr should be a COSE_Mac0 message
         ByteArrayInputStream bais = new ByteArrayInputStream(bundle);
@@ -280,12 +280,12 @@
         int numKeys = 1;
         byte[] eekPriv = X25519.generatePrivateKey();
         byte[] eekPub = X25519.publicFromPrivate(eekPriv);
-        GeekResponse geek = new GeekResponse(generateEekChain(eekPub), new byte[] {0x02});
         mBinder.generateKeyPair(true /* testMode */, SecurityLevel.TRUSTED_ENVIRONMENT);
         byte[] bundle =
             SystemInterface.generateCsr(true /* testMode */, numKeys,
                                         SecurityLevel.TRUSTED_ENVIRONMENT,
-                                        geek, encryptedBundle, deviceInfo, mBinder);
+                                        generateEekChain(eekPub),
+                                        new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
         ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
         List<DataItem> dataItems = new CborDecoder(bais).decode();
         // Parse encMsg into components: protected and unprotected headers, payload, and recipient