Improve practical performance of key monitoring.

This change fixes:
1) The potential for there to be a period between when the pool is last
   regularly checked and when the expired keys are cleaned out by making
   GenerateRkpKeyService much more proactive about triggering the
   reprovisioning process. It now bases the decision based on the same
   logic as the JobSchedule'd code, instead of just trying to act as an
   absolute last chance stopgap.

2) Unnecessarily overeager provisioning. The code will now allow the
   number of unassigned keys to drop below the "ideal" amount set by the
   server and avoid reprovisioning until a minimum remaining threshold
   is crossed. That threshold is currently set to 25% of the ideal
   amount. E.g. a system that is instructed to keep 20 extra attestation
   keys available for assignment won't trigger reprovisioning to top up
   the key pool unless fewer than 5 remain. This does not change the
   behavior when keys are expiring.

This change also refactors the code a bit to follow DRY, while also
increasing the testability of the logic that determines if provisioning
is needed.

Bug: 223489842
Test: atest RemoteProvisionerUnitTests
Change-Id: Idbd767202c30a24e3522ff431dec2d66d2ef631b
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index 418a69c..199632c 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -134,18 +134,9 @@
             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);
-                    Log.i(TAG, "Requesting " + batchSize + " keys to be provisioned.");
-                    Provisioner.provisionCerts(batchSize,
-                                               implInfos[i].secLevel,
-                                               resp.getGeekChain(implInfos[i].supportedCurve),
-                                               resp.getChallenge(),
-                                               binder,
-                                               mContext);
-                    keysToCertify -= batchSize;
-                }
+                int keysToProvision = keysNeededForSecLevel[i];
+                batchProvision(binder, mContext, keysToProvision, implInfos[i].secLevel,
+                               resp.getGeekChain(implInfos[i].supportedCurve), resp.getChallenge());
             }
             return Result.success();
         } catch (RemoteException e) {
@@ -157,6 +148,23 @@
         }
     }
 
