diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-08-10 07:30:43 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-08-10 07:30:43 +0000 |
commit | 969a267567794489f39ffaa12c527cf557f9a3b4 (patch) | |
tree | 2b5fe390795a99edf02e2729c99bb42312ffc58d | |
parent | 305c3dbff7a87b08cc8bed01d856c977ff655591 (diff) | |
parent | 27ab6419029734e396f30848daa704d8dfefec2d (diff) | |
download | RemoteProvisioner-android12-mainline-permission-release.tar.gz |
Snap for 7627094 from 27ab6419029734e396f30848daa704d8dfefec2d to mainline-permission-releaseandroid-mainline-12.0.0_r93android-mainline-12.0.0_r79android-mainline-12.0.0_r64android-mainline-12.0.0_r51android-mainline-12.0.0_r34android-mainline-12.0.0_r15android-mainline-12.0.0_r123android-mainline-12.0.0_r107android12-mainline-permission-release
Change-Id: I3193df2f020f1f6491ffad7165eef815429ad42b
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 |