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