1 /** 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.remoteprovisioner; 18 19 import static java.lang.Math.min; 20 21 import android.app.job.JobParameters; 22 import android.app.job.JobService; 23 import android.content.Context; 24 import android.net.ConnectivityManager; 25 import android.os.RemoteException; 26 import android.os.ServiceManager; 27 import android.security.remoteprovisioning.AttestationPoolStatus; 28 import android.security.remoteprovisioning.ImplInfo; 29 import android.security.remoteprovisioning.IRemoteProvisioning; 30 import android.util.Log; 31 32 import java.time.Duration; 33 34 /** 35 * A class that extends JobService in order to be scheduled to check the status of the attestation 36 * key pool at regular intervals. If the job determines that more keys need to be generated and 37 * signed, it drives that process. 38 */ 39 public class PeriodicProvisioner extends JobService { 40 41 private static final int FAILURE_MAXIMUM = 5; 42 private static final int SAFE_CSR_BATCH_SIZE = 20; 43 44 // How long to wait in between key pair generations to avoid flooding keystore with requests. 45 private static final Duration KEY_GENERATION_PAUSE = Duration.ofMillis(1000); 46 47 // If the connection is metered when the job service is started, try to avoid provisioning. 48 private static final long METERED_CONNECTION_EXPIRATION_CHECK = Duration.ofDays(1).toMillis(); 49 50 private static final String SERVICE = "android.security.remoteprovisioning"; 51 private static final String TAG = "RemoteProvisioningService"; 52 private ProvisionerThread mProvisionerThread; 53 54 /** 55 * Starts the periodic provisioning job, which will check the attestation key pool 56 * and provision it as necessary. 57 */ onStartJob(JobParameters params)58 public boolean onStartJob(JobParameters params) { 59 Log.i(TAG, "Starting provisioning job"); 60 mProvisionerThread = new ProvisionerThread(params, this); 61 mProvisionerThread.start(); 62 return true; 63 } 64 65 /** 66 * Allows the job to be stopped if need be. 67 */ onStopJob(JobParameters params)68 public boolean onStopJob(JobParameters params) { 69 return false; 70 } 71 72 private class ProvisionerThread extends Thread { 73 private Context mContext; 74 private JobParameters mParams; 75 ProvisionerThread(JobParameters params, Context context)76 ProvisionerThread(JobParameters params, Context context) { 77 mParams = params; 78 mContext = context; 79 } 80 run()81 public void run() { 82 try { 83 IRemoteProvisioning binder = 84 IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE)); 85 if (binder == null) { 86 Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service."); 87 jobFinished(mParams, false /* wantsReschedule */); 88 return; 89 } 90 91 ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService( 92 Context.CONNECTIVITY_SERVICE); 93 boolean isMetered = cm.isActiveNetworkMetered(); 94 Log.i(TAG, "Connection is metered: " + isMetered); 95 long expiringBy; 96 if (isMetered) { 97 // Check a shortened duration to attempt to avoid metered connection 98 // provisioning. 99 expiringBy = System.currentTimeMillis() + METERED_CONNECTION_EXPIRATION_CHECK; 100 } else { 101 expiringBy = SettingsManager.getExpiringBy(mContext) 102 .plusMillis(System.currentTimeMillis()) 103 .toMillis(); 104 } 105 ImplInfo[] implInfos = binder.getImplementationInfo(); 106 if (implInfos == null) { 107 Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in " 108 + SERVICE); 109 jobFinished(mParams, false /* wantsReschedule */); 110 return; 111 } 112 int[] keysNeededForSecLevel = new int[implInfos.length]; 113 boolean provisioningNeeded = 114 isProvisioningNeeded(binder, expiringBy, implInfos, keysNeededForSecLevel); 115 GeekResponse resp = null; 116 if (!provisioningNeeded) { 117 if (!isMetered) { 118 // So long as the connection is unmetered, go ahead and grab an updated 119 // device configuration file. 120 resp = ServerInterface.fetchGeek(mContext); 121 if (!checkGeekResp(resp)) { 122 jobFinished(mParams, false /* wantsReschedule */); 123 return; 124 } 125 SettingsManager.setDeviceConfig(mContext, 126 resp.numExtraAttestationKeys, 127 resp.timeToRefresh, 128 resp.provisioningUrl); 129 if (resp.numExtraAttestationKeys == 0) { 130 binder.deleteAllKeys(); 131 } 132 } 133 jobFinished(mParams, false /* wantsReschedule */); 134 return; 135 } 136 resp = ServerInterface.fetchGeek(mContext); 137 if (!checkGeekResp(resp)) { 138 jobFinished(mParams, false /* wantsReschedule */); 139 return; 140 } 141 SettingsManager.setDeviceConfig(mContext, 142 resp.numExtraAttestationKeys, 143 resp.timeToRefresh, 144 resp.provisioningUrl); 145 146 if (resp.numExtraAttestationKeys == 0) { 147 // Provisioning is disabled. Check with the server if it's time to turn it back 148 // on. If not, quit. Avoid checking if the connection is metered. Opt instead 149 // to just continue using the fallback factory provisioned key. 150 binder.deleteAllKeys(); 151 jobFinished(mParams, false /* wantsReschedule */); 152 return; 153 } 154 for (int i = 0; i < implInfos.length; i++) { 155 // Break very large CSR requests into chunks, so as not to overwhelm the 156 // backend. 157 int keysToCertify = keysNeededForSecLevel[i]; 158 while (keysToCertify != 0) { 159 int batchSize = min(keysToCertify, SAFE_CSR_BATCH_SIZE); 160 Log.i(TAG, "Requesting " + batchSize + " keys to be provisioned."); 161 Provisioner.provisionCerts(batchSize, 162 implInfos[i].secLevel, 163 resp.getGeekChain(implInfos[i].supportedCurve), 164 resp.getChallenge(), 165 binder, 166 mContext); 167 keysToCertify -= batchSize; 168 } 169 } 170 jobFinished(mParams, false /* wantsReschedule */); 171 } catch (RemoteException e) { 172 jobFinished(mParams, false /* wantsReschedule */); 173 Log.e(TAG, "Error on the binder side during provisioning.", e); 174 } catch (InterruptedException e) { 175 jobFinished(mParams, false /* wantsReschedule */); 176 Log.e(TAG, "Provisioner thread interrupted.", e); 177 } 178 } 179 checkGeekResp(GeekResponse resp)180 private boolean checkGeekResp(GeekResponse resp) { 181 if (resp == null) { 182 Log.e(TAG, "Failed to get a response from the server."); 183 if (SettingsManager.getFailureCounter(mContext) > FAILURE_MAXIMUM) { 184 Log.e(TAG, "Too many failures, resetting defaults."); 185 SettingsManager.clearPreferences(mContext); 186 } 187 jobFinished(mParams, false /* wantsReschedule */); 188 return false; 189 } 190 return true; 191 } 192 isProvisioningNeeded( IRemoteProvisioning binder, long expiringBy, ImplInfo[] implInfos, int[] keysNeededForSecLevel)193 private boolean isProvisioningNeeded( 194 IRemoteProvisioning binder, long expiringBy, ImplInfo[] implInfos, 195 int[] keysNeededForSecLevel) 196 throws InterruptedException, RemoteException { 197 if (implInfos == null || keysNeededForSecLevel == null 198 || keysNeededForSecLevel.length != implInfos.length) { 199 Log.e(TAG, "Invalid argument."); 200 return false; 201 } 202 boolean provisioningNeeded = false; 203 for (int i = 0; i < implInfos.length; i++) { 204 keysNeededForSecLevel[i] = 205 generateNumKeysNeeded(binder, 206 expiringBy, 207 implInfos[i].secLevel); 208 if (keysNeededForSecLevel[i] > 0) { 209 provisioningNeeded = true; 210 } 211 } 212 return provisioningNeeded; 213 } 214 215 /** 216 * This method will generate and bundle up keys for signing to make sure that there will be 217 * enough keys available for use by the system when current keys expire. 218 * 219 * Enough keys is defined by checking how many keys are currently assigned to apps and 220 * generating enough keys to cover any expiring certificates plus a bit of buffer room 221 * defined by {@code sExtraSignedKeysAvailable}. 222 * 223 * This allows devices to dynamically resize their key pools as the user downloads and 224 * removes apps that may also use attestation. 225 */ generateNumKeysNeeded(IRemoteProvisioning binder, long expiringBy, int secLevel)226 private int generateNumKeysNeeded(IRemoteProvisioning binder, long expiringBy, int secLevel) 227 throws InterruptedException, RemoteException { 228 AttestationPoolStatus pool = 229 SystemInterface.getPoolStatus(expiringBy, secLevel, binder); 230 if (pool == null) { 231 Log.e(TAG, "Failed to fetch pool status."); 232 return 0; 233 } 234 Log.i(TAG, "Pool status.\nTotal: " + pool.total 235 + "\nAttested: " + pool.attested 236 + "\nUnassigned: " + pool.unassigned 237 + "\nExpiring: " + pool.expiring); 238 int unattestedKeys = pool.total - pool.attested; 239 int keysInUse = pool.attested - pool.unassigned; 240 int totalSignedKeys = keysInUse + SettingsManager.getExtraSignedKeysAvailable(mContext); 241 int generated; 242 // If nothing is expiring, and the amount of available unassigned keys is sufficient, 243 // then do nothing. Otherwise, generate the complete amount of totalSignedKeys. It will 244 // reduce network usage if the app just provisions an entire new batch in one go, rather 245 // than consistently grabbing just a few at a time as the expiration dates become 246 // misaligned. 247 if (pool.expiring < pool.unassigned && pool.attested >= totalSignedKeys) { 248 Log.i(TAG, 249 "No keys expiring and the expected number of attested keys are available"); 250 return 0; 251 } 252 for (generated = 0; 253 generated + unattestedKeys < totalSignedKeys; generated++) { 254 SystemInterface.generateKeyPair(false /* isTestMode */, secLevel, binder); 255 // Prioritize provisioning if there are no keys available. No keys being available 256 // indicates that this is the first time a device is being brought online. 257 if (pool.total != 0) { 258 Thread.sleep(KEY_GENERATION_PAUSE.toMillis()); 259 } 260 } 261 if (totalSignedKeys > 0) { 262 Log.i(TAG, "Generated " + generated + " keys. " 263 + (generated + unattestedKeys) + " keys are now available for signing."); 264 return generated + unattestedKeys; 265 } 266 Log.i(TAG, "No keys generated."); 267 return 0; 268 } 269 } 270 } 271