+    public static void batchProvision(IRemoteProvisioning binder, Context context,
+                               int keysToProvision, int secLevel,
+                               byte[] geekChain, byte[] challenge)
+                               throws RemoteException {
+        while (keysToProvision != 0) {
+            int batchSize = min(keysToProvision, SAFE_CSR_BATCH_SIZE);
+            Log.i(TAG, "Requesting " + batchSize + " keys to be provisioned.");
+            Provisioner.provisionCerts(batchSize,
+                                       secLevel,
+                                       geekChain,
+                                       challenge,
+                                       binder,
+                                       context);
+            keysToProvision -= batchSize;
+        }
+    }
+
     private boolean checkGeekResp(GeekResponse resp) {
         if (resp == null) {
             Log.e(TAG, "Failed to get a response from the server.");
@@ -182,6 +190,7 @@
         for (int i = 0; i < implInfos.length; i++) {
             keysNeededForSecLevel[i] =
                     generateNumKeysNeeded(binder,
+                               mContext,
                                expiringBy,
                                implInfos[i].secLevel);
             if (keysNeededForSecLevel[i] > 0) {
@@ -202,7 +211,8 @@
      * 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)
+    public static int generateNumKeysNeeded(IRemoteProvisioning binder, Context context,
+                                            long expiringBy, int secLevel)
             throws InterruptedException, RemoteException {
         AttestationPoolStatus pool =
                 SystemInterface.getPoolStatus(expiringBy, secLevel, binder);
@@ -214,22 +224,15 @@
                    + "\nAttested: " + pool.attested
                    + "\nUnassigned: " + pool.unassigned
                    + "\nExpiring: " + pool.expiring);
-        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) {
-            Log.i(TAG,
-                    "No keys expiring and the expected number of attested keys are available");
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(
+                    pool, SettingsManager.getExtraSignedKeysAvailable(context));
+        if (!stats.provisioningNeeded) {
+            Log.i(TAG, "No provisioning needed.");
             return 0;
         }
-        for (generated = 0;
-                generated + unattestedKeys < totalSignedKeys; generated++) {
+        Log.i(TAG, "Need to generate " + stats.keysToGenerate + " keys.");
+        int generated;
+        for (generated = 0; generated < stats.keysToGenerate; generated++) {
             SystemInterface.generateKeyPair(false /* isTestMode */, secLevel, binder);
             // 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.
@@ -237,12 +240,10 @@
                 Thread.sleep(KEY_GENERATION_PAUSE.toMillis());
             }
         }
-        if (totalSignedKeys > 0) {
-            Log.i(TAG, "Generated " + generated + " keys. "
-                    + (generated + unattestedKeys) + " keys are now available for signing.");
-            return generated + unattestedKeys;
-        }
-        Log.i(TAG, "No keys generated.");
-        return 0;
+        Log.i(TAG, "Generated " + generated + " keys. " + stats.unattestedKeys
+                    + " keys were also available for signing previous to generation.");
+        return stats.idealTotalSignedKeys;
     }
+
+
 }
diff --git a/src/com/android/remoteprovisioner/StatsProcessor.java b/src/com/android/remoteprovisioner/StatsProcessor.java
new file mode 100644
index 0000000..85582ab
--- /dev/null
+++ b/src/com/android/remoteprovisioner/StatsProcessor.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (C) 2022 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.security.remoteprovisioning.AttestationPoolStatus;
+import android.util.Log;
+
+public class StatsProcessor {
+    public static final double LIMIT_SCALER = .4;
+
+    private static final String TAG = "RemoteProvisioningService.KeyPoolStats";
+
+    private StatsProcessor() {}
+
+    public static int calcMinUnassignedToTriggerProvisioning(int extraSignedKeysAvailable) {
+        return (int) Math.ceil(LIMIT_SCALER * extraSignedKeysAvailable);
+    }
+
+    /**
+     * Creates a PoolStats. Takes an {@Code AttestationPoolStatus} and calculates different
+     * pieces of status to inform the caller if any action needs to be taken to reprovision the pool
+     * and what action is needed in terms of keys to generate.
+     *
+     * @parameter pool the current status of the keypool in Keystore2
+     * @parameter extraSignedKeysAvailable how many extra attested keys should ideally be available
+     *                                     for assignment.
+     * @return the PoolStats object describing higher level info about the state of the key pool.
+     */
+    public static PoolStats processPool(
+            AttestationPoolStatus pool, int extraSignedKeysAvailable) {
+        PoolStats stats = new PoolStats();
+        stats.unattestedKeys = pool.total - pool.attested;
+        stats.keysInUse = pool.attested - pool.unassigned;
+        stats.idealTotalSignedKeys = stats.keysInUse + extraSignedKeysAvailable;
+        // If nothing is expiring, and the amount of available unassigned keys is sufficient,
+        // then do nothing. Otherwise, generate the complete amount of idealTotalSignedKeys. 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.
+        stats.provisioningNeeded =
+                pool.unassigned - pool.expiring
+                <= calcMinUnassignedToTriggerProvisioning(extraSignedKeysAvailable);
+        if (!stats.provisioningNeeded) {
+            Log.i(TAG, "Sufficient keys are available, no CSR needed.");
+            stats.keysToGenerate = 0;
+        } else {
+            stats.keysToGenerate = Math.max(0, stats.idealTotalSignedKeys - stats.unattestedKeys);
+        }
+        return stats;
+    }
+
+    public static class PoolStats {
+        public int unattestedKeys;
+        public int keysInUse;
+        public int idealTotalSignedKeys;
+        public int keysToGenerate;
+        public boolean provisioningNeeded = true;
+    }
+}
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index ab2ec8d..268a8b5 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -29,18 +29,24 @@
 import android.util.Log;
 
 import com.android.remoteprovisioner.GeekResponse;
-import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.PeriodicProvisioner;
 import com.android.remoteprovisioner.ServerInterface;
-import com.android.remoteprovisioner.SettingsManager;
+
+import java.time.Duration;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Provides the implementation for IGenerateKeyService.aidl
  */
 public class GenerateRkpKeyService extends Service {
     private static final int KEY_GENERATION_PAUSE_MS = 1000;
+    private static final Duration LOOKAHEAD_TIME = Duration.ofDays(1);
+
     private static final String SERVICE = "android.security.remoteprovisioning";
     private static final String TAG = "RemoteProvisioningService";
 
+    private static final ReentrantLock sLock = new ReentrantLock();
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -78,41 +84,46 @@
 
         private void checkAndFillPool(IRemoteProvisioning binder, int secLevel)
                 throws RemoteException {
-            AttestationPoolStatus pool =
-                    binder.getPoolStatus(System.currentTimeMillis(), secLevel);
-            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;
-                }
+            // No need to hammer the pool check with a ton of redundant requests.
+            if (!sLock.tryLock()) {
+                Log.i(TAG, "Exiting check; another process already started the check.");
+                return;
             }
-            // 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);
+            try {
+                AttestationPoolStatus pool =
+                        binder.getPoolStatus(System.currentTimeMillis(), secLevel);
+                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;
                     }
-                } catch (InterruptedException e) {
-                    Log.i(TAG, "Thread interrupted", e);
                 }
-                GeekResponse resp = ServerInterface.fetchGeek(context);
-                if (resp == null) {
-                    Log.e(TAG, "Server unavailable");
-                    return;
+
+                Context context = getApplicationContext();
+                int keysToProvision =
+                        PeriodicProvisioner.generateNumKeysNeeded(binder, context,
+                                                                  LOOKAHEAD_TIME.toMillis(),
+                                                                  secLevel);
+                // 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 (keysToProvision != 0 && pool.attested != 0) {
+                    Log.i(TAG, "All signed keys are currently in use, provisioning more.");
+                    GeekResponse resp = ServerInterface.fetchGeek(context);
+                    if (resp == null) {
+                        Log.e(TAG, "Server unavailable");
+                        return;
+                    }
+                    PeriodicProvisioner.batchProvision(binder, context, keysToProvision, secLevel,
+                                                     resp.getGeekChain(curve), resp.getChallenge());
                 }
-                Provisioner.provisionCerts(keysToProvision, secLevel, resp.getGeekChain(curve),
-                                           resp.getChallenge(), binder, context);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Provisioner thread interrupted.", e);
+            } finally {
+                sLock.unlock();
             }
         }
     };
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
index 94206af..f4fdf06 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -140,14 +140,16 @@
     @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 */,
