aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2021-08-10 07:30:43 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2021-08-10 07:30:43 +0000
commit969a267567794489f39ffaa12c527cf557f9a3b4 (patch)
tree2b5fe390795a99edf02e2729c99bb42312ffc58d
parent305c3dbff7a87b08cc8bed01d856c977ff655591 (diff)
parent27ab6419029734e396f30848daa704d8dfefec2d (diff)
downloadRemoteProvisioner-android12-mainline-permission-release.tar.gz
Change-Id: I3193df2f020f1f6491ffad7165eef815429ad42b
-rw-r--r--src/com/android/remoteprovisioner/BootReceiver.java62
-rw-r--r--src/com/android/remoteprovisioner/CborUtils.java189
-rw-r--r--src/com/android/remoteprovisioner/GeekResponse.java71
-rw-r--r--src/com/android/remoteprovisioner/PeriodicProvisioner.java195
-rw-r--r--src/com/android/remoteprovisioner/Provisioner.java81
-rw-r--r--src/com/android/remoteprovisioner/ServerInterface.java47
-rw-r--r--src/com/android/remoteprovisioner/SettingsManager.java188
-rw-r--r--src/com/android/remoteprovisioner/SystemInterface.java12
-rw-r--r--src/com/android/remoteprovisioner/X509Utils.java10
-rw-r--r--src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java48
-rw-r--r--tests/unittests/Android.bp1
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java219
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java180
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java106
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java14
15 files changed, 1256 insertions, 167 deletions
diff --git a/src/com/android/remoteprovisioner/BootReceiver.java b/src/com/android/remoteprovisioner/BootReceiver.java
index f39d155..bf58b9c 100644
--- a/src/com/android/remoteprovisioner/BootReceiver.java
+++ b/src/com/android/remoteprovisioner/BootReceiver.java
@@ -16,14 +16,23 @@
package com.android.remoteprovisioner;
+import static java.lang.Math.max;
+
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+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 receiver class that listens for boot to be completed and then starts a recurring job that will
* monitor the status of the attestation key pool on device, purging old certificates and requesting
@@ -31,14 +40,34 @@ import android.util.Log;
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = "RemoteProvisioningBootReceiver";
+ private static final String SERVICE = "android.security.remoteprovisioning";
+
+ private static final Duration SCHEDULER_PERIOD = Duration.ofDays(1);
+
+ private static final int ESTIMATED_DOWNLOAD_BYTES_STATIC = 2300;
+ private static final int ESTIMATED_X509_CERT_BYTES = 540;
+ private static final int ESTIMATED_UPLOAD_BYTES_STATIC = 600;
+ private static final int ESTIMATED_CSR_KEY_BYTES = 44;
+
@Override
public void onReceive(Context context, Intent intent) {
- Log.d(TAG, "Caught boot intent, waking up.");
+ Log.i(TAG, "Caught boot intent, waking up.");
+ SettingsManager.generateAndSetId(context);
+ // An average call transmits about 500 bytes total. These calculations are for the
+ // once a month wake-up where provisioning occurs, where the expected bytes sent is closer
+ // to 8-10KB.
+ int numKeysNeeded = max(SettingsManager.getExtraSignedKeysAvailable(context),
+ calcNumPotentialKeysToDownload());
+ int estimatedDlBytes =
+ ESTIMATED_DOWNLOAD_BYTES_STATIC + (ESTIMATED_X509_CERT_BYTES * numKeysNeeded);
+ int estimatedUploadBytes =
+ ESTIMATED_UPLOAD_BYTES_STATIC + (ESTIMATED_CSR_KEY_BYTES * numKeysNeeded);
+
JobInfo info = new JobInfo
.Builder(1, new ComponentName(context, PeriodicProvisioner.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
- .setEstimatedNetworkBytes(1000, 1000)
- .setPeriodic(1000 * 60 * 60 * 24)
+ .setEstimatedNetworkBytes(estimatedDlBytes, estimatedUploadBytes)
+ .setPeriodic(SCHEDULER_PERIOD.toMillis())
.build();
if (((JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(info)
!= JobScheduler.RESULT_SUCCESS) {
@@ -46,4 +75,31 @@ public class BootReceiver extends BroadcastReceiver {
}
}
+ private int calcNumPotentialKeysToDownload() {
+ try {
+ IRemoteProvisioning binder =
+ IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+ int totalKeysAssigned = 0;
+ if (binder == null) {
+ Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
+ return totalKeysAssigned;
+ }
+ ImplInfo[] implInfos = binder.getImplementationInfo();
+ if (implInfos == null) {
+ Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
+ + SERVICE);
+ return totalKeysAssigned;
+ }
+ for (int i = 0; i < implInfos.length; i++) {
+ AttestationPoolStatus pool = binder.getPoolStatus(0, implInfos[i].secLevel);
+ if (pool != null) {
+ totalKeysAssigned += pool.attested - pool.unassigned;
+ }
+ }
+ return totalKeysAssigned;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failure on the RemoteProvisioning backend.", e);
+ return 0;
+ }
+ }
}
diff --git a/src/com/android/remoteprovisioner/CborUtils.java b/src/com/android/remoteprovisioner/CborUtils.java
index d30f8f5..d3fc3d7 100644
--- a/src/com/android/remoteprovisioner/CborUtils.java
+++ b/src/com/android/remoteprovisioner/CborUtils.java
@@ -16,12 +16,14 @@
package com.android.remoteprovisioner;
+import android.content.Context;
import android.os.Build;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -35,8 +37,16 @@ import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.MajorType;
import co.nstant.in.cbor.model.Map;
import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
public class CborUtils {
+ public static final int EC_CURVE_P256 = 1;
+ public static final int EC_CURVE_25519 = 2;
+
+ public static final String EXTRA_KEYS = "num_extra_attestation_keys";
+ public static final String TIME_TO_REFRESH = "time_to_refresh_hours";
+ public static final String PROVISIONING_URL = "provisioning_url";
+
private static final int RESPONSE_CERT_ARRAY_INDEX = 0;
private static final int RESPONSE_ARRAY_SIZE = 1;
@@ -44,11 +54,18 @@ public class CborUtils {
private static final int UNIQUE_CERTIFICATES_INDEX = 1;
private static final int CERT_ARRAY_ENTRIES = 2;
- private static final int EEK_INDEX = 0;
+ private static final int EEK_AND_CURVE_INDEX = 0;
private static final int CHALLENGE_INDEX = 1;
- private static final int EEK_ARRAY_ENTRIES = 2;
+ private static final int CONFIG_INDEX = 2;
+
+ private static final int CURVE_AND_EEK_CHAIN_LENGTH = 2;
+ private static final int CURVE_INDEX = 0;
+ private static final int EEK_CERT_CHAIN_INDEX = 1;
+ private static final int EEK_ARRAY_ENTRIES_NO_CONFIG = 2;
+ private static final int EEK_ARRAY_ENTRIES_WITH_CONFIG = 3;
private static final String TAG = "RemoteProvisioningService";
+ private static final byte[] EMPTY_MAP = new byte[] {(byte) 0xA0};
/**
* Parses the signed certificate chains returned by the server. In order to reduce data use over
@@ -67,10 +84,10 @@ public class CborUtils {
ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
List<DataItem> dataItems = new CborDecoder(bais).decode();
if (dataItems.size() != RESPONSE_ARRAY_SIZE
- || dataItems.get(RESPONSE_CERT_ARRAY_INDEX).getMajorType() != MajorType.ARRAY) {
+ || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
+ MajorType.ARRAY, "CborResponse")) {
Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
- + dataItems.size() + "\nExpected major type: Array. Actual: "
- + dataItems.get(0).getMajorType().name());
+ + dataItems.size());
return null;
}
dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
@@ -79,12 +96,10 @@ public class CborUtils {
+ dataItems.size());
return null;
}
- if (dataItems.get(SHARED_CERTIFICATES_INDEX).getMajorType() != MajorType.BYTE_STRING
- || dataItems.get(UNIQUE_CERTIFICATES_INDEX).getMajorType() != MajorType.ARRAY) {
- Log.e(TAG, "Incorrect CBOR types. Expected 'Byte String' and 'Array'. Got: "
- + dataItems.get(SHARED_CERTIFICATES_INDEX).getMajorType().name()
- + " and "
- + dataItems.get(UNIQUE_CERTIFICATES_INDEX).getMajorType().name());
+ if (!checkType(dataItems.get(SHARED_CERTIFICATES_INDEX),
+ MajorType.BYTE_STRING, "SharedCertificates")
+ || !checkType(dataItems.get(UNIQUE_CERTIFICATES_INDEX),
+ MajorType.ARRAY, "UniqueCertificates")) {
return null;
}
byte[] sharedCertificates =
@@ -92,9 +107,7 @@ public class CborUtils {
Array uniqueCertificates = (Array) dataItems.get(UNIQUE_CERTIFICATES_INDEX);
List<byte[]> uniqueCertificateChains = new ArrayList<byte[]>();
for (DataItem entry : uniqueCertificates.getDataItems()) {
- if (entry.getMajorType() != MajorType.BYTE_STRING) {
- Log.e(TAG, "Incorrect CBOR type. Expected: 'Byte String'. Actual:"
- + entry.getMajorType().name());
+ if (!checkType(entry, MajorType.BYTE_STRING, "UniqueCertificate")) {
return null;
}
ByteArrayOutputStream concat = new ByteArrayOutputStream();
@@ -112,39 +125,109 @@ public class CborUtils {
return null;
}
+ private static boolean checkType(DataItem item, MajorType majorType, String field) {
+ if (item.getMajorType() != majorType) {
+ Log.e(TAG, "Incorrect CBOR type for field: " + field + ". Expected " + majorType.name()
+ + ". Actual: " + item.getMajorType().name());
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean parseDeviceConfig(GeekResponse resp, DataItem deviceConfig) {
+ if (!checkType(deviceConfig, MajorType.MAP, "DeviceConfig")) {
+ return false;
+ }
+ Map deviceConfiguration = (Map) deviceConfig;
+ DataItem extraKeys =
+ deviceConfiguration.get(new UnicodeString(EXTRA_KEYS));
+ DataItem timeToRefreshHours =
+ deviceConfiguration.get(new UnicodeString(TIME_TO_REFRESH));
+ DataItem newUrl =
+ deviceConfiguration.get(new UnicodeString(PROVISIONING_URL));
+ if (extraKeys != null) {
+ if (!checkType(extraKeys, MajorType.UNSIGNED_INTEGER, "ExtraKeys")) {
+ return false;
+ }
+ resp.numExtraAttestationKeys = ((UnsignedInteger) extraKeys).getValue().intValue();
+ }
+ if (timeToRefreshHours != null) {
+ if (!checkType(timeToRefreshHours, MajorType.UNSIGNED_INTEGER, "TimeToRefresh")) {
+ return false;
+ }
+ resp.timeToRefresh =
+ Duration.ofHours(((UnsignedInteger) timeToRefreshHours).getValue().intValue());
+ }
+ if (newUrl != null) {
+ if (!checkType(newUrl, MajorType.UNICODE_STRING, "ProvisioningURL")) {
+ return false;
+ }
+ resp.provisioningUrl = ((UnicodeString) newUrl).getString();
+ }
+ return true;
+ }
+
/**
* Parses the Google Endpoint Encryption Key response provided by the server which contains a
* Google signed EEK and a challenge for use by the underlying IRemotelyProvisionedComponent HAL
*/
public static GeekResponse parseGeekResponse(byte[] serverResp) {
try {
+ GeekResponse resp = new GeekResponse();
ByteArrayInputStream bais = new ByteArrayInputStream(serverResp);
List<DataItem> dataItems = new CborDecoder(bais).decode();
if (dataItems.size() != RESPONSE_ARRAY_SIZE
- || dataItems.get(RESPONSE_CERT_ARRAY_INDEX).getMajorType() != MajorType.ARRAY) {
+ || !checkType(dataItems.get(RESPONSE_CERT_ARRAY_INDEX),
+ MajorType.ARRAY, "CborResponse")) {
Log.e(TAG, "Improper formatting of CBOR response. Expected size 1. Actual: "
- + dataItems.size() + "\nExpected major type: Array. Actual: "
- + dataItems.get(0).getMajorType().name());
+ + dataItems.size());
return null;
}
- dataItems = ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
- if (dataItems.size() != EEK_ARRAY_ENTRIES) {
- Log.e(TAG, "Incorrect number of certificate array entries. Expected: 2. Actual: "
- + dataItems.size());
+ List<DataItem> respItems =
+ ((Array) dataItems.get(RESPONSE_CERT_ARRAY_INDEX)).getDataItems();
+ if (respItems.size() != EEK_ARRAY_ENTRIES_NO_CONFIG
+ && respItems.size() != EEK_ARRAY_ENTRIES_WITH_CONFIG) {
+ Log.e(TAG, "Incorrect number of certificate array entries. Expected: "
+ + EEK_ARRAY_ENTRIES_NO_CONFIG + " or " + EEK_ARRAY_ENTRIES_WITH_CONFIG
+ + ". Actual: " + respItems.size());
return null;
}
- if (dataItems.get(EEK_INDEX).getMajorType() != MajorType.ARRAY
- || dataItems.get(CHALLENGE_INDEX).getMajorType() != MajorType.BYTE_STRING) {
- Log.e(TAG, "Incorrect CBOR types. Expected 'Array' and 'Byte String'. Got: "
- + dataItems.get(EEK_INDEX).getMajorType().name()
- + " and "
- + dataItems.get(CHALLENGE_INDEX).getMajorType().name());
+ if (!checkType(respItems.get(EEK_AND_CURVE_INDEX), MajorType.ARRAY, "EekAndCurveArr")) {
return null;
}
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- new CborEncoder(baos).encode(dataItems.get(EEK_INDEX));
- return new GeekResponse(baos.toByteArray(),
- ((ByteString) dataItems.get(CHALLENGE_INDEX)).getBytes());
+ List<DataItem> curveAndEekChains =
+ ((Array) respItems.get(EEK_AND_CURVE_INDEX)).getDataItems();
+ for (int i = 0; i < curveAndEekChains.size(); i++) {
+ if (!checkType(curveAndEekChains.get(i), MajorType.ARRAY, "EekAndCurve")) {
+ return null;
+ }
+ List<DataItem> curveAndEekChain =
+ ((Array) curveAndEekChains.get(i)).getDataItems();
+ if (curveAndEekChain.size() != CURVE_AND_EEK_CHAIN_LENGTH) {
+ Log.e(TAG, "Wrong size. Expected: " + CURVE_AND_EEK_CHAIN_LENGTH + ". Actual: "
+ + curveAndEekChain.size());
+ return null;
+ }
+ if (!checkType(curveAndEekChain.get(CURVE_INDEX),
+ MajorType.UNSIGNED_INTEGER, "Curve")
+ || !checkType(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX),
+ MajorType.ARRAY, "EekCertChain")) {
+ return null;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new CborEncoder(baos).encode(curveAndEekChain.get(EEK_CERT_CHAIN_INDEX));
+ UnsignedInteger curve = (UnsignedInteger) curveAndEekChain.get(CURVE_INDEX);
+ resp.addGeek(curve.getValue().intValue(), baos.toByteArray());
+ }
+ if (!checkType(respItems.get(CHALLENGE_INDEX), MajorType.BYTE_STRING, "Challenge")) {
+ return null;
+ }
+ resp.setChallenge(((ByteString) respItems.get(CHALLENGE_INDEX)).getBytes());
+ if (respItems.size() == EEK_ARRAY_ENTRIES_WITH_CONFIG
+ && !parseDeviceConfig(resp, respItems.get(CONFIG_INDEX))) {
+ return null;
+ }
+ return resp;
} catch (CborException e) {
Log.e(TAG, "CBOR parsing/serializing failed.", e);
return null;
@@ -152,22 +235,46 @@ public class CborUtils {
}
/**
+ * Creates the bundle of data that the server needs in order to make a decision over what
+ * device configuration values to return. In general, this boils down to if remote provisioning
+ * is turned on at all or not.
+ *
+ * @return the CBOR encoded provisioning information relevant to the server.
+ */
+ public static byte[] buildProvisioningInfo(Context context) {
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new CborEncoder(baos).encode(new CborBuilder()
+ .addMap()
+ .put("fingerprint", Build.FINGERPRINT)
+ .put(new UnicodeString("id"),
+ new UnsignedInteger(SettingsManager.getId(context)))
+ .end()
+ .build());
+ return baos.toByteArray();
+ } catch (CborException e) {
+ Log.e(TAG, "CBOR serialization failed.", e);
+ return EMPTY_MAP;
+ }
+ }
+
+ /**
* Takes the various fields fetched from the server and the remote provisioning service and
* formats them in the CBOR blob the server is expecting as defined by the
* IRemotelyProvisionedComponent HAL AIDL files.
*/
public static byte[] buildCertificateRequest(byte[] deviceInfo, byte[] challenge,
byte[] protectedData, byte[] macedKeysToSign) {
- // This CBOR library doesn't support adding already serialized CBOR structures into a
- // CBOR builder. Because of this, we have to first deserialize the provided parameters
- // back into the library's CBOR object types, and then reserialize them into the
- // desired structure.
+ // This CBOR library doesn't support adding already serialized CBOR structures into a
+ // CBOR builder. Because of this, we have to first deserialize the provided parameters
+ // back into the library's CBOR object types, and then reserialize them into the
+ // desired structure.
try {
// Deserialize the protectedData blob
ByteArrayInputStream bais = new ByteArrayInputStream(protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
- if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.ARRAY) {
- Log.e(TAG, "protectedData is carrying unexpected data.");
+ if (dataItems.size() != 1
+ || !checkType(dataItems.get(0), MajorType.ARRAY, "ProtectedData")) {
return null;
}
Array protectedDataArray = (Array) dataItems.get(0);
@@ -175,8 +282,8 @@ public class CborUtils {
// Deserialize macedKeysToSign
bais = new ByteArrayInputStream(macedKeysToSign);
dataItems = new CborDecoder(bais).decode();
- if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.ARRAY) {
- Log.e(TAG, "macedKeysToSign is carrying unexpected data.");
+ if (dataItems.size() != 1
+ || !checkType(dataItems.get(0), MajorType.ARRAY, "MacedKeysToSign")) {
return null;
}
Array macedKeysToSignArray = (Array) dataItems.get(0);
@@ -184,8 +291,8 @@ public class CborUtils {
// Deserialize deviceInfo
bais = new ByteArrayInputStream(deviceInfo);
dataItems = new CborDecoder(bais).decode();
- if (dataItems.size() != 1 || dataItems.get(0).getMajorType() != MajorType.MAP) {
- Log.e(TAG, "macedKeysToSign is carrying unexpected data.");
+ if (dataItems.size() != 1
+ || !checkType(dataItems.get(0), MajorType.MAP, "DeviceInfo")) {
return null;
}
Map verifiedDeviceInfoMap = (Map) dataItems.get(0);
diff --git a/src/com/android/remoteprovisioner/GeekResponse.java b/src/com/android/remoteprovisioner/GeekResponse.java
index 0a1c798..dcfe040 100644
--- a/src/com/android/remoteprovisioner/GeekResponse.java
+++ b/src/com/android/remoteprovisioner/GeekResponse.java
@@ -16,24 +16,83 @@
package com.android.remoteprovisioner;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* Convenience class for packaging up the values returned by the server when initially requesting
* an Endpoint Encryption Key for remote provisioning. Those values are described by the following
* CDDL Schema:
* GeekResponse = [
- * EekChain,
+ * [+CurveAndEek],
* challenge : bstr,
+ * ? Config,
+ * ]
+ * CurveAndEek = [
+ * curve: uint,
+ * EekChain
* ]
+ * Config = {
+ * ? "num_extra_attestation_keys": uint,
+ * ? "time_to_refresh_hours" : uint,
+ * ? "provisioning_url": tstr,
+ * }
*
* The CDDL that defines EekChain is defined in the RemoteProvisioning HAL, but this app does not
* require any semantic understanding of the format to perform its function.
*/
public class GeekResponse {
- public byte[] challenge;
- public byte[] geek;
+ public static final int NO_EXTRA_KEY_UPDATE = -1;
+ private byte[] mChallenge;
+ private Map<Integer, byte[]> mCurveToGeek;
+ public int numExtraAttestationKeys;
+ public Duration timeToRefresh;
+ public String provisioningUrl;
+
+ /**
+ * Default initializer.
+ */
+ public GeekResponse() {
+ mCurveToGeek = new HashMap();
+ numExtraAttestationKeys = NO_EXTRA_KEY_UPDATE;
+ }
+
+ /**
+ * Add a CBOR encoded array containing a GEEK and the corresponding certificate chain, keyed
+ * on the EC {@code curve}.
+ *
+ * @param curve an integer which represents an EC curve.
+ * @param geekChain the encoded CBOR array containing an ECDH key and corresponding certificate
+ * chain.
+ */
+ public void addGeek(int curve, byte[] geekChain) {
+ mCurveToGeek.put(curve, geekChain);
+ }
+
+ /**
+ * Returns the encoded CBOR array with an ECDH key corresponding to the provided {@code curve}.
+ *
+ * @param curve an integer which represents an EC curve.
+ * @return the corresponding encoded CBOR array.
+ */
+ public byte[] getGeekChain(int curve) {
+ return mCurveToGeek.get(curve);
+ }
+
+ /**
+ * Sets the {@code challenge}.
+ */
+ public void setChallenge(byte[] challenge) {
+ mChallenge = challenge;
+ }
- public GeekResponse(byte[] geek, byte[] challenge) {
- this.geek = geek;
- this.challenge = challenge;
+ /**
+ * Returns the {@code challenge}.
+ *
+ * @return the challenge that will be embedded in the CSR sent to the server.
+ */
+ public byte[] getChallenge() {
+ return mChallenge;
}
}
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index 397ea9a..fd5e69c 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -16,36 +16,48 @@
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 {
- //TODO(b/176249146): Replace default values here with values fetched from the server.
- private static int sTotalSignedKeys = 0;
- // Check for expiring certs in the next 3 days
- private static int sExpiringBy = 1000 * 60 * 60 * 24 * 3;
+
+ 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 occasionally check the attestation key pool
+ * Starts the periodic provisioning job, which will check the attestation key pool
* and provision it as necessary.
*/
public boolean onStartJob(JobParameters params) {
- Log.d(TAG, "Starting provisioning job");
- mProvisionerThread = new ProvisionerThread(params);
+ Log.i(TAG, "Starting provisioning job");
+ mProvisionerThread = new ProvisionerThread(params, this);
mProvisionerThread.start();
return true;
}
@@ -54,15 +66,16 @@ public class PeriodicProvisioner extends JobService {
* Allows the job to be stopped if need be.
*/
public boolean onStopJob(JobParameters params) {
- mProvisionerThread.stop();
return false;
}
private class ProvisionerThread extends Thread {
+ private Context mContext;
private JobParameters mParams;
- ProvisionerThread(JobParameters params) {
+ ProvisionerThread(JobParameters params, Context context) {
mParams = params;
+ mContext = context;
}
public void run() {
@@ -71,45 +84,171 @@ public class PeriodicProvisioner extends JobService {
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
if (binder == null) {
Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
- jobFinished(mParams, true /* wantsReschedule */);
+ jobFinished(mParams, false /* wantsReschedule */);
return;
}
- int[] securityLevels = binder.getSecurityLevels();
- if (securityLevels == null) {
+
+ 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, true /* wantsReschedule */);
+ jobFinished(mParams, false /* wantsReschedule */);
return;
}
- for (int i = 0; i < securityLevels.length; i++) {
- // TODO(b/176249146): Replace expiration date parameter with value fetched from
- // server
- checkAndProvision(binder, System.currentTimeMillis() + sExpiringBy,
- securityLevels[i]);
+ 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, true /* wantsReschedule */);
+ jobFinished(mParams, false /* wantsReschedule */);
Log.e(TAG, "Error on the binder side during provisioning.", e);
} catch (InterruptedException e) {
- jobFinished(mParams, true /* wantsReschedule */);
+ jobFinished(mParams, false /* wantsReschedule */);
Log.e(TAG, "Provisioner thread interrupted.", e);
}
}
- private void checkAndProvision(IRemoteProvisioning binder, long expiringBy, int secLevel)
+ 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 generated = 0;
- while (generated + pool.total - pool.expiring < sTotalSignedKeys) {
- generated++;
+ 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);
- Thread.sleep(5000);
+ // 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 (generated > 0) {
- Log.d(TAG, "Keys generated, moving to provisioning process.");
- Provisioner.provisionCerts(generated, secLevel, binder);
+ if (totalSignedKeys > 0) {
+ return generated + unattestedKeys;
}
+ return 0;
}
}
}
diff --git a/src/com/android/remoteprovisioner/Provisioner.java b/src/com/android/remoteprovisioner/Provisioner.java
index eabe7d3..06a7f4d 100644
--- a/src/com/android/remoteprovisioner/Provisioner.java
+++ b/src/com/android/remoteprovisioner/Provisioner.java
@@ -17,6 +17,7 @@
package com.android.remoteprovisioner;
import android.annotation.NonNull;
+import android.content.Context;
import android.hardware.security.keymint.DeviceInfo;
import android.hardware.security.keymint.ProtectedData;
import android.security.remoteprovisioning.IRemoteProvisioning;
@@ -25,7 +26,7 @@ import android.util.Log;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
+import java.util.List;
/**
* Provides an easy package to run the provisioning process from start to finish, interfacing
@@ -36,57 +37,61 @@ public class Provisioner {
private static final String TAG = "RemoteProvisioningService";
/**
- * Drives the process of provisioning certs. The method first contacts the provided backend
- * server to retrieve an Endpoing Encryption Key with an accompanying certificate chain and a
- * challenge. It passes this data and the requested number of keys to the remote provisioning
- * system backend, which then works with KeyMint in order to get a CSR bundle generated, along
- * with an encrypted package containing metadata that the server needs in order to make
- * decisions about provisioning.
+ * Drives the process of provisioning certs. The method passes the data fetched from the
+ * provisioning server along with the requested number of keys to the remote provisioning
+ * system backend. The backend will talk to the underlying IRemotelyProvisionedComponent
+ * interface in order to get a CSR bundle generated, along with an encrypted package containing
+ * metadata that the server needs in order to make decisions about provisioning.
*
* This method then passes that bundle back out to the server backend, waits for the response,
* and, if successful, passes the certificate chains back to the remote provisioning service to
* be stored and later assigned to apps requesting a key attestation.
*
* @param numKeys The number of keys to be signed. The service will do a best-effort to
- * provision the number requested, but if the number requested is larger
- * than the number of unsigned attestation key pairs available, it will
- * only sign the number that is available at time of calling.
- *
+ * provision the number requested, but if the number requested is larger
+ * than the number of unsigned attestation key pairs available, it will
+ * only sign the number that is available at time of calling.
* @param secLevel Which KM instance should be used to provision certs.
+ * @param geekChain The certificate chain that signs the endpoint encryption key.
+ * @param challenge A server provided challenge to ensure freshness of the response.
* @param binder The IRemoteProvisioning binder interface needed by the method to handle talking
- * to the remote provisioning system component.
- *
- * @return True if certificates were successfully provisioned for the signing keys.
+ * to the remote provisioning system component.
+ * @param context The application context object which enables this method to make use of
+ * SettingsManager.
+ * @return The number of certificates provisoned. Ideally, this should equal {@code numKeys}.
*/
- public static boolean provisionCerts(int numKeys, int secLevel,
- @NonNull IRemoteProvisioning binder) {
+ public static int provisionCerts(int numKeys, int secLevel, byte[] geekChain, byte[] challenge,
+ @NonNull IRemoteProvisioning binder, Context context) {
if (numKeys < 1) {
Log.e(TAG, "Request at least 1 key to be signed. Num requested: " + numKeys);
- return false;
- }
- GeekResponse geek = ServerInterface.fetchGeek();
- if (geek == null) {
- Log.e(TAG, "The geek is null");
- return false;
+ return 0;
}
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData protectedData = new ProtectedData();
byte[] macedKeysToSign =
- SystemInterface.generateCsr(false /* testMode */, numKeys, secLevel, geek,
- protectedData, deviceInfo, binder);
+ SystemInterface.generateCsr(false /* testMode */, numKeys, secLevel, geekChain,
+ challenge, protectedData, deviceInfo, binder);
if (macedKeysToSign == null || protectedData.protectedData == null
|| deviceInfo.deviceInfo == null) {
Log.e(TAG, "Keystore failed to generate a payload");
- return false;
+ return 0;
}
byte[] certificateRequest =
CborUtils.buildCertificateRequest(deviceInfo.deviceInfo,
- geek.challenge,
+ challenge,
protectedData.protectedData,
macedKeysToSign);
- ArrayList<byte[]> certChains =
- new ArrayList<byte[]>(ServerInterface.requestSignedCertificates(
- certificateRequest, geek.challenge));
+ if (certificateRequest == null) {
+ Log.e(TAG, "Failed to serialize the payload generated by keystore.");
+ return 0;
+ }
+ List<byte[]> certChains = ServerInterface.requestSignedCertificates(context,
+ certificateRequest, challenge);
+ if (certChains == null) {
+ Log.e(TAG, "Server response failed on provisioning attempt.");
+ return 0;
+ }
+ int provisioned = 0;
for (byte[] certChain : certChains) {
// DER encoding specifies leaf to root ordering. Pull the public key and expiration
// date from the leaf.
@@ -95,21 +100,25 @@ public class Provisioner {
cert = X509Utils.formatX509Certs(certChain)[0];
} catch (CertificateException e) {
Log.e(TAG, "Failed to interpret DER encoded certificate chain", e);
- return false;
+ return 0;
}
// getTime returns the time in *milliseconds* since the epoch.
long expirationDate = cert.getNotAfter().getTime();
byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(cert);
+ if (rawPublicKey == null) {
+ Log.e(TAG, "Skipping malformed public key.");
+ continue;
+ }
try {
- return SystemInterface.provisionCertChain(rawPublicKey, cert.getEncoded(),
- certChain, expirationDate, secLevel,
- binder);
+ if (SystemInterface.provisionCertChain(rawPublicKey, cert.getEncoded(), certChain,
+ expirationDate, secLevel, binder)) {
+ provisioned++;
+ }
} catch (CertificateEncodingException e) {
Log.e(TAG, "Somehow can't re-encode the decoded batch cert...", e);
- return false;
+ return provisioned;
}
}
- Log.d(TAG, "Reaching this return statement implies the server returned 0 signed certs.");
- return false;
+ return provisioned;
}
}
diff --git a/src/com/android/remoteprovisioner/ServerInterface.java b/src/com/android/remoteprovisioner/ServerInterface.java
index 576e913..0ce70ef 100644
--- a/src/com/android/remoteprovisioner/ServerInterface.java
+++ b/src/com/android/remoteprovisioner/ServerInterface.java
@@ -16,6 +16,7 @@
package com.android.remoteprovisioner;
+import android.content.Context;
import android.util.Base64;
import android.util.Log;
@@ -36,30 +37,28 @@ public class ServerInterface {
private static final int TIMEOUT_MS = 5000;
private static final String TAG = "ServerInterface";
- private static final String PROVISIONING_URL = "https://remoteprovisioning.googleapis.com";
- private static final String GEEK_URL = PROVISIONING_URL + "/v1alpha1:fetchEekChain";
- private static final String CERTIFICATE_SIGNING_URL =
- PROVISIONING_URL + "/v1alpha1:signCertificates?challenge=";
+ private static final String GEEK_URL = ":fetchEekChain";
+ private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?challenge=";
/**
* Ferries the CBOR blobs returned by KeyMint to the provisioning server. The data sent to the
* provisioning server contains the MAC'ed CSRs and encrypted bundle containing the MAC key and
* the hardware unique public key.
*
+ * @param context The application context which is required to use SettingsManager.
* @param csr The CBOR encoded data containing the relevant pieces needed for the server to
* sign the CSRs. The data encoded within comes from Keystore / KeyMint.
- *
* @param challenge The challenge that was sent from the server. It is included here even though
* it is also included in `cborBlob` in order to allow the server to more
* easily reject bad requests.
- *
* @return A List of byte arrays, where each array contains an entire DER-encoded certificate
* chain for one attestation key pair.
*/
- public static List<byte[]> requestSignedCertificates(byte[] csr, byte[] challenge) {
+ public static List<byte[]> requestSignedCertificates(Context context, byte[] csr,
+ byte[] challenge) {
try {
- URL url = new URL(CERTIFICATE_SIGNING_URL
- + Base64.encodeToString(challenge, Base64.URL_SAFE));
+ URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL
+ + Base64.encodeToString(challenge, Base64.URL_SAFE));
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
@@ -69,15 +68,15 @@ public class ServerInterface {
// the output stream being automatically closed.
try (OutputStream os = con.getOutputStream()) {
os.write(csr, 0, csr.length);
- } catch (Exception e) {
- return null;
}
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ int failures = SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server connection for signing failed, response code: "
- + con.getResponseCode());
+ + con.getResponseCode() + "\nRepeated failure count: " + failures);
return null;
}
+ SettingsManager.clearFailureCounter(context);
BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
@@ -87,9 +86,11 @@ public class ServerInterface {
}
return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
} catch (SocketTimeoutException e) {
+ SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server timed out", e);
return null;
} catch (IOException e) {
+ SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Failed to request signed certificates from the server", e);
return null;
}
@@ -103,19 +104,30 @@ public class ServerInterface {
*
* A challenge is also returned from the server so that it can check freshness of the follow-up
* request to get keys signed.
+ *
+ * @param context The application context which is required to use SettingsManager.
+ * @return A GeekResponse object which optionally contains configuration data.
*/
- public static GeekResponse fetchGeek() {
+ public static GeekResponse fetchGeek(Context context) {
try {
- URL url = new URL(GEEK_URL);
+ URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
- con.setRequestMethod("GET");
+ con.setRequestMethod("POST");
con.setConnectTimeout(TIMEOUT_MS);
+ con.setDoOutput(true);
+
+ byte[] config = CborUtils.buildProvisioningInfo(context);
+ try (OutputStream os = con.getOutputStream()) {
+ os.write(config, 0, config.length);
+ }
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ int failures = SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server connection for GEEK failed, response code: "
- + con.getResponseCode());
+ + con.getResponseCode() + "\nRepeated failure count: " + failures);
return null;
}
+ SettingsManager.clearFailureCounter(context);
BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
@@ -127,8 +139,11 @@ public class ServerInterface {
inputStream.close();
return CborUtils.parseGeekResponse(cborBytes.toByteArray());
} catch (SocketTimeoutException e) {
+ SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server timed out", e);
} catch (IOException e) {
+ // This exception will trigger on a completely malformed URL.
+ SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
}
return null;
diff --git a/src/com/android/remoteprovisioner/SettingsManager.java b/src/com/android/remoteprovisioner/SettingsManager.java
new file mode 100644
index 0000000..5808475
--- /dev/null
+++ b/src/com/android/remoteprovisioner/SettingsManager.java
@@ -0,0 +1,188 @@
+/**
+ * Copyright (C) 2021 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 android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import java.time.Duration;
+import java.util.Random;
+
+/**
+ * SettingsManager makes use of SharedPreferences in order to store key/value pairs related to
+ * configuration settings that can be retrieved from the server. In the event that none have yet
+ * been retrieved, or for some reason a reset has occurred, there are reasonable default values.
+ */
+public class SettingsManager {
+
+ public static final int ID_UPPER_BOUND = 1000000;
+ public static final int EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT = 6;
+ // Check for expiring certs in the next 3 days
+ public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3;
+ public static final String URL_DEFAULT = "https://remoteprovisioning.googleapis.com/v1";
+
+ private static final String KEY_EXPIRING_BY = "expiring_by";
+ private static final String KEY_EXTRA_KEYS = "extra_keys";
+ private static final String KEY_ID = "settings_id";
+ private static final String KEY_FAILURE_COUNTER = "failure_counter";
+ private static final String KEY_URL = "url";
+ private static final String PREFERENCES_NAME = "com.android.remoteprovisioner.preferences";
+ private static final String TAG = "RemoteProvisionerSettings";
+
+ /**
+ * Generates a random ID for the use of gradual ramp up of remote provisioning.
+ */
+ public static void generateAndSetId(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ if (sharedPref.contains(KEY_ID)) {
+ // ID is already set, don't rotate it.
+ return;
+ }
+ Log.i(TAG, "Setting ID");
+ Random rand = new Random();
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND));
+ editor.apply();
+ }
+
+ /**
+ * Fetches the generated ID.
+ */
+ public static int getId(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ Random rand = new Random();
+ return sharedPref.getInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND) /* defaultValue */);
+ }
+
+ /**
+ * Sets the remote provisioning configuration values based on what was fetched from the server.
+ * The server is not guaranteed to have sent every available parameter in the config that
+ * was returned to the device, so the parameters should be checked for null values.
+ *
+ * @param extraKeys How many server signed remote provisioning key pairs that should be kept
+ * available in KeyStore.
+ * @param expiringBy How far in the future the app should check for expiring keys.
+ * @param url The base URL for the provisioning server.
+ * @return {@code true} if any settings were updated.
+ */
+ public static boolean setDeviceConfig(Context context, int extraKeys,
+ Duration expiringBy, String url) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ boolean wereUpdatesMade = false;
+ if (extraKeys != GeekResponse.NO_EXTRA_KEY_UPDATE
+ && sharedPref.getInt(KEY_EXTRA_KEYS, -5) != extraKeys) {
+ editor.putInt(KEY_EXTRA_KEYS, extraKeys);
+ wereUpdatesMade = true;
+ }
+ if (expiringBy != null
+ && sharedPref.getLong(KEY_EXPIRING_BY, -1) != expiringBy.toMillis()) {
+ editor.putLong(KEY_EXPIRING_BY, expiringBy.toMillis());
+ wereUpdatesMade = true;
+ }
+ if (url != null && !sharedPref.getString(KEY_URL, "").equals(url)) {
+ editor.putString(KEY_URL, url);
+ wereUpdatesMade = true;
+ }
+ if (wereUpdatesMade) {
+ editor.apply();
+ }
+ return wereUpdatesMade;
+ }
+
+ /**
+ * Gets the setting for how many extra keys should be kept signed and available in KeyStore.
+ */
+ public static int getExtraSignedKeysAvailable(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ return sharedPref.getInt(KEY_EXTRA_KEYS, EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT);
+ }
+
+ /**
+ * Gets the setting for how far into the future the provisioner should check for expiring keys.
+ */
+ public static Duration getExpiringBy(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ return Duration.ofMillis(sharedPref.getLong(KEY_EXPIRING_BY, EXPIRING_BY_MS_DEFAULT));
+ }
+
+ /**
+ * Gets the setting for what base URL the provisioner should use to talk to provisioning
+ * servers.
+ */
+ public static String getUrl(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ return sharedPref.getString(KEY_URL, URL_DEFAULT);
+ }
+
+ /**
+ * Increments the failure counter. This is intended to be used when reaching the server fails
+ * for any reason so that the app logic can decide if the preferences should be reset to
+ * defaults in the event that a bad push stored an incorrect URL string.
+ *
+ * @return the current failure counter after incrementing.
+ */
+ public static int incrementFailureCounter(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ int failures = sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */);
+ editor.putInt(KEY_FAILURE_COUNTER, ++failures);
+ editor.apply();
+ return failures;
+ }
+
+ /**
+ * Gets the current failure counter.
+ */
+ public static int getFailureCounter(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ return sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */);
+ }
+
+ /**
+ * Resets the failure counter to {@code 0}.
+ */
+ public static void clearFailureCounter(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ if (sharedPref.getInt(KEY_FAILURE_COUNTER, 0) != 0) {
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putInt(KEY_FAILURE_COUNTER, 0);
+ editor.apply();
+ }
+ }
+
+ /**
+ * Clears all preferences, thus restoring the defaults.
+ */
+ public static void clearPreferences(Context context) {
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.clear();
+ editor.apply();
+ }
+}
diff --git a/src/com/android/remoteprovisioner/SystemInterface.java b/src/com/android/remoteprovisioner/SystemInterface.java
index 160f185..67ab028 100644
--- a/src/com/android/remoteprovisioner/SystemInterface.java
+++ b/src/com/android/remoteprovisioner/SystemInterface.java
@@ -68,18 +68,22 @@ public class SystemInterface {
* Sends a generateCsr request over the binder interface. `dataBlob` is an out parameter that
* will be populated by the underlying binder service.
*/
- public static byte[] generateCsr(boolean testMode, int numKeys, int secLevel, GeekResponse geek,
- ProtectedData protectedData, DeviceInfo deviceInfo,
+ public static byte[] generateCsr(boolean testMode, int numKeys, int secLevel,
+ byte[] geekChain, byte[] challenge, ProtectedData protectedData, DeviceInfo deviceInfo,
@NonNull IRemoteProvisioning binder) {
try {
ProtectedData dataBundle = new ProtectedData();
byte[] macedPublicKeys = binder.generateCsr(testMode,
numKeys,
- geek.geek,
- geek.challenge,
+ geekChain,
+ challenge,
secLevel,
protectedData,
deviceInfo);
+ if (macedPublicKeys == null) {
+ Log.e(TAG, "Keystore didn't generate a CSR successfully.");
+ return null;
+ }
ByteArrayInputStream bais = new ByteArrayInputStream(macedPublicKeys);
List<DataItem> dataItems = new CborDecoder(bais).decode();
List<DataItem> macInfo = ((Array) dataItems.get(0)).getDataItems();
diff --git a/src/com/android/remoteprovisioner/X509Utils.java b/src/com/android/remoteprovisioner/X509Utils.java
index 60fee4a..d33d573 100644
--- a/src/com/android/remoteprovisioner/X509Utils.java
+++ b/src/com/android/remoteprovisioner/X509Utils.java
@@ -16,8 +16,11 @@
package com.android.remoteprovisioner;
+import android.util.Log;
+
import java.io.ByteArrayInputStream;
import java.math.BigInteger;
+import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -30,6 +33,8 @@ import java.util.ArrayList;
*/
public class X509Utils {
+ private static final String TAG = "RemoteProvisionerX509Utils";
+
/**
* Takes a byte array composed of DER encoded certificates and returns the X.509 certificates
* contained within as an X509Certificate array.
@@ -47,6 +52,11 @@ public class X509Utils {
* the certificate chain to the proper key when passed into the keystore database.
*/
public static byte[] getAndFormatRawPublicKey(X509Certificate cert) {
+ PublicKey pubKey = cert.getPublicKey();
+ if (!(pubKey instanceof ECPublicKey)) {
+ Log.e(TAG, "Certificate public key is not an instance of ECPublicKey");
+ return null;
+ }
ECPublicKey key = (ECPublicKey) cert.getPublicKey();
// Remote key provisioning internally supports the default, uncompressed public key
// format for ECDSA. This defines the format as (s | x | y), where s is the byte
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index f505d28..73c83b1 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -17,21 +17,27 @@
package com.android.remoteprovisioner.service;
import android.app.Service;
+import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.security.IGenerateRkpKeyService;
import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
import android.security.remoteprovisioning.IRemoteProvisioning;
import android.util.Log;
+import com.android.remoteprovisioner.GeekResponse;
import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
/**
* Provides the implementation for IGenerateKeyService.aidl
*/
public class GenerateRkpKeyService extends Service {
+ private static final int KEY_GENERATION_PAUSE_MS = 1000;
private static final String SERVICE = "android.security.remoteprovisioning";
private static final String TAG = "RemoteProvisioningService";
@@ -51,7 +57,6 @@ public class GenerateRkpKeyService extends Service {
try {
IRemoteProvisioning binder =
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
- // Iterate through each security level backend
checkAndFillPool(binder, securityLevel);
} catch (RemoteException e) {
Log.e(TAG, "Remote Exception: ", e);
@@ -63,7 +68,6 @@ public class GenerateRkpKeyService extends Service {
try {
IRemoteProvisioning binder =
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
- // Iterate through each security level backend
checkAndFillPool(binder, securityLevel);
} catch (RemoteException e) {
Log.e(TAG, "Remote Exception: ", e);
@@ -74,13 +78,39 @@ public class GenerateRkpKeyService extends Service {
throws RemoteException {
AttestationPoolStatus pool =
binder.getPoolStatus(System.currentTimeMillis(), secLevel);
- // If there are no unassigned keys, go ahead and provision some. If there are no keys
- // at all on system, this implies that it is a hybrid rkp/factory-provisioned system
- // that has turned off RKP. In that case, do not provision.
- if (pool.unassigned == 0 && pool.total != 0) {
- Log.d(TAG, "All signed keys are currently in use, provisioning more.");
- binder.generateKeyPair(false /* isTestMode */, secLevel);
- Provisioner.provisionCerts(1 /* numCsr */, secLevel, binder);
+ ImplInfo[] implInfos = binder.getImplementationInfo();
+ int curve = 0;
+ for (int i = 0; i < implInfos.length; i++) {
+ if (implInfos[i].secLevel == secLevel) {
+ curve = implInfos[i].supportedCurve;
+ break;
+ }
+ }
+ // If there are no unassigned keys, go ahead and provision some. If there are no
+ // attested keys at all on the system, this implies that it is a hybrid
+ // rkp/factory-provisioned system that has turned off RKP. In that case, do
+ // not provision.
+ if (pool.unassigned == 0 && pool.attested != 0) {
+ Log.i(TAG, "All signed keys are currently in use, provisioning more.");
+ Context context = getApplicationContext();
+ int keysToProvision = SettingsManager.getExtraSignedKeysAvailable(context);
+ int existingUnsignedKeys = pool.total - pool.attested;
+ int keysToGenerate = keysToProvision - existingUnsignedKeys;
+ try {
+ for (int i = 0; i < keysToGenerate; i++) {
+ binder.generateKeyPair(false /* isTestMode */, secLevel);
+ Thread.sleep(KEY_GENERATION_PAUSE_MS);
+ }
+ } catch (InterruptedException e) {
+ Log.i(TAG, "Thread interrupted", e);
+ }
+ GeekResponse resp = ServerInterface.fetchGeek(context);
+ if (resp == null) {
+ Log.e(TAG, "Server unavailable");
+ return;
+ }
+ Provisioner.provisionCerts(keysToProvision, secLevel, resp.getGeekChain(curve),
+ resp.getChallenge(), binder, context);
}
}
};
diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp
index c2ed374..2aafdc7 100644
--- a/tests/unittests/Android.bp
+++ b/tests/unittests/Android.bp
@@ -19,6 +19,7 @@ android_test {
name: "RemoteProvisionerUnitTests",
srcs: ["src/**/*.java"],
static_libs: [
+ "androidx.test.core",
"androidx.test.rules",
"android.security.remoteprovisioning-java",
"platform-test-annotations",
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
index 4c30a6e..cc561a8 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/CborUtilsTest.java
@@ -31,8 +31,12 @@ import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborEncoder;
import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.MajorType;
+import co.nstant.in.cbor.model.Map;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
import org.junit.Before;
import org.junit.Test;
@@ -47,10 +51,48 @@ import java.util.List;
public class CborUtilsTest {
private ByteArrayOutputStream mBaos;
+ private Array mGeekChain1;
+ private byte[] mEncodedmGeekChain1;
+ private Array mGeekChain2;
+ private byte[] mEncodedGeekChain2;
+ private Map mDeviceConfig;
+ private static final byte[] CHALLENGE = new byte[]{0x0a, 0x0b, 0x0c};
+ private static final int TEST_EXTRA_KEYS = 18;
+ private static final int TEST_TIME_TO_REFRESH_HOURS = 42;
+ private static final String TEST_URL = "https://www.wonderifthisisvalid.combutjustincase";
+
+ private byte[] encodeDataItem(DataItem toEncode) throws Exception {
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .add(toEncode)
+ .build());
+ byte[] encoded = mBaos.toByteArray();
+ mBaos.reset();
+ return encoded;
+ }
@Before
public void setUp() throws Exception {
mBaos = new ByteArrayOutputStream();
+
+ mGeekChain1 = new Array();
+ mGeekChain1.add(new ByteString(new byte[] {0x01, 0x02, 0x03}))
+ .add(new ByteString(new byte[] {0x04, 0x05, 0x06}))
+ .add(new ByteString(new byte[] {0x07, 0x08, 0x09}));
+ mEncodedmGeekChain1 = encodeDataItem(mGeekChain1);
+
+ mGeekChain2 = new Array();
+ mGeekChain2.add(new ByteString(new byte[] {0x09, 0x08, 0x07}))
+ .add(new ByteString(new byte[] {0x06, 0x05, 0x04}))
+ .add(new ByteString(new byte[] {0x03, 0x02, 0x01}));
+ mEncodedGeekChain2 = encodeDataItem(mGeekChain2);
+
+ mDeviceConfig = new Map();
+ mDeviceConfig.put(new UnicodeString(CborUtils.EXTRA_KEYS),
+ new UnsignedInteger(TEST_EXTRA_KEYS))
+ .put(new UnicodeString(CborUtils.TIME_TO_REFRESH),
+ new UnsignedInteger(TEST_TIME_TO_REFRESH_HOURS))
+ .put(new UnicodeString(CborUtils.PROVISIONING_URL),
+ new UnicodeString(TEST_URL));
}
@Presubmit
@@ -116,39 +158,182 @@ public class CborUtilsTest {
public void testParseGeekResponseFakeData() throws Exception {
new CborEncoder(mBaos).encode(new CborBuilder()
.addArray()
- .addArray() // GEEK Chain
- .add(new byte[] {0x01, 0x02, 0x03})
- .add(new byte[] {0x04, 0x05, 0x06})
- .add(new byte[] {0x07, 0x08, 0x09})
+ .addArray() // GEEK Curve to Chains
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+ .add(mGeekChain2)
+ .end()
.end()
- .add(new byte[] {0x0a, 0x0b, 0x0c}) // Challenge
+ .add(CHALLENGE)
+ .add(mDeviceConfig)
.end()
.build());
GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
mBaos.reset();
+ assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+ assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+ assertArrayEquals(CHALLENGE, resp.getChallenge());
+ assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+ assertEquals(TEST_TIME_TO_REFRESH_HOURS, resp.timeToRefresh.toHours());
+ assertEquals(TEST_URL, resp.provisioningUrl);
+ }
+
+ @Test
+ public void testExtraDeviceConfigEntriesDontFail() throws Exception {
new CborEncoder(mBaos).encode(new CborBuilder()
.addArray()
- .add(new byte[] {0x01, 0x02, 0x03})
- .add(new byte[] {0x04, 0x05, 0x06})
- .add(new byte[] {0x07, 0x08, 0x09})
+ .addArray() // GEEK Curve to Chains
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+ .add(mGeekChain2)
+ .end()
+ .end()
+ .add(CHALLENGE)
+ .add(mDeviceConfig.put(new UnicodeString("new_field"),
+ new UnsignedInteger(84)))
.end()
.build());
- byte[] expectedGeek = mBaos.toByteArray();
- assertArrayEquals(expectedGeek, resp.geek);
- assertArrayEquals(new byte[] {0x0a, 0x0b, 0x0c}, resp.challenge);
+ GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+ mBaos.reset();
+ assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+ assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+ assertArrayEquals(CHALLENGE, resp.getChallenge());
+ assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+ assertEquals(TEST_TIME_TO_REFRESH_HOURS, resp.timeToRefresh.toHours());
+ assertEquals(TEST_URL, resp.provisioningUrl);
}
@Test
- public void testParseGeekResponseWrongSize() throws Exception {
+ public void testMissingDeviceConfigDoesntFail() throws Exception {
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .addArray() // GEEK Curve to Chains
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+ .add(mGeekChain2)
+ .end()
+ .end()
+ .add(CHALLENGE)
+ .end()
+ .build());
+ GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+ mBaos.reset();
+ assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+ assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+ assertArrayEquals(CHALLENGE, resp.getChallenge());
+ assertEquals(GeekResponse.NO_EXTRA_KEY_UPDATE, resp.numExtraAttestationKeys);
+ assertEquals(null, resp.timeToRefresh);
+ assertEquals(null, resp.provisioningUrl);
+ }
+
+ @Test
+ public void testMissingDeviceConfigEntriesDoesntFail() throws Exception {
+ mDeviceConfig.remove(new UnicodeString(CborUtils.TIME_TO_REFRESH));
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .addArray() // GEEK Curve to Chains
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_P256))
+ .add(mGeekChain2)
+ .end()
+ .end()
+ .add(CHALLENGE)
+ .add(mDeviceConfig)
+ .end()
+ .build());
+ GeekResponse resp = CborUtils.parseGeekResponse(mBaos.toByteArray());
+ mBaos.reset();
+ assertArrayEquals(mEncodedmGeekChain1, resp.getGeekChain(CborUtils.EC_CURVE_25519));
+ assertArrayEquals(mEncodedGeekChain2, resp.getGeekChain(CborUtils.EC_CURVE_P256));
+ assertArrayEquals(CHALLENGE, resp.getChallenge());
+ assertEquals(TEST_EXTRA_KEYS, resp.numExtraAttestationKeys);
+ assertEquals(null, resp.timeToRefresh);
+ assertEquals(TEST_URL, resp.provisioningUrl);
+ }
+
+ @Test
+ public void testParseGeekResponseFailsOnWrongType() throws Exception {
new CborEncoder(mBaos).encode(new CborBuilder()
.addArray()
.addArray()
- .add(new byte[] {0x01, 0x02, 0x03})
- .add(new byte[] {0x04, 0x05, 0x06})
- .add(new byte[] {0x07, 0x08, 0x09})
+ .addArray()
+ .add("String instead of curve enum")
+ .add(mGeekChain1)
+ .end()
+ .end()
+ .add(CHALLENGE)
+ .add(mDeviceConfig)
+ .end()
+ .build());
+ assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+ mBaos.reset();
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .addArray()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(new ByteString(CHALLENGE)) // Must be an array of bstrs
+ .end()
+ .end()
+ .add(CHALLENGE)
+ .add(mDeviceConfig)
+ .end()
+ .build());
+ assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+ mBaos.reset();
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .addArray()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
+ .end()
+ .add(new UnicodeString("tstr instead of bstr"))
+ .add(mDeviceConfig)
+ .end()
+ .build());
+ assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+ mBaos.reset();
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .addArray()
+ .addArray()
+ .add(new UnsignedInteger(CborUtils.EC_CURVE_25519))
+ .add(mGeekChain1)
+ .end()
.end()
- .add(new byte[] {0x0a, 0x0b, 0x0c})
- .add("One more entry than there should be")
+ .add(CHALLENGE)
+ .add(CHALLENGE)
+ .end()
+ .build());
+ assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
+ }
+
+ @Test
+ public void testParseGeekResponseWrongSize() throws Exception {
+ new CborEncoder(mBaos).encode(new CborBuilder()
+ .addArray()
+ .add("one entry")
+ .add("two entries")
+ .add("three entries")
+ .add("whoops")
.end()
.build());
assertNull(CborUtils.parseGeekResponse(mBaos.toByteArray()));
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
new file mode 100644
index 0000000..94206af
--- /dev/null
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2021 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.unittest;
+
+import static android.hardware.security.keymint.SecurityLevel.TRUSTED_ENVIRONMENT;
+import static android.security.keystore.KeyProperties.KEY_ALGORITHM_EC;
+import static android.security.keystore.KeyProperties.PURPOSE_SIGN;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.os.ServiceManager;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.remoteprovisioning.AttestationPoolStatus;
+import android.security.remoteprovisioning.ImplInfo;
+import android.security.remoteprovisioning.IRemoteProvisioning;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.remoteprovisioner.GeekResponse;
+import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class ServerToSystemTest {
+
+ private static final boolean IS_TEST_MODE = false;
+ private static final String SERVICE = "android.security.remoteprovisioning";
+
+ private static Context sContext;
+ private static IRemoteProvisioning sBinder;
+ private static int sCurve = 0;
+
+ private Duration mDuration;
+
+ private void assertPoolStatus(int total, int attested,
+ int unassigned, int expiring, Duration time) throws Exception {
+ AttestationPoolStatus pool = sBinder.getPoolStatus(time.toMillis(), TRUSTED_ENVIRONMENT);
+ assertEquals(total, pool.total);
+ assertEquals(attested, pool.attested);
+ assertEquals(unassigned, pool.unassigned);
+ assertEquals(expiring, pool.expiring);
+ }
+
+ private static Certificate[] generateKeyStoreKey(String alias) throws Exception {
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_EC,
+ "AndroidKeyStore");
+ KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, PURPOSE_SIGN)
+ .setAttestationChallenge("challenge".getBytes())
+ .build();
+ keyPairGenerator.initialize(spec);
+ keyPairGenerator.generateKeyPair();
+ Certificate[] certs = keyStore.getCertificateChain(spec.getKeystoreAlias());
+ keyStore.deleteEntry(alias);
+ return certs;
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ sContext = ApplicationProvider.getApplicationContext();
+ sBinder =
+ IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+ assertNotNull(sBinder);
+ ImplInfo[] info = sBinder.getImplementationInfo();
+ for (int i = 0; i < info.length; i++) {
+ if (info[i].secLevel == TRUSTED_ENVIRONMENT) {
+ sCurve = info[i].supportedCurve;
+ break;
+ }
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ SettingsManager.clearPreferences(sContext);
+ sBinder.deleteAllKeys();
+ mDuration = Duration.ofMillis(System.currentTimeMillis());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ SettingsManager.clearPreferences(sContext);
+ sBinder.deleteAllKeys();
+ }
+
+ @Test
+ public void testFullRoundTrip() throws Exception {
+ int numTestKeys = 1;
+ assertPoolStatus(0, 0, 0, 0, mDuration);
+ sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+ assertPoolStatus(numTestKeys, 0, 0, 0, mDuration);
+ GeekResponse geek = ServerInterface.fetchGeek(sContext);
+ assertNotNull(geek);
+ int numProvisioned =
+ Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
+ geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
+ sContext);
+ assertEquals(numTestKeys, numProvisioned);
+ assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
+ // Certificate duration sent back from the server may change, however ~6 months should be
+ // pretty safe.
+ assertPoolStatus(numTestKeys, numTestKeys, numTestKeys,
+ numTestKeys, mDuration.plusDays(180));
+ }
+
+ @Test
+ public void testFallback() throws Exception {
+ // Feed a fake URL into the device config to ensure that remote provisioning fails.
+ SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */,
+ "Not even a URL" /* url */);
+ int numTestKeys = 1;
+ assertPoolStatus(0, 0, 0, 0, mDuration);
+ Certificate[] fallbackKeyCerts1 = generateKeyStoreKey("test1");
+
+ SettingsManager.clearPreferences(sContext);
+ sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+ GeekResponse geek = ServerInterface.fetchGeek(sContext);
+ int numProvisioned =
+ Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
+ geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
+ sContext);
+ assertEquals(numTestKeys, numProvisioned);
+ assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
+ Certificate[] provisionedKeyCerts = generateKeyStoreKey("test2");
+ sBinder.deleteAllKeys();
+ sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
+
+ SettingsManager.setDeviceConfig(sContext, 2 /* extraKeys */, mDuration /* expiringBy */,
+ "Not even a URL" /* url */);
+ // Even if there is an unsigned key hanging around, fallback should still occur.
+ Certificate[] fallbackKeyCerts2 = generateKeyStoreKey("test3");
+ // Due to there being no attested keys in the pool, the provisioning service should not
+ // have even attempted to provision more certificates.
+ assertEquals(0, SettingsManager.getFailureCounter(sContext));
+ assertTrue(fallbackKeyCerts1.length == fallbackKeyCerts2.length);
+ for (int i = 1; i < fallbackKeyCerts1.length; i++) {
+ assertArrayEquals("Cert: " + i, fallbackKeyCerts1[i].getEncoded(),
+ fallbackKeyCerts2[i].getEncoded());
+ }
+ assertTrue(provisionedKeyCerts.length > 0);
+ // The root certificates should not match.
+ assertFalse("Provisioned and fallback attestation key root certificates match.",
+ Arrays.equals(fallbackKeyCerts1[fallbackKeyCerts1.length - 1].getEncoded(),
+ provisionedKeyCerts[provisionedKeyCerts.length - 1].getEncoded()));
+ }
+}
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
new file mode 100644
index 0000000..7db20a9
--- /dev/null
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.unittest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.remoteprovisioner.SettingsManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+
+@RunWith(AndroidJUnit4.class)
+public class SettingsManagerTest {
+
+ private static Context sContext;
+
+ @BeforeClass
+ public static void init() {
+ sContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Before
+ public void setUp() {
+ SettingsManager.clearPreferences(sContext);
+ }
+
+ @After
+ public void tearDown() {
+ SettingsManager.clearPreferences(sContext);
+ }
+
+ @Test
+ public void testCheckDefaults() throws Exception {
+ assertEquals(SettingsManager.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+ SettingsManager.getExtraSignedKeysAvailable(sContext));
+ assertEquals(SettingsManager.EXPIRING_BY_MS_DEFAULT,
+ SettingsManager.getExpiringBy(sContext).toMillis());
+ assertEquals(SettingsManager.URL_DEFAULT,
+ SettingsManager.getUrl(sContext));
+ assertEquals(0, SettingsManager.getFailureCounter(sContext));
+ }
+
+ @Test
+ public void testCheckIdSettings() throws Exception {
+ int defaultRandom = SettingsManager.getId(sContext);
+ assertTrue("Default ID out of bounds.",
+ defaultRandom < SettingsManager.ID_UPPER_BOUND && defaultRandom >= 0);
+ SettingsManager.generateAndSetId(sContext);
+ int setId = SettingsManager.getId(sContext);
+ assertTrue("Stored ID out of bounds.",
+ setId < SettingsManager.ID_UPPER_BOUND && setId >= 0);
+ SettingsManager.generateAndSetId(sContext);
+ assertEquals("ID should not be updated by a repeated call",
+ setId, SettingsManager.getId(sContext));
+ }
+
+ @Test
+ public void testSetDeviceConfig() {
+ int extraKeys = 12;
+ Duration expiringBy = Duration.ofMillis(1000);
+ String url = "https://www.remoteprovisionalot";
+ assertTrue("Method did not return true on write.",
+ SettingsManager.setDeviceConfig(sContext, extraKeys, expiringBy, url));
+ assertEquals(extraKeys, SettingsManager.getExtraSignedKeysAvailable(sContext));
+ assertEquals(expiringBy.toMillis(), SettingsManager.getExpiringBy(sContext).toMillis());
+ assertEquals(url, SettingsManager.getUrl(sContext));
+ }
+
+ @Test
+ public void testFailureCounter() {
+ assertEquals(1, SettingsManager.incrementFailureCounter(sContext));
+ assertEquals(1, SettingsManager.getFailureCounter(sContext));
+ for (int i = 1; i < 10; i++) {
+ assertEquals(i + 1, SettingsManager.incrementFailureCounter(sContext));
+ }
+ SettingsManager.clearFailureCounter(sContext);
+ assertEquals(0, SettingsManager.getFailureCounter(sContext));
+ SettingsManager.incrementFailureCounter(sContext);
+ assertEquals(1, SettingsManager.getFailureCounter(sContext));
+ }
+}
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
index 2a2adc4..44be452 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
@@ -38,7 +38,7 @@ import android.security.remoteprovisioning.IRemoteProvisioning;
import androidx.test.runner.AndroidJUnit4;
-import com.android.remoteprovisioner.GeekResponse;
+import com.android.remoteprovisioner.CborUtils;
import com.android.remoteprovisioner.SystemInterface;
import com.android.remoteprovisioner.X509Utils;
@@ -119,11 +119,11 @@ public class SystemInterfaceTest {
ProtectedData encryptedBundle = new ProtectedData();
byte[] eek = new byte[32];
new Random().nextBytes(eek);
- GeekResponse geek = new GeekResponse(generateEekChain(eek), new byte[] {0x02});
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, 0 /* numKeys */,
SecurityLevel.TRUSTED_ENVIRONMENT,
- geek, encryptedBundle, deviceInfo, mBinder);
+ generateEekChain(eek),
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
// encryptedBundle should contain a COSE_Encrypt message
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
@@ -156,14 +156,14 @@ public class SystemInterfaceTest {
int numKeys = 10;
byte[] eek = new byte[32];
new Random().nextBytes(eek);
- GeekResponse geek = new GeekResponse(generateEekChain(eek), new byte[] {0x02});
for (int i = 0; i < numKeys; i++) {
mBinder.generateKeyPair(true /* testMode */, SecurityLevel.TRUSTED_ENVIRONMENT);
}
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, numKeys,
SecurityLevel.TRUSTED_ENVIRONMENT,
- geek, encryptedBundle, deviceInfo, mBinder);
+ generateEekChain(eek),
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
assertNotNull(bundle);
// The return value of generateCsr should be a COSE_Mac0 message
ByteArrayInputStream bais = new ByteArrayInputStream(bundle);
@@ -280,12 +280,12 @@ public class SystemInterfaceTest {
int numKeys = 1;
byte[] eekPriv = X25519.generatePrivateKey();
byte[] eekPub = X25519.publicFromPrivate(eekPriv);
- GeekResponse geek = new GeekResponse(generateEekChain(eekPub), new byte[] {0x02});
mBinder.generateKeyPair(true /* testMode */, SecurityLevel.TRUSTED_ENVIRONMENT);
byte[] bundle =
SystemInterface.generateCsr(true /* testMode */, numKeys,
SecurityLevel.TRUSTED_ENVIRONMENT,
- geek, encryptedBundle, deviceInfo, mBinder);
+ generateEekChain(eekPub),
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
// Parse encMsg into components: protected and unprotected headers, payload, and recipient