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