blob: fd5e69c0be7de1de7b6bd89ccb2dffd8388a2090 [file] [log] [blame]
/**
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.remoteprovisioner;
import static java.lang.Math.min;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Context;
import android.net.ConnectivityManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.security.remoteprovisioning.AttestationPoolStatus;
import android.security.remoteprovisioning.ImplInfo;
import android.security.remoteprovisioning.IRemoteProvisioning;
import android.util.Log;
import java.time.Duration;
/**
* A class that extends JobService in order to be scheduled to check the status of the attestation
* key pool at regular intervals. If the job determines that more keys need to be generated and
* signed, it drives that process.
*/
public class PeriodicProvisioner extends JobService {
private static final int FAILURE_MAXIMUM = 5;
private static final int SAFE_CSR_BATCH_SIZE = 20;
// How long to wait in between key pair generations to avoid flooding keystore with requests.
private static final Duration KEY_GENERATION_PAUSE = Duration.ofMillis(1000);
// If the connection is metered when the job service is started, try to avoid provisioning.
private static final long METERED_CONNECTION_EXPIRATION_CHECK = Duration.ofDays(1).toMillis();
private static final String SERVICE = "android.security.remoteprovisioning";
private static final String TAG = "RemoteProvisioningService";
private ProvisionerThread mProvisionerThread;
/**
* Starts the periodic provisioning job, which will check the attestation key pool
* and provision it as necessary.
*/
public boolean onStartJob(JobParameters params) {
Log.i(TAG, "Starting provisioning job");
mProvisionerThread = new ProvisionerThread(params, this);
mProvisionerThread.start();
return true;
}
/**
* Allows the job to be stopped if need be.
*/
public boolean onStopJob(JobParameters params) {
return false;
}
private class ProvisionerThread extends Thread {
private Context mContext;
private JobParameters mParams;
ProvisionerThread(JobParameters params, Context context) {
mParams = params;
mContext = context;
}
public void run() {
try {
IRemoteProvisioning binder =
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
if (binder == null) {
Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
jobFinished(mParams, false /* wantsReschedule */);
return;
}
ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
Context.CONNECTIVITY_SERVICE);
boolean isMetered = cm.isActiveNetworkMetered();
long expiringBy;
if (isMetered) {
// Check a shortened duration to attempt to avoid metered connection
// provisioning.
expiringBy = System.currentTimeMillis() + METERED_CONNECTION_EXPIRATION_CHECK;
} else {
expiringBy = SettingsManager.getExpiringBy(mContext)
.plusMillis(System.currentTimeMillis())
.toMillis();
}
ImplInfo[] implInfos = binder.getImplementationInfo();
if (implInfos == null) {
Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
+ SERVICE);
jobFinished(mParams, false /* wantsReschedule */);
return;
}
int[] keysNeededForSecLevel = new int[implInfos.length];
boolean provisioningNeeded =
isProvisioningNeeded(binder, expiringBy, implInfos, keysNeededForSecLevel);
GeekResponse resp = null;
if (!provisioningNeeded) {
if (!isMetered) {
// So long as the connection is unmetered, go ahead and grab an updated
// device configuration file.
resp = ServerInterface.fetchGeek(mContext);
if (!checkGeekResp(resp)) {
jobFinished(mParams, false /* wantsReschedule */);
return;
}
SettingsManager.setDeviceConfig(mContext,
resp.numExtraAttestationKeys,
resp.timeToRefresh,
resp.provisioningUrl);
if (resp.numExtraAttestationKeys == 0) {
binder.deleteAllKeys();
}
}
jobFinished(mParams, false /* wantsReschedule */);
return;
}
resp = ServerInterface.fetchGeek(mContext);
if (!checkGeekResp(resp)) {
jobFinished(mParams, false /* wantsReschedule */);
return;
}
SettingsManager.setDeviceConfig(mContext,
resp.numExtraAttestationKeys,
resp.timeToRefresh,
resp.provisioningUrl);
if (resp.numExtraAttestationKeys == 0) {
// Provisioning is disabled. Check with the server if it's time to turn it back
// on. If not, quit. Avoid checking if the connection is metered. Opt instead
// to just continue using the fallback factory provisioned key.
binder.deleteAllKeys();
jobFinished(mParams, false /* wantsReschedule */);
return;
}
for (int i = 0; i < implInfos.length; i++) {
// Break very large CSR requests into chunks, so as not to overwhelm the
// backend.
int keysToCertify = keysNeededForSecLevel[i];
while (keysToCertify != 0) {
int batchSize = min(keysToCertify, SAFE_CSR_BATCH_SIZE);
Provisioner.provisionCerts(batchSize,
implInfos[i].secLevel,
resp.getGeekChain(implInfos[i].supportedCurve),
resp.getChallenge(),
binder,
mContext);
keysToCertify -= batchSize;
}
}
jobFinished(mParams, false /* wantsReschedule */);
} catch (RemoteException e) {
jobFinished(mParams, false /* wantsReschedule */);
Log.e(TAG, "Error on the binder side during provisioning.", e);
} catch (InterruptedException e) {
jobFinished(mParams, false /* wantsReschedule */);
Log.e(TAG, "Provisioner thread interrupted.", e);
}
}
private boolean checkGeekResp(GeekResponse resp) {
if (resp == null) {
Log.e(TAG, "Failed to get a response from the server.");
if (SettingsManager.getFailureCounter(mContext) > FAILURE_MAXIMUM) {
Log.e(TAG, "Too many failures, resetting defaults.");
SettingsManager.clearPreferences(mContext);
}
jobFinished(mParams, false /* wantsReschedule */);
return false;
}
return true;
}
private boolean isProvisioningNeeded(
IRemoteProvisioning binder, long expiringBy, ImplInfo[] implInfos,
int[] keysNeededForSecLevel)
throws InterruptedException, RemoteException {
if (implInfos == null || keysNeededForSecLevel == null
|| keysNeededForSecLevel.length != implInfos.length) {
Log.e(TAG, "Invalid argument.");
return false;
}
boolean provisioningNeeded = false;
for (int i = 0; i < implInfos.length; i++) {
keysNeededForSecLevel[i] =
generateNumKeysNeeded(binder,
expiringBy,
implInfos[i].secLevel);
if (keysNeededForSecLevel[i] > 0) {
provisioningNeeded = true;
}
}
return provisioningNeeded;
}
/**
* This method will generate and bundle up keys for signing to make sure that there will be
* enough keys available for use by the system when current keys expire.
*
* Enough keys is defined by checking how many keys are currently assigned to apps and
* generating enough keys to cover any expiring certificates plus a bit of buffer room
* defined by {@code sExtraSignedKeysAvailable}.
*
* This allows devices to dynamically resize their key pools as the user downloads and
* removes apps that may also use attestation.
*/
private int generateNumKeysNeeded(IRemoteProvisioning binder, long expiringBy, int secLevel)
throws InterruptedException, RemoteException {
AttestationPoolStatus pool = binder.getPoolStatus(expiringBy, secLevel);
int unattestedKeys = pool.total - pool.attested;
int keysInUse = pool.attested - pool.unassigned;
int totalSignedKeys = keysInUse + SettingsManager.getExtraSignedKeysAvailable(mContext);
int generated;
// If nothing is expiring, and the amount of available unassigned keys is sufficient,
// then do nothing. Otherwise, generate the complete amount of totalSignedKeys. It will
// reduce network usage if the app just provisions an entire new batch in one go, rather
// than consistently grabbing just a few at a time as the expiration dates become
// misaligned.
if (pool.expiring > pool.unassigned && pool.attested == totalSignedKeys) {
return 0;
}
for (generated = 0;
generated + unattestedKeys < totalSignedKeys; generated++) {
binder.generateKeyPair(false /* isTestMode */, secLevel);
// Prioritize provisioning if there are no keys available. No keys being available
// indicates that this is the first time a device is being brought online.
if (pool.total != 0) {
Thread.sleep(KEY_GENERATION_PAUSE.toMillis());
}
}
if (totalSignedKeys > 0) {
return generated + unattestedKeys;
}
return 0;
}
}
}