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