+        SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */,
                                         "Not even a URL" /* url */);
         int numTestKeys = 1;
         assertPoolStatus(0, 0, 0, 0, mDuration);
+        // Note that due to the GenerateRkpKeyService, this call to generate an attested key will
+        // still cause the service to generate keys up the number specified as `extraKeys` in the
+        // `setDeviceConfig`. This will provide us 1 key for the followup call to provisionCerts.
         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,
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/StatsProcessorTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/StatsProcessorTest.java
new file mode 100644
index 0000000..3f30ebb
--- /dev/null
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/StatsProcessorTest.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2022 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.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.Presubmit;
+import android.security.remoteprovisioning.AttestationPoolStatus;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.remoteprovisioner.StatsProcessor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StatsProcessorTest {
+
+    private AttestationPoolStatus mPool = new AttestationPoolStatus();
+    private int mNumExtraKeys;
+
+    @Before
+    public void setUp() {
+        mNumExtraKeys = 0;
+        mPool.expiring = 0;
+        mPool.unassigned = 0;
+        mPool.attested = 0;
+        mPool.total = 0;
+    }
+
+    @Presubmit
+    @Test
+    public void testMinUnassignedBoundary() throws Exception {
+        mNumExtraKeys = 5;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        // Add an unassigned key to avoid the threshold for triggering reprovisioning.
+        mPool.unassigned += 1;
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertFalse(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(7, stats.keysInUse);
+        assertEquals(12, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+        // Now test provisioning needed boundary
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.expiring = 0;
+        stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(8, stats.keysInUse);
+        assertEquals(13, stats.idealTotalSignedKeys);
+        assertEquals(13, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsNoProvisioning() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        // Add an unassigned key to avoid the threshold for triggering reprovisioning.
+        mPool.unassigned += 1;
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertFalse(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(7, stats.keysInUse);
+        assertEquals(11, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsProvisioning1() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(8, stats.keysInUse);
+        assertEquals(12, stats.idealTotalSignedKeys);
+        assertEquals(12, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsProvisioning2() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.unassigned -= 1;
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(9, stats.keysInUse);
+        assertEquals(13, stats.idealTotalSignedKeys);
+        assertEquals(13, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsProvisioningSomePreGenerated() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 15;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(5, stats.unattestedKeys);
+        assertEquals(8, stats.keysInUse);
+        assertEquals(12, stats.idealTotalSignedKeys);
+        assertEquals(7, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsProvisioningAllPreGenerated() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 22;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(12, stats.unattestedKeys);
+        assertEquals(8, stats.keysInUse);
+        assertEquals(12, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testStatsProvisioningTonsPreGenerated() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 33;
+        mPool.attested = 10;
+        mPool.unassigned = StatsProcessor.calcMinUnassignedToTriggerProvisioning(mNumExtraKeys);
+        assertEquals(2, mPool.unassigned);
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(23, stats.unattestedKeys);
+        assertEquals(8, stats.keysInUse);
+        assertEquals(12, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeeded1() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 6;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(9, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeeded2() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 10;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(9, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeeded3() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 5;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(9, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeeded4() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 10;
+        mPool.expiring = 10;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(0, stats.keysInUse);
+        assertEquals(4, stats.idealTotalSignedKeys);
+        assertEquals(4, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeeded5() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 3;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(9, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNotNeeded1() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 0;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertFalse(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNotNeeded2() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 2;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertFalse(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(0, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testExpiringProvisioningNeededSomeKeysPregenerated() throws Exception {
+        mNumExtraKeys = 4;
+        mPool.total = 12;
+        mPool.attested = 10;
+        mPool.unassigned = 5;
+        mPool.expiring = 6;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(2, stats.unattestedKeys);
+        assertEquals(5, stats.keysInUse);
+        assertEquals(9, stats.idealTotalSignedKeys);
+        assertEquals(7, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testBothExpiringAndBelowMinimumExtraKeysAvailable() throws Exception {
+        mNumExtraKeys = 5;
+        mPool.total = 10;
+        mPool.attested = 10;
+        mPool.unassigned = 1;
+        mPool.expiring = 6;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(0, stats.unattestedKeys);
+        assertEquals(9, stats.keysInUse);
+        assertEquals(14, stats.idealTotalSignedKeys);
+        assertEquals(14, stats.keysToGenerate);
+    }
+
+    @Presubmit
+    @Test
+    public void testBothExpiringAndBelowMinimumExtraKeysAvailableWithPreGenKeys() throws Exception {
+        mNumExtraKeys = 5;
+        mPool.total = 14;
+        mPool.attested = 10;
+        mPool.unassigned = 1;
+        mPool.expiring = 6;
+        StatsProcessor.PoolStats stats = StatsProcessor.processPool(mPool, mNumExtraKeys);
+        assertTrue(stats.provisioningNeeded);
+        assertEquals(4, stats.unattestedKeys);
+        assertEquals(9, stats.keysInUse);
+        assertEquals(14, stats.idealTotalSignedKeys);
+        assertEquals(10, stats.keysToGenerate);
+    }
+}