diff options
author | Seth Moore <sethmo@google.com> | 2022-05-12 14:51:58 -0700 |
---|---|---|
committer | Seth Moore <sethmo@google.com> | 2022-05-15 13:05:59 -0700 |
commit | 77f611f12aea5309d008929b1094682453639024 (patch) | |
tree | 39b70ee13a158c8fc786264a22badc2a665859e5 | |
parent | 6ac2f49905421c32aeca7a29173f9550aff8e059 (diff) | |
download | RemoteProvisioner-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
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 |