aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSeth Moore <sethmo@google.com>2022-05-12 14:51:58 -0700
committerSeth Moore <sethmo@google.com>2022-05-15 13:05:59 -0700
commit77f611f12aea5309d008929b1094682453639024 (patch)
tree39b70ee13a158c8fc786264a22badc2a665859e5
parent6ac2f49905421c32aeca7a29173f9550aff8e059 (diff)
downloadRemoteProvisioner-77f611f12aea5309d008929b1094682453639024.tar.gz
Add metrics for remote provisioner
We collect various event metrics now for the remote provisioner. Include tests for the metrics. Bug: 231495834 Test: RemoteProvisionerUnitTests Test: RemoteProvisionerStatsTests Change-Id: I561ead9a0f34363f3964e89a32d7b6b6ce0f2c7f
-rw-r--r--src/com/android/remoteprovisioner/PeriodicProvisioner.java59
-rw-r--r--src/com/android/remoteprovisioner/Provisioner.java12
-rw-r--r--src/com/android/remoteprovisioner/ProvisionerMetrics.java439
-rw-r--r--src/com/android/remoteprovisioner/ServerInterface.java106
-rw-r--r--src/com/android/remoteprovisioner/SystemInterface.java58
-rw-r--r--src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java55
-rw-r--r--tests/hosttest/Android.bp40
-rw-r--r--tests/hosttest/AndroidTest.xml28
-rw-r--r--tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerStatsTests.java474
-rw-r--r--tests/unittests/Android.bp2
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java160
-rw-r--r--tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java42
12 files changed, 1356 insertions, 119 deletions
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index 1291376..e427568 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -60,17 +60,20 @@ public class PeriodicProvisioner extends Worker {
@Override
public Result doWork() {
Log.i(TAG, "Waking up; checking provisioning state.");
- try {
+ try (ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(
+ mContext)) {
IRemoteProvisioning binder =
IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
if (binder == null) {
Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
+ metrics.setStatus(ProvisionerMetrics.Status.INTERNAL_ERROR);
return Result.failure();
}
ImplInfo[] implInfos = binder.getImplementationInfo();
if (implInfos == null) {
Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
+ SERVICE);
+ metrics.setStatus(ProvisionerMetrics.Status.NO_PROVISIONING_NEEDED);
return Result.failure();
}
int[] keysNeededForSecLevel = new int[implInfos.length];
@@ -78,31 +81,46 @@ public class PeriodicProvisioner extends Worker {
if (SettingsManager.getExtraSignedKeysAvailable(mContext) == 0) {
// Provisioning has been purposefully disabled in the past. Go ahead and grab
// an EEK just to see if provisioning should resume.
- resp = fetchGeekAndUpdate(binder);
+ resp = fetchGeekAndUpdate(binder, metrics);
if (resp.numExtraAttestationKeys == 0) {
+ metrics.setEnablement(ProvisionerMetrics.Enablement.DISABLED);
+ metrics.setStatus(ProvisionerMetrics.Status.PROVISIONING_DISABLED);
return Result.success();
}
}
boolean provisioningNeeded =
isProvisioningNeeded(binder,
SettingsManager.getExpirationTime(mContext).toEpochMilli(),
- implInfos, keysNeededForSecLevel);
+ implInfos, keysNeededForSecLevel, metrics);
if (!provisioningNeeded) {
+ metrics.setStatus(ProvisionerMetrics.Status.NO_PROVISIONING_NEEDED);
return Result.success();
}
// Resp may already be populated in the extremely rare case that this job is executing
// to resume provisioning for the first time after a server-induced RKP shutdown. Grab
// a fresh response anyways to refresh the challenge.
- resp = fetchGeekAndUpdate(binder);
+ resp = fetchGeekAndUpdate(binder, metrics);
if (resp.numExtraAttestationKeys == 0) {
+ metrics.setEnablement(ProvisionerMetrics.Enablement.DISABLED);
+ metrics.setStatus(ProvisionerMetrics.Status.PROVISIONING_DISABLED);
return Result.success();
+ } else {
+ // Just in case we got an updated config, let's recalculate how many keys need to
+ // be provisioned.
+ if (!isProvisioningNeeded(binder,
+ SettingsManager.getExpirationTime(mContext).toEpochMilli(),
+ implInfos, keysNeededForSecLevel, metrics)) {
+ metrics.setStatus(ProvisionerMetrics.Status.NO_PROVISIONING_NEEDED);
+ return Result.success();
+ }
}
for (int i = 0; i < implInfos.length; i++) {
// Break very large CSR requests into chunks, so as not to overwhelm the
// backend.
int keysToProvision = keysNeededForSecLevel[i];
batchProvision(binder, mContext, keysToProvision, implInfos[i].secLevel,
- resp.getGeekChain(implInfos[i].supportedCurve), resp.getChallenge());
+ resp.getGeekChain(implInfos[i].supportedCurve), resp.getChallenge(),
+ metrics);
}
return Result.success();
} catch (RemoteException e) {
@@ -126,9 +144,10 @@ public class PeriodicProvisioner extends Worker {
* values. This will also delete all keys in the attestation key pool if the server has
* indicated that RKP should be turned off.
*/
- private GeekResponse fetchGeekAndUpdate(IRemoteProvisioning binder)
+ private GeekResponse fetchGeekAndUpdate(IRemoteProvisioning binder,
+ ProvisionerMetrics metrics)
throws RemoteException, RemoteProvisioningException {
- GeekResponse resp = ServerInterface.fetchGeek(mContext);
+ GeekResponse resp = ServerInterface.fetchGeek(mContext, metrics);
SettingsManager.setDeviceConfig(mContext,
resp.numExtraAttestationKeys,
resp.timeToRefresh,
@@ -136,14 +155,17 @@ public class PeriodicProvisioner extends Worker {
if (resp.numExtraAttestationKeys == 0) {
// The server has indicated that provisioning is disabled.
- binder.deleteAllKeys();
+ try (ProvisionerMetrics.StopWatch ignored = metrics.startBinderWait()) {
+ binder.deleteAllKeys();
+ }
}
return resp;
}
public static void batchProvision(IRemoteProvisioning binder, Context context,
- int keysToProvision, int secLevel,
- byte[] geekChain, byte[] challenge)
+ int keysToProvision, int secLevel,
+ byte[] geekChain, byte[] challenge,
+ ProvisionerMetrics metrics)
throws RemoteException, RemoteProvisioningException {
while (keysToProvision != 0) {
int batchSize = min(keysToProvision, SAFE_CSR_BATCH_SIZE);
@@ -153,14 +175,16 @@ public class PeriodicProvisioner extends Worker {
geekChain,
challenge,
binder,
- context);
+ context,
+ metrics);
keysToProvision -= batchSize;
}
+ metrics.setStatus(ProvisionerMetrics.Status.KEYS_SUCCESSFULLY_PROVISIONED);
}
private boolean isProvisioningNeeded(
IRemoteProvisioning binder, long expiringBy, ImplInfo[] implInfos,
- int[] keysNeededForSecLevel)
+ int[] keysNeededForSecLevel, ProvisionerMetrics metrics)
throws InterruptedException, RemoteException {
if (implInfos == null || keysNeededForSecLevel == null
|| keysNeededForSecLevel.length != implInfos.length) {
@@ -173,7 +197,8 @@ public class PeriodicProvisioner extends Worker {
generateNumKeysNeeded(binder,
mContext,
expiringBy,
- implInfos[i].secLevel);
+ implInfos[i].secLevel,
+ metrics);
if (keysNeededForSecLevel[i] > 0) {
provisioningNeeded = true;
}
@@ -193,10 +218,11 @@ public class PeriodicProvisioner extends Worker {
* removes apps that may also use attestation.
*/
public static int generateNumKeysNeeded(IRemoteProvisioning binder, Context context,
- long expiringBy, int secLevel)
+ long expiringBy, int secLevel,
+ ProvisionerMetrics metrics)
throws InterruptedException, RemoteException {
AttestationPoolStatus pool =
- SystemInterface.getPoolStatus(expiringBy, secLevel, binder);
+ SystemInterface.getPoolStatus(expiringBy, secLevel, binder, metrics);
if (pool == null) {
Log.e(TAG, "Failed to fetch pool status.");
return 0;
@@ -214,7 +240,8 @@ public class PeriodicProvisioner extends Worker {
Log.i(TAG, "Need to generate " + stats.keysToGenerate + " keys.");
int generated;
for (generated = 0; generated < stats.keysToGenerate; generated++) {
- SystemInterface.generateKeyPair(SettingsManager.isTestMode(), secLevel, binder);
+ SystemInterface.generateKeyPair(SettingsManager.isTestMode(), secLevel, binder,
+ metrics);
// 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) {
diff --git a/src/com/android/remoteprovisioner/Provisioner.java b/src/com/android/remoteprovisioner/Provisioner.java
index 293ab00..c259abe 100644
--- a/src/com/android/remoteprovisioner/Provisioner.java
+++ b/src/com/android/remoteprovisioner/Provisioner.java
@@ -35,7 +35,7 @@ import java.util.List;
* attestation certificates to the device.
*/
public class Provisioner {
- private static final String TAG = "RemoteProvisioningService";
+ protected static final String TAG = "RemoteProvisioningService";
/**
* Drives the process of provisioning certs. The method passes the data fetched from the
@@ -59,10 +59,12 @@ public class Provisioner {
* to the remote provisioning system component.
* @param context The application context object which enables this method to make use of
* SettingsManager.
+ * @param metrics Datapoint collector for logging metrics.
* @return The number of certificates provisioned. Ideally, this should equal {@code numKeys}.
*/
public static int provisionCerts(int numKeys, int secLevel, byte[] geekChain, byte[] challenge,
- @NonNull IRemoteProvisioning binder, Context context) throws
+ @NonNull IRemoteProvisioning binder, Context context,
+ ProvisionerMetrics metrics) throws
RemoteProvisioningException {
Log.i(TAG, "Request for " + numKeys + " keys to be provisioned.");
if (numKeys < 1) {
@@ -72,7 +74,7 @@ public class Provisioner {
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData protectedData = new ProtectedData();
byte[] macedKeysToSign = SystemInterface.generateCsr(SettingsManager.isTestMode(), numKeys,
- secLevel, geekChain, challenge, protectedData, deviceInfo, binder);
+ secLevel, geekChain, challenge, protectedData, deviceInfo, binder, metrics);
if (macedKeysToSign == null || protectedData.protectedData == null
|| deviceInfo.deviceInfo == null) {
throw new RemoteProvisioningException(IGenerateRkpKeyService.Status.INTERNAL_ERROR,
@@ -88,7 +90,7 @@ public class Provisioner {
"Failed to serialize the payload generated by keystore.");
}
List<byte[]> certChains = ServerInterface.requestSignedCertificates(context,
- certificateRequest, challenge);
+ certificateRequest, challenge, metrics);
if (certChains == null) {
// This is marked as an internal error, because ServerInterface should never return
// null, and if it does it's indicative of a bug.
@@ -116,7 +118,7 @@ public class Provisioner {
}
try {
if (SystemInterface.provisionCertChain(rawPublicKey, cert.getEncoded(), certChain,
- expirationDate, secLevel, binder)) {
+ expirationDate, secLevel, binder, metrics)) {
provisioned++;
}
} catch (CertificateEncodingException e) {
diff --git a/src/com/android/remoteprovisioner/ProvisionerMetrics.java b/src/com/android/remoteprovisioner/ProvisionerMetrics.java
new file mode 100644
index 0000000..05991bf
--- /dev/null
+++ b/src/com/android/remoteprovisioner/ProvisionerMetrics.java
@@ -0,0 +1,439 @@
+/**
+ * Copyright (C) 2022 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.hardware.security.keymint.SecurityLevel;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import java.time.Duration;
+
+/**
+ * Contains the metrics values that are recorded for every attempt to remotely provision keys.
+ * This class will automatically push the atoms on close, and is intended to be used with a
+ * try-with-resources block to ensure metrics are automatically logged on completion of an attempt.
+ */
+public final class ProvisionerMetrics implements AutoCloseable {
+ // The state of remote provisioning enablement
+ public enum Enablement {
+ UNKNOWN,
+ ENABLED_WITH_FALLBACK,
+ ENABLED_RKP_ONLY,
+ DISABLED
+ }
+
+ public enum Status {
+ UNKNOWN,
+ KEYS_SUCCESSFULLY_PROVISIONED,
+ NO_PROVISIONING_NEEDED,
+ PROVISIONING_DISABLED,
+ INTERNAL_ERROR,
+ NO_NETWORK_CONNECTIVITY,
+ OUT_OF_ERROR_BUDGET,
+ INTERRUPTED,
+ GENERATE_KEYPAIR_FAILED,
+ GENERATE_CSR_FAILED,
+ GET_POOL_STATUS_FAILED,
+ INSERT_CHAIN_INTO_POOL_FAILED,
+ FETCH_GEEK_TIMED_OUT,
+ FETCH_GEEK_IO_EXCEPTION,
+ FETCH_GEEK_HTTP_ERROR,
+ SIGN_CERTS_TIMED_OUT,
+ SIGN_CERTS_IO_EXCEPTION,
+ SIGN_CERTS_HTTP_ERROR,
+ SIGN_CERTS_DEVICE_NOT_REGISTERED
+ }
+
+ /**
+ * Restartable stopwatch class that can be used to measure multiple start->stop time
+ * intervals. All measured time intervals are summed and returned by getElapsedMillis.
+ */
+ public static class StopWatch implements AutoCloseable {
+ private long mStartTime = 0;
+ private long mElapsedTime = 0;
+
+ /** Start or resume a timer. */
+ public void start() {
+ if (isRunning()) {
+ Log.w(TAG, "Starting a timer that's already been running for "
+ + getElapsedMillis() + "ms");
+ } else {
+ mStartTime = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /** Stop recording time. */
+ public void stop() {
+ if (!isRunning()) {
+ Log.w(TAG, "Attempting to stop a timer that hasn't been started.");
+ } else {
+ mElapsedTime += SystemClock.elapsedRealtime() - mStartTime;
+ mStartTime = 0;
+ }
+ }
+
+ /** Stops the timer if it's running. */
+ @Override
+ public void close() {
+ if (isRunning()) {
+ stop();
+ }
+ }
+
+ /** Get how long the timer has been recording. */
+ public int getElapsedMillis() {
+ if (isRunning()) {
+ return (int) (mElapsedTime + SystemClock.elapsedRealtime() - mStartTime);
+ } else {
+ return (int) mElapsedTime;
+ }
+ }
+
+ /** Is the timer currently recording time? */
+ public boolean isRunning() {
+ return mStartTime != 0;
+ }
+ }
+
+ private static final String TAG = Provisioner.TAG;
+
+ private final Context mContext;
+ private final int mCause;
+ private final StopWatch mServerWaitTimer = new StopWatch();
+ private final StopWatch mBinderWaitTimer = new StopWatch();
+ private final StopWatch mLockWaitTimer = new StopWatch();
+ private final StopWatch mTotalTimer = new StopWatch();
+ private final String mRemotelyProvisionedComponent;
+ private Enablement mEnablement;
+ private boolean mIsKeyPoolEmpty = false;
+ private Status mStatus = Status.UNKNOWN;
+ private int mHttpStatusError = 0;
+
+ private ProvisionerMetrics(Context context, int cause,
+ String remotelyProvisionedComponent, Enablement enablement) {
+ mContext = context;
+ mCause = cause;
+ mRemotelyProvisionedComponent = remotelyProvisionedComponent;
+ mEnablement = enablement;
+ mTotalTimer.start();
+ }
+
+ /** Start collecting metrics for scheduled provisioning. */
+ public static ProvisionerMetrics createScheduledAttemptMetrics(Context context) {
+ // Scheduled jobs (PeriodicProvisioner) intermix a lot of operations for multiple
+ // components, which makes it difficult to tease apart what is happening for which
+ // remotely provisioned component. Thus, on these calls, the component and
+ // component-specific enablement are not logged.
+ return new ProvisionerMetrics(
+ context,
+ RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_ATTEMPT__CAUSE__SCHEDULED,
+ "",
+ Enablement.UNKNOWN);
+ }
+
+ /** Start collecting metrics when an attestation key has been consumed from the pool. */
+ public static ProvisionerMetrics createKeyConsumedAttemptMetrics(Context context,
+ int securityLevel) {
+ return new ProvisionerMetrics(
+ context,
+ RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_ATTEMPT__CAUSE__KEY_CONSUMED,
+ getRemotelyProvisionedComponentName(securityLevel),
+ getEnablementForSecurityLevel(securityLevel));
+ }
+
+ /** Start collecting metrics when the spare attestation key pool is empty. */
+ public static ProvisionerMetrics createOutOfKeysAttemptMetrics(Context context,
+ int securityLevel) {
+ return new ProvisionerMetrics(
+ context,
+ RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_ATTEMPT__CAUSE__OUT_OF_KEYS,
+ getRemotelyProvisionedComponentName(securityLevel),
+ getEnablementForSecurityLevel(securityLevel));
+ }
+
+ /** Record the state of RKP configuration. */
+ public void setEnablement(Enablement enablement) {
+ mEnablement = enablement;
+ }
+
+ /** Set to true if the provisioning encountered an empty key pool. */
+ public void setIsKeyPoolEmpty(boolean isEmpty) {
+ mIsKeyPoolEmpty = isEmpty;
+ }
+
+ /** Set the status for this provisioning attempt. */
+ public void setStatus(Status status) {
+ mStatus = status;
+ }
+
+ /** Set the last HTTP status encountered. */
+ public void setHttpStatusError(int httpStatusError) {
+ mHttpStatusError = httpStatusError;
+ }
+
+ /**
+ * Starts the server wait timer, returning a reference to an object to be closed when the
+ * wait is over.
+ */
+ public StopWatch startServerWait() {
+ mServerWaitTimer.start();
+ return mServerWaitTimer;
+ }
+
+ /**
+ * Starts the binder wait timer, returning a reference to an object to be closed when the
+ * wait is over.
+ */
+ public StopWatch startBinderWait() {
+ mBinderWaitTimer.start();
+ return mBinderWaitTimer;
+ }
+
+ /**
+ * Starts the lock wait timer, returning a reference to an object to be closed when the
+ * wait is over.
+ */
+ public StopWatch startLockWait() {
+ mLockWaitTimer.start();
+ return mLockWaitTimer;
+ }
+
+ /** Record the atoms for this metrics object. */
+ @Override
+ public void close() {
+ mTotalTimer.stop();
+
+ int transportType = getTransportTypeForActiveNetwork();
+ RemoteProvisionerStatsLog.write(RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_ATTEMPT,
+ mCause, mRemotelyProvisionedComponent, getUpTimeBucket(), getIntEnablement(),
+ mIsKeyPoolEmpty, getIntStatus());
+ RemoteProvisionerStatsLog.write(
+ RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_NETWORK_INFO,
+ transportType, getIntStatus(), mHttpStatusError);
+ RemoteProvisionerStatsLog.write(RemoteProvisionerStatsLog.REMOTE_KEY_PROVISIONING_TIMING,
+ mServerWaitTimer.getElapsedMillis(), mBinderWaitTimer.getElapsedMillis(),
+ mLockWaitTimer.getElapsedMillis(),
+ mTotalTimer.getElapsedMillis(), transportType, mRemotelyProvisionedComponent);
+ }
+
+ // TODO: Fix this in U when the provisioner uses the remotely provisioned component names.
+ private static String getRemotelyProvisionedComponentName(int securityLevel) {
+ switch (securityLevel) {
+ case SecurityLevel.SOFTWARE:
+ return "SOFTWARE_KEYMINT";
+ case SecurityLevel.TRUSTED_ENVIRONMENT:
+ return "TEE_KEYMINT";
+ case SecurityLevel.STRONGBOX:
+ return "STRONGBOX";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private static Enablement getEnablementForSecurityLevel(int securityLevel) {
+ switch (securityLevel) {
+ case SecurityLevel.SOFTWARE:
+ return Enablement.ENABLED_WITH_FALLBACK;
+ case SecurityLevel.TRUSTED_ENVIRONMENT:
+ return readRkpOnlyProperty("remote_provisioning.tee.rkp_only");
+ case SecurityLevel.STRONGBOX:
+ return readRkpOnlyProperty("remote_provisioning.strongbox.rkp_only");
+ default:
+ return Enablement.UNKNOWN;
+ }
+ }
+
+ private static Enablement readRkpOnlyProperty(String property) {
+ if (SystemProperties.getBoolean(property, false)) {
+ return Enablement.ENABLED_RKP_ONLY;
+ }
+ return Enablement.ENABLED_WITH_FALLBACK;
+ }
+
+ private int getTransportTypeForActiveNetwork() {
+ ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ if (cm == null) {
+ Log.w(TAG, "Unable to get ConnectivityManager instance");
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_UNKNOWN;
+ }
+
+ NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
+ if (capabilities == null) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_UNKNOWN;
+ }
+
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
+ && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_WIFI_CELLULAR_VPN;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_CELLULAR_VPN;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_WIFI_VPN;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_BLUETOOTH_VPN;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_ETHERNET_VPN;
+ }
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_UNKNOWN;
+ }
+
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_CELLULAR;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_WIFI;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_BLUETOOTH;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_ETHERNET;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_WIFI_AWARE;
+ }
+ if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN)) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_LOWPAN;
+ }
+
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__TRANSPORT_TYPE__TT_UNKNOWN;
+ }
+
+ private int getUpTimeBucket() {
+ final long uptimeMillis = SystemClock.uptimeMillis();
+ if (uptimeMillis < Duration.ofMinutes(5).toMillis()) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__UPTIME__LESS_THAN_5_MINUTES;
+ } else if (uptimeMillis < Duration.ofMinutes(60).toMillis()) {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__UPTIME__BETWEEN_5_AND_60_MINUTES;
+ } else {
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__UPTIME__MORE_THAN_60_MINUTES;
+ }
+ }
+
+ private int getIntStatus() {
+ switch (mStatus) {
+ // A whole bunch of generated types here just don't fit in our line length limit.
+ // CHECKSTYLE:OFF Generated code
+ case UNKNOWN:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__REMOTE_KEY_PROVISIONING_STATUS_UNKNOWN;
+ case KEYS_SUCCESSFULLY_PROVISIONED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__KEYS_SUCCESSFULLY_PROVISIONED;
+ case NO_PROVISIONING_NEEDED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__NO_PROVISIONING_NEEDED;
+ case PROVISIONING_DISABLED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__PROVISIONING_DISABLED;
+ case INTERNAL_ERROR:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__INTERNAL_ERROR;
+ case NO_NETWORK_CONNECTIVITY:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__NO_NETWORK_CONNECTIVITY;
+ case OUT_OF_ERROR_BUDGET:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__OUT_OF_ERROR_BUDGET;
+ case INTERRUPTED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__INTERRUPTED;
+ case GENERATE_KEYPAIR_FAILED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__GENERATE_KEYPAIR_FAILED;
+ case GENERATE_CSR_FAILED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__GENERATE_CSR_FAILED;
+ case GET_POOL_STATUS_FAILED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__GET_POOL_STATUS_FAILED;
+ case INSERT_CHAIN_INTO_POOL_FAILED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__INSERT_CHAIN_INTO_POOL_FAILED;
+ case FETCH_GEEK_TIMED_OUT:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__FETCH_GEEK_TIMED_OUT;
+ case FETCH_GEEK_IO_EXCEPTION:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__FETCH_GEEK_IO_EXCEPTION;
+ case FETCH_GEEK_HTTP_ERROR:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__FETCH_GEEK_HTTP_ERROR;
+ case SIGN_CERTS_TIMED_OUT:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__SIGN_CERTS_TIMED_OUT;
+ case SIGN_CERTS_IO_EXCEPTION:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__SIGN_CERTS_IO_EXCEPTION;
+ case SIGN_CERTS_HTTP_ERROR:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__SIGN_CERTS_HTTP_ERROR;
+ case SIGN_CERTS_DEVICE_NOT_REGISTERED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__SIGN_CERTS_DEVICE_NOT_REGISTERED;
+ }
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_NETWORK_INFO__STATUS__REMOTE_KEY_PROVISIONING_STATUS_UNKNOWN;
+ // CHECKSTYLE:ON Generated code
+ }
+
+ private int getIntEnablement() {
+ switch (mEnablement) {
+ case UNKNOWN:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__ENABLEMENT__ENABLEMENT_UNKNOWN;
+ case ENABLED_WITH_FALLBACK:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__ENABLEMENT__ENABLED_WITH_FALLBACK;
+ case ENABLED_RKP_ONLY:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__ENABLEMENT__ENABLED_RKP_ONLY;
+ case DISABLED:
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__ENABLEMENT__DISABLED;
+ }
+ return RemoteProvisionerStatsLog
+ .REMOTE_KEY_PROVISIONING_ATTEMPT__ENABLEMENT__ENABLEMENT_UNKNOWN;
+ }
+}
diff --git a/src/com/android/remoteprovisioner/ServerInterface.java b/src/com/android/remoteprovisioner/ServerInterface.java
index 571b559..013a37f 100644
--- a/src/com/android/remoteprovisioner/ServerInterface.java
+++ b/src/com/android/remoteprovisioner/ServerInterface.java
@@ -58,11 +58,10 @@ public class ServerInterface {
* chain for one attestation key pair.
*/
public static List<byte[]> requestSignedCertificates(Context context, byte[] csr,
- byte[] challenge) throws
- RemoteProvisioningException {
- checkDataBudget(context);
+ byte[] challenge, ProvisionerMetrics metrics) throws RemoteProvisioningException {
+ checkDataBudget(context, metrics);
int bytesTransacted = 0;
- try {
+ try (ProvisionerMetrics.StopWatch serverWaitTimer = metrics.startServerWait()) {
URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL
+ Base64.encodeToString(challenge, Base64.URL_SAFE));
HttpURLConnection con = (HttpURLConnection) url.openConnection();
@@ -78,31 +77,45 @@ public class ServerInterface {
bytesTransacted += csr.length;
}
+ metrics.setHttpStatusError(con.getResponseCode());
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ serverWaitTimer.stop();
int failures = SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server connection for signing failed, response code: "
+ con.getResponseCode() + "\nRepeated failure count: " + failures);
SettingsManager.consumeErrDataBudget(context, bytesTransacted);
- throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
+ RemoteProvisioningException ex =
+ RemoteProvisioningException.createFromHttpError(con.getResponseCode());
+ if (ex.getErrorCode() == IGenerateRkpKeyService.Status.DEVICE_NOT_REGISTERED) {
+ metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_DEVICE_NOT_REGISTERED);
+ } else {
+ metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_HTTP_ERROR);
+ }
+ throw ex;
}
+ serverWaitTimer.stop();
SettingsManager.clearFailureCounter(context);
BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
+ serverWaitTimer.start();
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
cborBytes.write(buffer, 0, read);
bytesTransacted += read;
}
+ serverWaitTimer.stop();
return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
} catch (SocketTimeoutException e) {
Log.e(TAG, "Server timed out", e);
+ metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_TIMED_OUT);
} catch (IOException e) {
Log.e(TAG, "Failed to request signed certificates from the server", e);
+ metrics.setStatus(ProvisionerMetrics.Status.SIGN_CERTS_IO_EXCEPTION);
}
SettingsManager.incrementFailureCounter(context);
SettingsManager.consumeErrDataBudget(context, bytesTransacted);
- throw makeNetworkError(context, "Error getting CSR signed.");
+ throw makeNetworkError(context, "Error getting CSR signed.", metrics);
}
/**
@@ -115,45 +128,54 @@ public class ServerInterface {
* request to get keys signed.
*
* @param context The application context which is required to use SettingsManager.
+ * @param metrics
* @return A GeekResponse object which optionally contains configuration data.
*/
- public static GeekResponse fetchGeek(Context context) throws RemoteProvisioningException {
- checkDataBudget(context);
+ public static GeekResponse fetchGeek(Context context,
+ ProvisionerMetrics metrics) throws RemoteProvisioningException {
+ checkDataBudget(context, metrics);
int bytesTransacted = 0;
try {
URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL);
- HttpURLConnection con = (HttpURLConnection) url.openConnection();
- con.setRequestMethod("POST");
- con.setConnectTimeout(TIMEOUT_MS);
- con.setReadTimeout(TIMEOUT_MS);
- con.setDoOutput(true);
+ ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
+ try (ProvisionerMetrics.StopWatch serverWaitTimer = metrics.startServerWait()) {
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod("POST");
+ con.setConnectTimeout(TIMEOUT_MS);
+ con.setReadTimeout(TIMEOUT_MS);
+ con.setDoOutput(true);
- byte[] config = CborUtils.buildProvisioningInfo(context);
- try (OutputStream os = con.getOutputStream()) {
- os.write(config, 0, config.length);
- bytesTransacted += config.length;
- }
+ byte[] config = CborUtils.buildProvisioningInfo(context);
+ try (OutputStream os = con.getOutputStream()) {
+ os.write(config, 0, config.length);
+ bytesTransacted += 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() + "\nRepeated failure count: " + failures);
- SettingsManager.consumeErrDataBudget(context, bytesTransacted);
- throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
- }
- SettingsManager.clearFailureCounter(context);
-
- BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
- ByteArrayOutputStream cborBytes = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
- int read = 0;
- while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
- cborBytes.write(buffer, 0, read);
- bytesTransacted += read;
+ metrics.setHttpStatusError(con.getResponseCode());
+ if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ serverWaitTimer.stop();
+ int failures = SettingsManager.incrementFailureCounter(context);
+ Log.e(TAG, "Server connection for GEEK failed, response code: "
+ + con.getResponseCode() + "\nRepeated failure count: " + failures);
+ SettingsManager.consumeErrDataBudget(context, bytesTransacted);
+ metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_HTTP_ERROR);
+ throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
+ }
+ serverWaitTimer.stop();
+ SettingsManager.clearFailureCounter(context);
+ BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
+ byte[] buffer = new byte[1024];
+ int read = 0;
+ serverWaitTimer.start();
+ while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
+ cborBytes.write(buffer, 0, read);
+ bytesTransacted += read;
+ }
+ inputStream.close();
}
- inputStream.close();
GeekResponse resp = CborUtils.parseGeekResponse(cborBytes.toByteArray());
if (resp == null) {
+ metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_HTTP_ERROR);
throw new RemoteProvisioningException(
IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR,
"Response failed to parse.");
@@ -161,31 +183,37 @@ public class ServerInterface {
return resp;
} catch (SocketTimeoutException e) {
Log.e(TAG, "Server timed out", e);
+ metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_TIMED_OUT);
} catch (IOException e) {
// This exception will trigger on a completely malformed URL.
Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
+ metrics.setStatus(ProvisionerMetrics.Status.FETCH_GEEK_IO_EXCEPTION);
}
SettingsManager.incrementFailureCounter(context);
SettingsManager.consumeErrDataBudget(context, bytesTransacted);
- throw makeNetworkError(context, "Error fetching GEEK");
+ throw makeNetworkError(context, "Error fetching GEEK", metrics);
}
- private static void checkDataBudget(Context context) throws RemoteProvisioningException {
+ private static void checkDataBudget(Context context, ProvisionerMetrics metrics)
+ throws RemoteProvisioningException {
if (!SettingsManager.hasErrDataBudget(context, null /* curTime */)) {
+ metrics.setStatus(ProvisionerMetrics.Status.OUT_OF_ERROR_BUDGET);
int bytesConsumed = SettingsManager.getErrDataBudgetConsumed(context);
throw makeNetworkError(context,
"Out of data budget due to repeated errors. Consumed "
- + bytesConsumed + " bytes.");
+ + bytesConsumed + " bytes.", metrics);
}
}
- private static RemoteProvisioningException makeNetworkError(Context context, String message) {
+ private static RemoteProvisioningException makeNetworkError(Context context, String message,
+ ProvisionerMetrics metrics) {
ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
return new RemoteProvisioningException(
IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR, message);
}
+ metrics.setStatus(ProvisionerMetrics.Status.NO_NETWORK_CONNECTIVITY);
return new RemoteProvisioningException(
IGenerateRkpKeyService.Status.NO_NETWORK_CONNECTIVITY, message);
}
diff --git a/src/com/android/remoteprovisioner/SystemInterface.java b/src/com/android/remoteprovisioner/SystemInterface.java
index d04beec..4b07400 100644
--- a/src/com/android/remoteprovisioner/SystemInterface.java
+++ b/src/com/android/remoteprovisioner/SystemInterface.java
@@ -72,19 +72,25 @@ public class SystemInterface {
*/
public static byte[] generateCsr(boolean testMode, int numKeys, int secLevel,
byte[] geekChain, byte[] challenge, ProtectedData protectedData, DeviceInfo deviceInfo,
- @NonNull IRemoteProvisioning binder) {
+ @NonNull IRemoteProvisioning binder,
+ ProvisionerMetrics metrics) {
try {
Log.i(TAG, "Packaging " + numKeys + " keys into a CSR. Test: " + testMode);
ProtectedData dataBundle = new ProtectedData();
- byte[] macedPublicKeys = binder.generateCsr(testMode,
- numKeys,
- geekChain,
- challenge,
- secLevel,
- protectedData,
- deviceInfo);
+ byte[] macedPublicKeys = null;
+ try (ProvisionerMetrics.StopWatch ignored = metrics.startBinderWait()) {
+ macedPublicKeys = binder.generateCsr(testMode,
+ numKeys,
+ geekChain,
+ challenge,
+ secLevel,
+ protectedData,
+ deviceInfo);
+ }
+
if (macedPublicKeys == null) {
Log.e(TAG, "Keystore didn't generate a CSR successfully.");
+ metrics.setStatus(ProvisionerMetrics.Status.GENERATE_CSR_FAILED);
return null;
}
ByteArrayInputStream bais = new ByteArrayInputStream(macedPublicKeys);
@@ -103,35 +109,35 @@ public class SystemInterface {
return baos.toByteArray();
} catch (RemoteException e) {
Log.e(TAG, "Failed to generate CSR blob", e);
- return null;
} catch (ServiceSpecificException e) {
Log.e(TAG, "Failure in Keystore or Keymint to facilitate blob generation.", e);
- return null;
} catch (CborException e) {
Log.e(TAG, "Failed to parse/build CBOR", e);
- return null;
}
+ metrics.setStatus(ProvisionerMetrics.Status.GENERATE_CSR_FAILED);
+ return null;
}
/**
* Sends a provisionCertChain request down to the underlying remote provisioning binder service.
*/
public static boolean provisionCertChain(byte[] rawPublicKey, byte[] encodedCert,
- byte[] certChain,
- long expirationDate, int secLevel,
- IRemoteProvisioning binder) {
- try {
+ byte[] certChain,
+ long expirationDate, int secLevel,
+ IRemoteProvisioning binder,
+ ProvisionerMetrics metrics) {
+ try (ProvisionerMetrics.StopWatch ignored = metrics.startBinderWait()) {
binder.provisionCertChain(rawPublicKey, encodedCert, certChain,
expirationDate, secLevel);
return true;
} catch (RemoteException e) {
Log.e(TAG, "Error on the binder side when attempting to provision the signed chain",
e);
- return false;
} catch (ServiceSpecificException e) {
Log.e(TAG, "Error on the Keystore side", e);
- return false;
}
+ metrics.setStatus(ProvisionerMetrics.Status.INSERT_CHAIN_INTO_POOL_FAILED);
+ return false;
}
/**
@@ -140,12 +146,18 @@ public class SystemInterface {
*/
public static AttestationPoolStatus getPoolStatus(long expiringBy,
int secLevel,
- IRemoteProvisioning binder)
+ IRemoteProvisioning binder,
+ ProvisionerMetrics metrics)
throws RemoteException {
- try {
- return binder.getPoolStatus(expiringBy, secLevel);
+ try (ProvisionerMetrics.StopWatch ignored = metrics.startBinderWait()) {
+ AttestationPoolStatus status = binder.getPoolStatus(expiringBy, secLevel);
+ Log.i(TAG, "Pool status " + status.attested + ", " + status.unassigned + ", "
+ + status.expiring + ", " + status.total);
+ metrics.setIsKeyPoolEmpty(status.unassigned == 0);
+ return status;
} catch (ServiceSpecificException e) {
Log.e(TAG, "Failure in Keystore", e);
+ metrics.setStatus(ProvisionerMetrics.Status.GET_POOL_STATUS_FAILED);
throw new RemoteException(e);
}
}
@@ -156,12 +168,14 @@ public class SystemInterface {
* @param isTestMode Whether or not to generate a test key, which would accept any EEK later.
* @param secLevel Which security level to generate the key for.
*/
- public static void generateKeyPair(boolean isTestMode, int secLevel, IRemoteProvisioning binder)
+ public static void generateKeyPair(boolean isTestMode, int secLevel, IRemoteProvisioning binder,
+ ProvisionerMetrics metrics)
throws RemoteException {
- try {
+ try (ProvisionerMetrics.StopWatch ignored = metrics.startBinderWait()) {
binder.generateKeyPair(isTestMode, secLevel);
} catch (ServiceSpecificException e) {
Log.e(TAG, "Failure in Keystore or KeyMint.", e);
+ metrics.setStatus(ProvisionerMetrics.Status.GENERATE_KEYPAIR_FAILED);
throw new RemoteException(e);
}
}
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index f4475ca..4d21873 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -30,6 +30,8 @@ import android.util.Log;
import com.android.remoteprovisioner.GeekResponse;
import com.android.remoteprovisioner.PeriodicProvisioner;
+import com.android.remoteprovisioner.ProvisionerMetrics;
+import com.android.remoteprovisioner.ProvisionerMetrics.StopWatch;
import com.android.remoteprovisioner.RemoteProvisioningException;
import com.android.remoteprovisioner.ServerInterface;
import com.android.remoteprovisioner.SettingsManager;
@@ -66,40 +68,59 @@ public class GenerateRkpKeyService extends Service {
@Override
public int generateKey(int securityLevel) {
Log.i(TAG, "generateKey ping for secLevel: " + securityLevel);
- IRemoteProvisioning binder =
- IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
- return checkAndFillPool(binder, securityLevel, Concurrency.BLOCKING);
+ try (ProvisionerMetrics metrics =
+ ProvisionerMetrics.createOutOfKeysAttemptMetrics(
+ getApplicationContext(), securityLevel)) {
+ IRemoteProvisioning binder =
+ IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+ return checkAndFillPool(binder, securityLevel, metrics, Concurrency.BLOCKING);
+ }
}
@Override
public void notifyKeyGenerated(int securityLevel) {
Log.i(TAG, "Notify key generated ping for secLevel: " + securityLevel);
- IRemoteProvisioning binder =
- IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
- checkAndFillPool(binder, securityLevel, Concurrency.NON_BLOCKING);
+ try (ProvisionerMetrics metrics =
+ ProvisionerMetrics.createKeyConsumedAttemptMetrics(
+ getApplicationContext(), securityLevel)) {
+ IRemoteProvisioning binder =
+ IRemoteProvisioning.Stub.asInterface(ServiceManager.getService(SERVICE));
+ checkAndFillPool(binder, securityLevel, metrics, Concurrency.NON_BLOCKING);
+ }
}
private int checkAndFillPool(IRemoteProvisioning binder, int secLevel,
- Concurrency concurrency) {
+ ProvisionerMetrics metrics, Concurrency concurrency) {
// No need to hammer the pool check with a ton of redundant requests.
if (concurrency == Concurrency.BLOCKING) {
Log.i(TAG, "Waiting on lock to check pool status.");
- sLock.lock();
+ try (StopWatch ignored = metrics.startLockWait()) {
+ sLock.lock();
+ }
} else if (!sLock.tryLock()) {
Log.i(TAG, "Exiting check; another process already started the check.");
+ metrics.setStatus(ProvisionerMetrics.Status.UNKNOWN);
return Status.OK;
}
try {
- AttestationPoolStatus pool =
- binder.getPoolStatus(System.currentTimeMillis(), secLevel);
- ImplInfo[] implInfos = binder.getImplementationInfo();
- int curve = 0;
+ AttestationPoolStatus pool;
+ ImplInfo[] implInfos;
+ try (StopWatch ignored = metrics.startBinderWait()) {
+ pool = binder.getPoolStatus(System.currentTimeMillis(), secLevel);
+ implInfos = binder.getImplementationInfo();
+ }
+ int curve = -1;
for (int i = 0; i < implInfos.length; i++) {
if (implInfos[i].secLevel == secLevel) {
curve = implInfos[i].supportedCurve;
break;
}
}
+ // If curve has not been set, the security level does not have an associated
+ // RKP instance. This should only be possible for StrongBox on S.
+ if (curve == -1) {
+ metrics.setStatus(ProvisionerMetrics.Status.NO_PROVISIONING_NEEDED);
+ }
Context context = getApplicationContext();
int keysToProvision =
@@ -107,12 +128,16 @@ public class GenerateRkpKeyService extends Service {
binder,
context,
SettingsManager.getExpirationTime(context).toEpochMilli(),
- secLevel);
+ secLevel,
+ metrics);
if (keysToProvision != 0) {
Log.i(TAG, "All signed keys are currently in use, provisioning more.");
- GeekResponse resp = ServerInterface.fetchGeek(context);
+ GeekResponse resp = ServerInterface.fetchGeek(context, metrics);
PeriodicProvisioner.batchProvision(binder, context, keysToProvision, secLevel,
- resp.getGeekChain(curve), resp.getChallenge());
+ resp.getGeekChain(curve), resp.getChallenge(), metrics);
+ metrics.setStatus(ProvisionerMetrics.Status.KEYS_SUCCESSFULLY_PROVISIONED);
+ } else {
+ metrics.setStatus(ProvisionerMetrics.Status.NO_PROVISIONING_NEEDED);
}
} catch (InterruptedException e) {
Log.e(TAG, "Provisioner thread interrupted.", e);
diff --git a/tests/hosttest/Android.bp b/tests/hosttest/Android.bp
new file mode 100644
index 0000000..de0ab88
--- /dev/null
+++ b/tests/hosttest/Android.bp
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_test_host {
+ name: "RemoteProvisionerHostTests",
+ srcs: ["src/**/*.java"],
+ test_suites: ["general-tests"],
+ libs: [
+ "compatibility-host-util",
+ "core_cts_test_resources",
+ "cts-tradefed",
+ "host-libprotobuf-java-full",
+ "platformprotos",
+ "tradefed",
+ "truth-prebuilt",
+ ],
+
+ static_libs: [
+ "cts-statsd-atom-host-test-utils",
+ ],
+
+ data: [
+ ":RemoteProvisionerUnitTests",
+ ]
+}
+
diff --git a/tests/hosttest/AndroidTest.xml b/tests/hosttest/AndroidTest.xml
new file mode 100644
index 0000000..2365fd0
--- /dev/null
+++ b/tests/hosttest/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<configuration Description="Config for RemoteProvisioner host test cases">
+ <option name="config-descriptor:metadata" key="component" value="RemoteProvisioner" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RemoteProvisionerUnitTests.apk" />
+ </target_preparer>
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="RemoteProvisionerHostTests.jar" />
+ </test>
+</configuration>
diff --git a/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerStatsTests.java b/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerStatsTests.java
new file mode 100644
index 0000000..4beaf27
--- /dev/null
+++ b/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerStatsTests.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2022 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.hosttest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.cts.statsdatom.lib.ConfigUtils;
+import android.cts.statsdatom.lib.ReportUtils;
+import android.stats.connectivity.TransportType;
+
+import com.android.os.AtomsProto;
+import com.android.os.AtomsProto.RemoteKeyProvisioningAttempt;
+import com.android.os.AtomsProto.RemoteKeyProvisioningAttempt.Cause;
+import com.android.os.AtomsProto.RemoteKeyProvisioningAttempt.Enablement;
+import com.android.os.AtomsProto.RemoteKeyProvisioningAttempt.UpTime;
+import com.android.os.AtomsProto.RemoteKeyProvisioningNetworkInfo;
+import com.android.os.AtomsProto.RemoteKeyProvisioningTiming;
+import com.android.os.StatsLog.EventMetricData;
+import com.android.remoteprovisioner.RemoteprovisionerEnums.RemoteKeyProvisioningStatus;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class RemoteProvisionerStatsTests extends BaseHostJUnit4Test {
+ private static final String APP_PACKAGE_NAME = "com.android.remoteprovisioner";
+ private static final String TEST_PACKAGE_NAME = "com.android.remoteprovisioner.unittest";
+ private static final int NO_HTTP_STATUS_ERROR = 0;
+
+ private static final List<TransportType> VALID_TRANSPORT_TYPES = Arrays.asList(
+ TransportType.TT_CELLULAR, TransportType.TT_WIFI, TransportType.TT_BLUETOOTH,
+ TransportType.TT_ETHERNET, TransportType.TT_WIFI_AWARE, TransportType.TT_LOWPAN,
+ TransportType.TT_CELLULAR_VPN, TransportType.TT_WIFI_VPN,
+ TransportType.TT_BLUETOOTH_VPN, TransportType.TT_ETHERNET_VPN,
+ TransportType.TT_WIFI_CELLULAR_VPN
+ );
+
+ private static final List<Enablement> VALID_ENABLEMENTS = Arrays.asList(
+ Enablement.ENABLED_RKP_ONLY, Enablement.ENABLED_WITH_FALLBACK);
+
+ @Before
+ public void setUp() throws Exception {
+ ConfigUtils.removeConfig(getDevice());
+ ReportUtils.clearReports(getDevice());
+
+ ConfigUtils.uploadConfigForPushedAtoms(getDevice(),
+ APP_PACKAGE_NAME,
+ new int[]{
+ AtomsProto.Atom.REMOTE_KEY_PROVISIONING_ATTEMPT_FIELD_NUMBER,
+ AtomsProto.Atom.REMOTE_KEY_PROVISIONING_NETWORK_INFO_FIELD_NUMBER,
+ AtomsProto.Atom.REMOTE_KEY_PROVISIONING_TIMING_FIELD_NUMBER});
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ ConfigUtils.removeConfig(getDevice());
+ ReportUtils.clearReports(getDevice());
+ }
+
+ @Test
+ public void testGenerateKeyRkpOnly() throws Exception {
+ runTest("ServerToSystemTest", "testGenerateKeyRkpOnly");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(6);
+
+ // First three metrics are for when we actually generated keys
+ final List<EventMetricData> firstAttemptData = data.subList(0, 3);
+ RemoteKeyProvisioningAttempt attempt = getAttemptMetric(firstAttemptData);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.OUT_OF_KEYS);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.KEYS_SUCCESSFULLY_PROVISIONED);
+
+ RemoteKeyProvisioningTiming timing = getTimingMetric(firstAttemptData);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ // We're going over the internet here, so it realistically must take at least 1ms
+ assertThat(timing.getServerWaitMillis()).isAtLeast(1);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(firstAttemptData);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(200);
+
+ // Second three metrics are for the call to keyGenerated, which should have been a noop
+ final List<EventMetricData> secondAttemptData = data.subList(3, 6);
+ attempt = getAttemptMetric(secondAttemptData);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.KEY_CONSUMED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
+ assertThat(attempt.getIsKeyPoolEmpty()).isFalse();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.NO_PROVISIONING_NEEDED);
+
+ timing = getTimingMetric(secondAttemptData);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isEqualTo(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ network = getNetworkMetric(secondAttemptData);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(0);
+ }
+
+ @Test
+ public void testDataBudgetEmptyCallGenerateRkpKeyService() throws Exception {
+ runTest("ServerToSystemTest", "testDataBudgetEmptyCallGenerateRkpKeyService");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.KEY_CONSUMED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isIn(VALID_ENABLEMENTS);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(RemoteKeyProvisioningStatus.OUT_OF_ERROR_BUDGET);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isEqualTo(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ }
+
+ @Test
+ public void testRetryableRkpError() throws Exception {
+ runTest("ServerToSystemTest", "testRetryableRkpError");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.OUT_OF_KEYS);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.FETCH_GEEK_IO_EXCEPTION);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ }
+
+ @Test
+ public void testRetryNeverWhenDeviceNotRegistered() throws Exception {
+ runTest("ServerToSystemTest", "testRetryNeverWhenDeviceNotRegistered");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.OUT_OF_KEYS);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.SIGN_CERTS_DEVICE_NOT_REGISTERED);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(444);
+ }
+
+ @Test
+ public void testRetryWithoutNetworkTee() throws Exception {
+ runTest("ServerToSystemTest", "testRetryWithoutNetworkTee");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.OUT_OF_KEYS);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEqualTo("TEE_KEYMINT");
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isIn(VALID_ENABLEMENTS);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.NO_NETWORK_CONNECTIVITY);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ }
+
+ @Test
+ public void testPeriodicProvisionerRoundTrip() throws Exception {
+ runTest("ServerToSystemTest", "testPeriodicProvisionerRoundTrip");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.SCHEDULED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEmpty();
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ // PeriodicProvisioner provisions ALL remotely provisioned components, and each one
+ // has its own enablement flag, so it reports UNKNOWN or DISABLED only.
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLEMENT_UNKNOWN);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.KEYS_SUCCESSFULLY_PROVISIONED);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(200);
+ }
+
+ @Test
+ public void testPeriodicProvisionerNoop() throws Exception {
+ // First pass of the test will provision some keys
+ runTest("ServerToSystemTest", "testPeriodicProvisionerNoop");
+
+ List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(6);
+
+ // drop the first three metrics, because those are for the first round trip and we've
+ // already tested those metrics elsewhere.
+ data = data.subList(3, 6);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.SCHEDULED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEmpty();
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ // PeriodicProvisioner provisions ALL remotely provisioned components, and each one
+ // has its own enablement flag, so it reports UNKNOWN or DISABLED only.
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLEMENT_UNKNOWN);
+ assertThat(attempt.getIsKeyPoolEmpty()).isFalse();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.NO_PROVISIONING_NEEDED);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ }
+
+ @Test
+ public void testPeriodicProvisionerDataBudgetEmpty() throws Exception {
+ runTest("ServerToSystemTest", "testPeriodicProvisionerDataBudgetEmpty");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.SCHEDULED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEmpty();
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLEMENT_UNKNOWN);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.OUT_OF_ERROR_BUDGET);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ }
+
+ @Test
+ public void testPeriodicProvisionerProvisioningDisabled() throws Exception {
+ runTest("ServerToSystemTest", "testPeriodicProvisionerProvisioningDisabled");
+ final List<EventMetricData> data = ReportUtils.getEventMetricDataList(getDevice());
+ assertThat(data).hasSize(3);
+
+ final RemoteKeyProvisioningAttempt attempt = getAttemptMetric(data);
+ assertThat(attempt).isNotNull();
+ assertThat(attempt.getCause()).isEqualTo(Cause.SCHEDULED);
+ assertThat(attempt.getRemotelyProvisionedComponent()).isEmpty();
+ assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
+ assertThat(attempt.getEnablement()).isEqualTo(Enablement.DISABLED);
+ assertThat(attempt.getIsKeyPoolEmpty()).isTrue();
+ assertThat(attempt.getStatus()).isEqualTo(
+ RemoteKeyProvisioningStatus.PROVISIONING_DISABLED);
+
+ final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
+ assertThat(timing).isNotNull();
+ assertThat(timing.getTransportType()).isNotEqualTo(VALID_TRANSPORT_TYPES);
+ assertThat(timing.getRemotelyProvisionedComponent()).isEqualTo(
+ attempt.getRemotelyProvisionedComponent());
+ assertThat(timing.getServerWaitMillis()).isAtLeast(0);
+ assertThat(timing.getBinderWaitMillis()).isAtLeast(0);
+ assertThat(timing.getLockWaitMillis()).isAtLeast(0);
+ assertThat(timing.getTotalProcessingTime()).isAtLeast(
+ timing.getServerWaitMillis() + timing.getBinderWaitMillis()
+ + timing.getLockWaitMillis());
+
+ final RemoteKeyProvisioningNetworkInfo network = getNetworkMetric(data);
+ assertThat(network).isNotNull();
+ assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
+ assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
+ assertThat(network.getHttpStatusError()).isEqualTo(200);
+ }
+
+ private void runTest(String testClassName, String testMethodName) throws Exception {
+ testClassName = TEST_PACKAGE_NAME + "." + testClassName;
+ assertThat(runDeviceTests(TEST_PACKAGE_NAME, testClassName, testMethodName)).isTrue();
+ }
+
+ private static RemoteKeyProvisioningAttempt getAttemptMetric(List<EventMetricData> data) {
+ RemoteKeyProvisioningAttempt metric = null;
+ for (EventMetricData event : data) {
+ if (event.getAtom().hasRemoteKeyProvisioningAttempt()) {
+ assertThat(metric).isNull();
+ metric = event.getAtom().getRemoteKeyProvisioningAttempt();
+ }
+ }
+ return metric;
+ }
+
+ private static RemoteKeyProvisioningTiming getTimingMetric(List<EventMetricData> data) {
+ RemoteKeyProvisioningTiming metric = null;
+ for (EventMetricData event : data) {
+ if (event.getAtom().hasRemoteKeyProvisioningTiming()) {
+ assertThat(metric).isNull();
+ metric = event.getAtom().getRemoteKeyProvisioningTiming();
+ }
+ }
+ return metric;
+ }
+
+ private static RemoteKeyProvisioningNetworkInfo getNetworkMetric(List<EventMetricData> data) {
+ RemoteKeyProvisioningNetworkInfo metric = null;
+ for (EventMetricData event : data) {
+ if (event.getAtom().hasRemoteKeyProvisioningNetworkInfo()) {
+ assertThat(metric).isNull();
+ metric = event.getAtom().getRemoteKeyProvisioningNetworkInfo();
+ }
+ }
+ return metric;
+ }
+}
diff --git a/tests/unittests/Android.bp b/tests/unittests/Android.bp
index c6a238d..5c81b4b 100644
--- a/tests/unittests/Android.bp
+++ b/tests/unittests/Android.bp
@@ -22,6 +22,8 @@ android_test {
"Nene",
"androidx.test.core",
"androidx.test.rules",
+ "androidx.work_work-runtime",
+ "androidx.work_work-testing",
"android.security.remoteprovisioning-java",
"libnanohttpd",
"platform-test-annotations",
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
index 975370c..1d077ec 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -19,6 +19,7 @@ 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 android.security.keystore.KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -28,11 +29,13 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.Manifest;
+import android.app.ActivityThread;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.ServiceManager;
import android.os.SystemProperties;
+import android.security.GenerateRkpKey;
import android.security.KeyStoreException;
import android.security.NetworkSecurityPolicy;
import android.security.keystore.KeyGenParameterSpec;
@@ -45,11 +48,15 @@ import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
+import androidx.work.ListenableWorker;
+import androidx.work.testing.TestWorkerBuilder;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.permissions.PermissionContext;
import com.android.remoteprovisioner.GeekResponse;
+import com.android.remoteprovisioner.PeriodicProvisioner;
import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.ProvisionerMetrics;
import com.android.remoteprovisioner.RemoteProvisioningException;
import com.android.remoteprovisioner.ServerInterface;
import com.android.remoteprovisioner.SettingsManager;
@@ -70,6 +77,7 @@ import java.security.ProviderException;
import java.security.cert.Certificate;
import java.time.Duration;
import java.util.Arrays;
+import java.util.concurrent.Executors;
import fi.iki.elonen.NanoHTTPD;
@@ -101,6 +109,27 @@ public class ServerToSystemTest {
+ "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMU",
Base64.DEFAULT);
+ // Same as GEEK_RESPONSE, but the "num_extra_attestation_keys" value is 0, disabling RKP.
+ private static final byte[] GEEK_RESPONSE_RKP_DISABLED = Base64.decode(
+ "g4KCAYOEQ6EBJqBYTaUBAgMmIAEhWCD3FIrbl/TMU+/SZBHE43UfZh+kcQxsz/oJRoB0h1TyrSJY"
+ + "IF5/W/bs5PYZzP8TN/0PociT2xgGdsRd5tdqd4bDLa+PWEAvl45C+74HLZVHhUeTQLAf1JtHpMRE"
+ + "qfKhB4cQx5/LEfS/n+g74Oc0TBX8e8N+MwX00TQ87QIEYHoV4HnTiv8khEOhASagWE2lAQIDJiAB"
+ + "IVggUYCsz4+WjOwPUOGpG7eQhjSL48OsZQJNtPYxDghGMjkiWCBU65Sd/ra05HM6JU4vH52dvfpm"
+ + "wRGL6ZaMQ+Qw9tp2q1hAmDj7NDpl23OYsSeiFXTyvgbnjSJO3fC/wgF0xLcpayQctdjSZvpE7/Uw"
+ + "LAR07ejGYNrOn1ZXJ3Qh096Tj+O4zYRDoQEmoFhxpgECAlggg5/4/RAcEp+SQcdbjeRO9BkTmscb"
+ + "bacOlfJkU12nHcEDOBggASFYIBakUhJjs4ZWUNjf8qCofbzZbqdoYOqMXPGT5ZcZDazeIlggib7M"
+ + "bD9esDk0r5e6ONEWHaHMHWTTjEhO+HKBGzs+Me5YQPrazy2rpTAMc8Xlq0mSWWBE+sTyM+UEsmwZ"
+ + "ZOkc42Q7NIYAZS313a+qAcmvg8lO+FqU6GWTUeMYHjmAp2lLM82CAoOEQ6EBJ6BYKqQBAQMnIAYh"
+ + "WCCZue7dXuRS9oXGTGLcPmGrV0h9dTcprXaAMtKzy2NY2VhAHiIIS6S3pMjXTgMO/rivFEynO2+l"
+ + "zdzaecYrZP6ZOa9254D6ZgCFDQeYKqyRXKclFEkGNHXKiid62eNaSesCA4RDoQEnoFgqpAEBAycg"
+ + "BiFYIOovhQ6eagxc973Z+igyv9pV6SCiUQPJA5MYzqAVKezRWECCa8ddpjZXt8dxEq0cwmqzLCMq"
+ + "3RQwy4IUtonF0x4xu7hQIUpJTbqRDG8zTYO8WCsuhNvFWQ+YYeLB6ony0K4EhEOhASegWE6lAQEC"
+ + "WCBvktEEbXHYp46I2NFWgV+W0XiD5jAbh+2/INFKO/5qLgM4GCAEIVggtl0cS5qDOp21FVk3oSb7"
+ + "D9/nnKwB1aTsyDopAIhYJTlYQICyn9Aynp1K/rAl8sLSImhGxiCwqugWrGShRYObzElUJX+rFgVT"
+ + "8L01k/PGu1lOXvneIQcUo7ako4uPgpaWugNYHQAAAYBINcxrASC0rWP9VTSO7LdABvcdkv7W2vh+"
+ + "onV0aW1lX3RvX3JlZnJlc2hfaG91cnMYSHgabnVtX2V4dHJhX2F0dGVzdGF0aW9uX2tleXMA",
+ Base64.DEFAULT);
+
private static Context sContext;
private static IRemoteProvisioning sBinder;
private static int sCurve = 0;
@@ -184,17 +213,18 @@ public class ServerToSystemTest {
@Test
public void testFullRoundTrip() throws Exception {
+ ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext);
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);
+ GeekResponse geek = ServerInterface.fetchGeek(sContext, metrics);
assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
assertNotNull(geek);
int numProvisioned =
Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
- sContext);
+ sContext, metrics);
assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
assertEquals(numTestKeys, numProvisioned);
assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
@@ -205,7 +235,104 @@ public class ServerToSystemTest {
}
@Test
+ public void testPeriodicProvisionerRoundTrip() throws Exception {
+ PeriodicProvisioner provisioner = TestWorkerBuilder.from(
+ sContext,
+ PeriodicProvisioner.class,
+ Executors.newSingleThreadExecutor()).build();
+ assertEquals(provisioner.doWork(), ListenableWorker.Result.success());
+ AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(),
+ TRUSTED_ENVIRONMENT);
+ assertTrue("Pool must not be empty", pool.total > 0);
+ assertEquals("All keys must be attested", pool.total, pool.attested);
+ assertEquals("Nobody should have consumed keys yet", pool.total, pool.unassigned);
+ assertEquals("All keys should be freshly generated", 0, pool.expiring);
+ }
+
+ @Test
+ public void testPeriodicProvisionerNoop() throws Exception {
+ // Similar to the PeriodicProvisioner round trip, except first we actually populate the
+ // key pool to ensure that the PeriodicProvisioner just noops.
+ PeriodicProvisioner provisioner = TestWorkerBuilder.from(
+ sContext,
+ PeriodicProvisioner.class,
+ Executors.newSingleThreadExecutor()).build();
+ assertEquals(provisioner.doWork(), ListenableWorker.Result.success());
+ final AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(),
+ TRUSTED_ENVIRONMENT);
+ assertTrue("Pool must not be empty", pool.total > 0);
+ assertEquals("All keys must be attested", pool.total, pool.attested);
+ assertEquals("Nobody should have consumed keys yet", pool.total, pool.unassigned);
+ assertEquals("All keys should be freshly generated", 0, pool.expiring);
+
+ // The metrics host test will perform additional validation by ensuring correct metrics
+ // are recorded.
+ assertEquals(provisioner.doWork(), ListenableWorker.Result.success());
+ assertPoolStatus(pool.total, pool.attested, pool.unassigned, pool.expiring, mDuration);
+ }
+
+ @Test
+ public void testPeriodicProvisionerDataBudgetEmpty() throws Exception {
+ // Check the data budget in order to initialize a rolling window.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+ SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+
+ PeriodicProvisioner provisioner = TestWorkerBuilder.from(
+ sContext,
+ PeriodicProvisioner.class,
+ Executors.newSingleThreadExecutor()).build();
+ assertEquals(provisioner.doWork(), ListenableWorker.Result.failure());
+ AttestationPoolStatus pool = sBinder.getPoolStatus(mDuration.toMillis(),
+ TRUSTED_ENVIRONMENT);
+ assertTrue("Keys should have been generated", pool.total > 0);
+ assertEquals("No keys should be attested", 0, pool.attested);
+ assertEquals("No keys should have been assigned", 0, pool.unassigned);
+ assertEquals("No keys can possibly be expiring yet", 0, pool.expiring);
+ }
+
+ @Test
+ public void testPeriodicProvisionerProvisioningDisabled() throws Exception {
+ // We need to run an HTTP server that returns a config indicating no keys are needed
+ final NanoHTTPD server = new NanoHTTPD("localhost", 0) {
+ @Override
+ public Response serve(IHTTPSession session) {
+ consumeRequestBody((HTTPSession) session);
+ if (session.getUri().contains(":fetchEekChain")) {
+ return newFixedLengthResponse(Response.Status.OK, "application/cbor",
+ new ByteArrayInputStream(GEEK_RESPONSE_RKP_DISABLED),
+ GEEK_RESPONSE_RKP_DISABLED.length);
+ }
+ Assert.fail("Unexpected HTTP request: " + session.getUri());
+ return null;
+ }
+
+ void consumeRequestBody(HTTPSession session) {
+ try {
+ session.getInputStream().readNBytes((int) session.getBodySize());
+ } catch (IOException e) {
+ Assert.fail("Error reading request bytes: " + e.toString());
+ }
+ }
+ };
+ server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
+
+ final boolean cleartextPolicy =
+ NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted();
+ NetworkSecurityPolicy.getInstance().setCleartextTrafficPermitted(true);
+ SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */,
+ "http://localhost:" + server.getListeningPort() + "/");
+
+ PeriodicProvisioner provisioner = TestWorkerBuilder.from(
+ sContext,
+ PeriodicProvisioner.class,
+ Executors.newSingleThreadExecutor()).build();
+ assertEquals(provisioner.doWork(), ListenableWorker.Result.success());
+ assertPoolStatus(0, 0, 0, 0, mDuration);
+ }
+
+ @Test
public void testFallback() throws Exception {
+ ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext);
Assume.assumeFalse(
"Skipping test as this system does not support fallback from RKP keys",
SystemProperties.getBoolean(RKP_ONLY_PROP, false));
@@ -221,11 +348,11 @@ public class ServerToSystemTest {
Certificate[] fallbackKeyCerts1 = generateKeyStoreKey("test1");
SettingsManager.clearPreferences(sContext);
- GeekResponse geek = ServerInterface.fetchGeek(sContext);
+ GeekResponse geek = ServerInterface.fetchGeek(sContext, metrics);
int numProvisioned =
Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
- sContext);
+ sContext, metrics);
assertEquals(numTestKeys, numProvisioned);
assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
Certificate[] provisionedKeyCerts = generateKeyStoreKey("test2");
@@ -253,8 +380,9 @@ public class ServerToSystemTest {
// Check the data budget in order to initialize a rolling window.
assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+ ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext);
try {
- ServerInterface.fetchGeek(sContext);
+ ServerInterface.fetchGeek(sContext, metrics);
fail("Network transaction should not have proceeded.");
} catch (RemoteProvisioningException e) {
return;
@@ -266,8 +394,9 @@ public class ServerToSystemTest {
// Check the data budget in order to initialize a rolling window.
assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+ ProvisionerMetrics metrics = ProvisionerMetrics.createScheduledAttemptMetrics(sContext);
try {
- ServerInterface.requestSignedCertificates(sContext, null, null);
+ ServerInterface.requestSignedCertificates(sContext, null, null, metrics);
fail("Network transaction should not have proceeded.");
} catch (RemoteProvisioningException e) {
return;
@@ -275,6 +404,25 @@ public class ServerToSystemTest {
}
@Test
+ public void testDataBudgetEmptyCallGenerateRkpKeyService() throws Exception {
+ // Check the data budget in order to initialize a rolling window.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+ SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+ GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread.currentApplication());
+ keyGen.notifyKeyGenerated(SECURITY_LEVEL_TRUSTED_ENVIRONMENT);
+ // Nothing to check here. This test is primarily used by the Metrics host test to
+ // validate that correct metrics are logged.
+ }
+
+ @Test
+ public void testGenerateKeyRkpOnly() throws Exception {
+ try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) {
+ Certificate[] certs = generateKeyStoreKey("this-better-work");
+ assertTrue(certs.length > 0);
+ }
+ }
+
+ @Test
public void testRetryableRkpError() throws Exception {
try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) {
SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */,
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
index cb664c8..9203803 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
@@ -36,26 +36,16 @@ import android.platform.test.annotations.Presubmit;
import android.security.keystore.KeyGenParameterSpec;
import android.security.remoteprovisioning.IRemoteProvisioning;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
-import com.android.remoteprovisioner.CborUtils;
+import com.android.remoteprovisioner.ProvisionerMetrics;
import com.android.remoteprovisioner.SystemInterface;
import com.android.remoteprovisioner.X509Utils;
import com.google.crypto.tink.subtle.Hkdf;
import com.google.crypto.tink.subtle.X25519;
-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.NegativeInteger;
-import co.nstant.in.cbor.model.UnsignedInteger;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -79,6 +69,17 @@ import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+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.NegativeInteger;
+import co.nstant.in.cbor.model.UnsignedInteger;
+
@RunWith(AndroidJUnit4.class)
public class SystemInterfaceTest {
@@ -117,6 +118,8 @@ public class SystemInterfaceTest {
@Presubmit
@Test
public void testGenerateCSR() throws Exception {
+ ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
+ ApplicationProvider.getApplicationContext(), SecurityLevel.TRUSTED_ENVIRONMENT);
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
byte[] eek = new byte[32];
@@ -125,7 +128,8 @@ public class SystemInterfaceTest {
SystemInterface.generateCsr(true /* testMode */, 0 /* numKeys */,
SecurityLevel.TRUSTED_ENVIRONMENT,
generateEekChain(eek),
- new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder,
+ metrics);
// encryptedBundle should contain a COSE_Encrypt message
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
@@ -153,6 +157,8 @@ public class SystemInterfaceTest {
@Presubmit
@Test
public void testGenerateCSRProvisionAndUseKey() throws Exception {
+ ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
+ ApplicationProvider.getApplicationContext(), SecurityLevel.TRUSTED_ENVIRONMENT);
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
int numKeys = 10;
@@ -165,7 +171,8 @@ public class SystemInterfaceTest {
SystemInterface.generateCsr(true /* testMode */, numKeys,
SecurityLevel.TRUSTED_ENVIRONMENT,
generateEekChain(eek),
- new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder,
+ metrics);
assertNotNull(bundle);
// The return value of generateCsr should be a COSE_Mac0 message
ByteArrayInputStream bais = new ByteArrayInputStream(bundle);
@@ -205,7 +212,7 @@ public class SystemInterfaceTest {
os.toByteArray() /* certChain */,
expiringBy.toEpochMilli() /* validity */,
SecurityLevel.TRUSTED_ENVIRONMENT,
- mBinder);
+ mBinder, metrics);
}
// getPoolStatus will clean the key pool before we go to assign a new provisioned key
mBinder.getPoolStatus(0, SecurityLevel.TRUSTED_ENVIRONMENT);
@@ -280,6 +287,8 @@ public class SystemInterfaceTest {
public void testDecryptProtectedPayload() throws Exception {
DeviceInfo deviceInfo = new DeviceInfo();
ProtectedData encryptedBundle = new ProtectedData();
+ ProvisionerMetrics metrics = ProvisionerMetrics.createOutOfKeysAttemptMetrics(
+ ApplicationProvider.getApplicationContext(), SecurityLevel.TRUSTED_ENVIRONMENT);
int numKeys = 1;
byte[] eekPriv = X25519.generatePrivateKey();
byte[] eekPub = X25519.publicFromPrivate(eekPriv);
@@ -288,7 +297,8 @@ public class SystemInterfaceTest {
SystemInterface.generateCsr(true /* testMode */, numKeys,
SecurityLevel.TRUSTED_ENVIRONMENT,
generateEekChain(eekPub),
- new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder);
+ new byte[] {0x02}, encryptedBundle, deviceInfo, mBinder,
+ metrics);
ByteArrayInputStream bais = new ByteArrayInputStream(encryptedBundle.protectedData);
List<DataItem> dataItems = new CborDecoder(bais).decode();
// Parse encMsg into components: protected and unprotected headers, payload, and recipient