• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.rkpdapp.service;
18 
19 import android.content.Context;
20 import android.os.IBinder;
21 import android.os.RemoteException;
22 import android.os.Trace;
23 import android.util.Log;
24 
25 import androidx.annotation.GuardedBy;
26 
27 import com.android.rkpdapp.GeekResponse;
28 import com.android.rkpdapp.IGetKeyCallback;
29 import com.android.rkpdapp.IRegistration;
30 import com.android.rkpdapp.IStoreUpgradedKeyCallback;
31 import com.android.rkpdapp.RemotelyProvisionedKey;
32 import com.android.rkpdapp.RkpdException;
33 import com.android.rkpdapp.database.ProvisionedKey;
34 import com.android.rkpdapp.database.ProvisionedKeyDao;
35 import com.android.rkpdapp.interfaces.ServerInterface;
36 import com.android.rkpdapp.interfaces.SystemInterface;
37 import com.android.rkpdapp.metrics.ProvisioningAttempt;
38 import com.android.rkpdapp.metrics.RkpdClientOperation;
39 import com.android.rkpdapp.provisioner.Provisioner;
40 import com.android.rkpdapp.utils.Settings;
41 
42 import java.time.Duration;
43 import java.time.Instant;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.HashMap;
47 import java.util.concurrent.ExecutorService;
48 import java.util.concurrent.Future;
49 
50 import co.nstant.in.cbor.CborException;
51 
52 /**
53  * Implementation of com.android.rkpdapp.IRegistration, which fetches keys for a (caller UID,
54  * IRemotelyProvisionedComponent) tuple.
55  */
56 public final class RegistrationBinder extends IRegistration.Stub {
57     // The minimum amount of time that the registration will consider a key valid. If a key expires
58     // before this time elapses, then the key is considered too stale and will not be used.
59     public static final Duration MIN_KEY_LIFETIME = Duration.ofHours(1);
60 
61     static final String TAG = "RkpdRegistrationBinder";
62 
63     private final Context mContext;
64     private final int mClientUid;
65     private final SystemInterface mSystemInterface;
66     private final ProvisionedKeyDao mProvisionedKeyDao;
67     private final ServerInterface mRkpServer;
68     private final Provisioner mProvisioner;
69     private final ExecutorService mThreadPool;
70     private final Object mTasksLock = new Object();
71     @GuardedBy("mTasksLock")
72     private final HashMap<IBinder, Future<?>> mTasks = new HashMap<>();
73 
RegistrationBinder(Context context, int clientUid, SystemInterface systemInterface, ProvisionedKeyDao provisionedKeyDao, ServerInterface rkpServer, Provisioner provisioner, ExecutorService threadPool)74     public RegistrationBinder(Context context, int clientUid, SystemInterface systemInterface,
75             ProvisionedKeyDao provisionedKeyDao, ServerInterface rkpServer,
76             Provisioner provisioner, ExecutorService threadPool) {
77         mContext = context;
78         mClientUid = clientUid;
79         mSystemInterface = systemInterface;
80         mProvisionedKeyDao = provisionedKeyDao;
81         mRkpServer = rkpServer;
82         mProvisioner = provisioner;
83         mThreadPool = threadPool;
84     }
85 
getKeyWorker(int keyId, IGetKeyCallback callback)86     private void getKeyWorker(int keyId, IGetKeyCallback callback)
87             throws CborException, InterruptedException, RkpdException {
88         Log.i(TAG, "Key requested for : " + mSystemInterface.getServiceName() + ", clientUid: "
89                 + mClientUid + ", keyId: " + keyId + ", callback: "
90                 + callback.asBinder().hashCode());
91         // Use reduced look-ahead to get rid of soon-to-be expired keys, because the periodic
92         // provisioner should be ensuring that old keys are already expired. However, in the
93         // edge case that periodic provisioning didn't work, we want to allow slightly "more stale"
94         // keys to be used. This reduces window of time in which key attestation is not available
95         // (e.g. if there is a provisioning server outage). Note that we must have some look-ahead,
96         // rather than using "now", else we might return a key that expires so soon that the caller
97         // can never successfully use it.
98         final Instant minExpiry = Instant.now().plus(MIN_KEY_LIFETIME);
99         mProvisionedKeyDao.deleteExpiringKeys(minExpiry);
100 
101         ProvisionedKey assignedKey = mProvisionedKeyDao.getKeyForClientAndIrpc(
102                 mSystemInterface.getServiceName(), mClientUid, keyId);
103 
104         if (assignedKey == null) {
105             assignedKey = tryToAssignKey(minExpiry, keyId);
106         }
107 
108         if (assignedKey == null) {
109             // Since provisionKeys goes over the network, this represents our last chancel to cancel
110             // before we go off and hit the network. It's not worth checking for interruption prior
111             // to this, because none of the prior work is long-running.
112             checkForCancel();
113 
114             Log.i(TAG, "No keys are available, kicking off provisioning");
115             checkedCallback(callback::onProvisioningNeeded);
116             try (ProvisioningAttempt metrics = ProvisioningAttempt.createOutOfKeysAttemptMetrics(
117                     mContext, mSystemInterface.getServiceName())) {
118                 Trace.beginSection("Registration.Binder.fetchGeekAndProvisionKeys");
119                 try {
120                     fetchGeekAndProvisionKeys(metrics);
121                 } finally {
122                     Trace.endSection();
123                 }
124             }
125             assignedKey = tryToAssignKey(minExpiry, keyId);
126         }
127 
128         // Now that we've gotten back from our network round-trip, it's possible an interrupt came
129         // in, so deal with it. However, it's most likely that an InterruptedException came from
130         // the SDK while we were sitting on the socket down in Provisioner.provisionKeys.
131         checkForCancel();
132 
133         if (assignedKey == null) {
134             // This can happen if provisioning is disabled on the device for some reason,
135             // or if we're not connected to the internet.
136             Log.e(TAG, "Unable to provision keys");
137             checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
138                     "Provisioning failed, no keys available"));
139         } else {
140             Log.i(TAG, "Key successfully assigned to client");
141             RemotelyProvisionedKey key = new RemotelyProvisionedKey();
142             key.keyBlob = assignedKey.keyBlob;
143             key.encodedCertChain = assignedKey.certificateChain;
144             checkedCallback(() -> callback.onSuccess(key));
145         }
146     }
147 
fetchGeekAndProvisionKeys(ProvisioningAttempt metrics)148     private void fetchGeekAndProvisionKeys(ProvisioningAttempt metrics)
149             throws CborException, RkpdException, InterruptedException {
150         GeekResponse response = mRkpServer.fetchGeekAndUpdate(metrics);
151         if (response.numExtraAttestationKeys == 0) {
152             Log.v(TAG, "Provisioning disabled.");
153             metrics.setEnablement(ProvisioningAttempt.Enablement.DISABLED);
154             metrics.setStatus(ProvisioningAttempt.Status.PROVISIONING_DISABLED);
155             return;
156         }
157         mProvisioner.provisionKeys(metrics, mSystemInterface, response);
158     }
159 
tryToAssignKey(Instant minExpiry, int keyId)160     private ProvisionedKey tryToAssignKey(Instant minExpiry, int keyId) {
161         // Since we're going to be assigning a fresh key to the app, we ideally want a key that's
162         // longer-lived than the minimum. We use the server-configured expiration, which is normally
163         // days, as the preferred lifetime for a key. However, if we cannot find a key that is valid
164         // for that long, we'll settle for a shorter-lived key.
165         Instant[] expirations = new Instant[]{
166                 Instant.now().plus(Settings.getExpiringBy(mContext)),
167                 minExpiry
168         };
169         Arrays.sort(expirations, Collections.reverseOrder());
170         for (Instant expiry : expirations) {
171             Log.i(TAG, "No key assigned, looking for an available key with expiry of " + expiry);
172             ProvisionedKey key = mProvisionedKeyDao.getOrAssignKey(
173                     mSystemInterface.getServiceName(),
174                     expiry, mClientUid, keyId);
175             if (key != null) {
176                 provisionKeysOnKeyConsumed();
177                 return key;
178             }
179         }
180         return null;
181     }
182 
provisionKeysOnKeyConsumed()183     private void provisionKeysOnKeyConsumed() {
184         try (ProvisioningAttempt metrics = ProvisioningAttempt.createKeyConsumedAttemptMetrics(
185                 mContext, mSystemInterface.getServiceName())) {
186             if (!mProvisioner.isProvisioningNeeded(metrics, mSystemInterface.getServiceName())) {
187                 metrics.setStatus(ProvisioningAttempt.Status.NO_PROVISIONING_NEEDED);
188                 return;
189             }
190 
191             mThreadPool.execute(() -> {
192                 try {
193                     fetchGeekAndProvisionKeys(metrics);
194                 } catch (CborException | RkpdException | InterruptedException e) {
195                     Log.e(TAG, "Error provisioning keys", e);
196                 }
197             });
198         }
199     }
200 
checkForCancel()201     private void checkForCancel() throws InterruptedException {
202         if (Thread.currentThread().isInterrupted()) {
203             throw new InterruptedException();
204         }
205     }
206 
207     private interface CallbackWrapper {
run()208         void run() throws RemoteException;
209     }
210 
checkedCallback(CallbackWrapper wrapper)211     private void checkedCallback(CallbackWrapper wrapper) {
212         try {
213             wrapper.run();
214         } catch (RemoteException e) {
215             // This should only ever happen if there's a binder issue invoking the callback
216             Log.e(TAG, "Error performing client callback", e);
217         }
218     }
219 
220     @Override
getKey(int keyId, IGetKeyCallback callback)221     public void getKey(int keyId, IGetKeyCallback callback) {
222         Trace.beginSection("Registration.Binder.getKey");
223         synchronized (mTasksLock) {
224             if (mTasks.containsKey(callback.asBinder())) {
225                 Trace.endSection();
226                 throw new IllegalArgumentException("Callback " + callback.asBinder().hashCode()
227                         + " is already associated with a getKey operation that is in-progress");
228             }
229 
230             mTasks.put(callback.asBinder(),
231                     mThreadPool.submit(() -> getKeyThreadWorker(keyId, callback)));
232         }
233         Trace.endSection();
234     }
235 
getKeyThreadWorker(int keyId, IGetKeyCallback callback)236     private void getKeyThreadWorker(int keyId, IGetKeyCallback callback) {
237         // We don't use a try-with-resources here because the metric may need to be updated
238         // inside an exception handler, but close would have been called prior to that. Therefore,
239         // we explicitly close the metric explicitly in the "finally" block, after all handlers
240         // have had a chance to run.
241         RkpdClientOperation metric = RkpdClientOperation.getKey(mClientUid,
242                 mSystemInterface.getServiceName());
243         try {
244             getKeyWorker(keyId, callback);
245             metric.setResult(RkpdClientOperation.Result.SUCCESS);
246         } catch (InterruptedException e) {
247             Log.i(TAG, "getKey was interrupted");
248             metric.setResult(RkpdClientOperation.Result.CANCELED);
249             checkedCallback(callback::onCancel);
250         } catch (RkpdException e) {
251             Log.e(TAG, "RKPD failed to provision keys", e);
252             final byte mappedError = mapToGetKeyError(e, metric);
253             checkedCallback(
254                     () -> callback.onError(mappedError, e.getMessage()));
255         } catch (Exception e) {
256             // Do our best to inform the callback when the unexpected happens. Otherwise,
257             // the caller is going to wait until they timeout without knowing something like
258             // a RuntimeException occurred.
259             Log.e(TAG, "Unexpected error provisioning keys", e);
260             checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
261                     e.getMessage()));
262         } finally {
263             metric.close();
264             synchronized (mTasksLock) {
265                 mTasks.remove(callback.asBinder());
266             }
267         }
268     }
269 
270     /** Maps an RkpdException into an IGetKeyCallback.Error value. */
mapToGetKeyError(RkpdException e, RkpdClientOperation metric)271     private byte mapToGetKeyError(RkpdException e, RkpdClientOperation metric) {
272         switch (e.getErrorCode()) {
273             case NO_NETWORK_CONNECTIVITY:
274                 metric.setResult(RkpdClientOperation.Result.ERROR_PENDING_INTERNET_CONNECTIVITY);
275                 return IGetKeyCallback.Error.ERROR_PENDING_INTERNET_CONNECTIVITY;
276 
277             case DEVICE_NOT_REGISTERED:
278                 metric.setResult(RkpdClientOperation.Result.ERROR_PERMANENT);
279                 return IGetKeyCallback.Error.ERROR_PERMANENT;
280 
281             case INTERNAL_ERROR:
282                 metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
283                 return IGetKeyCallback.Error.ERROR_UNKNOWN;
284 
285             case NETWORK_COMMUNICATION_ERROR:
286             case HTTP_CLIENT_ERROR:
287             case HTTP_SERVER_ERROR:
288             case HTTP_UNKNOWN_ERROR:
289             default:
290                 return IGetKeyCallback.Error.ERROR_UNKNOWN;
291         }
292     }
293 
294     @Override
cancelGetKey(IGetKeyCallback callback)295     public void cancelGetKey(IGetKeyCallback callback) throws RemoteException {
296         Log.i(TAG, "cancelGetKey(" + callback.asBinder().hashCode() + ")");
297         synchronized (mTasksLock) {
298             try (RkpdClientOperation metric = RkpdClientOperation.cancelGetKey(mClientUid,
299                     mSystemInterface.getServiceName())) {
300                 Future<?> task = mTasks.get(callback.asBinder());
301 
302                 if (task == null) {
303                     Log.w(TAG, "callback not found, task may have already completed");
304                 } else if (task.isDone()) {
305                     Log.w(TAG, "task already completed, not cancelling");
306                 } else if (task.isCancelled()) {
307                     Log.w(TAG, "task already cancelled, cannot cancel it any further");
308                 } else {
309                     task.cancel(true);
310                 }
311                 metric.setResult(RkpdClientOperation.Result.SUCCESS);
312             }
313         }
314     }
315 
316     @Override
storeUpgradedKeyAsync(byte[] oldKeyBlob, byte[] newKeyBlob, IStoreUpgradedKeyCallback callback)317     public void storeUpgradedKeyAsync(byte[] oldKeyBlob, byte[] newKeyBlob,
318             IStoreUpgradedKeyCallback callback) throws RemoteException {
319         Log.i(TAG, "storeUpgradedKeyAsync");
320         mThreadPool.execute(() -> {
321             try (RkpdClientOperation metric = RkpdClientOperation.storeUpgradedKey(
322                     mClientUid, mSystemInterface.getServiceName())) {
323                 int keysUpgraded = mProvisionedKeyDao.upgradeKeyBlob(mClientUid, oldKeyBlob,
324                         newKeyBlob);
325                 if (keysUpgraded == 1) {
326                     metric.setResult(RkpdClientOperation.Result.SUCCESS);
327                     checkedCallback(callback::onSuccess);
328                 } else if (keysUpgraded == 0) {
329                     metric.setResult(RkpdClientOperation.Result.ERROR_KEY_NOT_FOUND);
330                     checkedCallback(() -> callback.onError("No keys matching oldKeyBlob found"));
331                 } else {
332                     metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
333                     Log.e(TAG, "Multiple keys matched the upgrade (" + keysUpgraded
334                             + "). This should be impossible!");
335                     checkedCallback(() -> callback.onError("Internal error"));
336                 }
337             } catch (Exception e) {
338                 checkedCallback(() -> callback.onError(e.getMessage()));
339             }
340         });
341     }
342 }
343