Snap for 11273583 from 32c7e888ba8ff0dcf3dcb5ca421c7eece08ff569 to mainline-ipsec-release
Change-Id: I0d01f139a11673e7d1dd4698a303eb23f80f91e9
diff --git a/OWNERS b/OWNERS
index 01955db..2cbf527 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 1084908
sethmo@google.com
vikramgaur@google.com
diff --git a/apex/Android.bp b/apex/Android.bp
index aaa8cd8..f465cca 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -37,7 +37,7 @@
"com.android.rkpd-systemserverclasspath-fragment",
],
updatable: true,
- min_sdk_version: "UpsideDownCake",
+ min_sdk_version: "33",
apps: ["rkpdapp"],
compile_multilib: "both",
}
diff --git a/apex/manifest.json b/apex/manifest.json
index f8decae..54e9e34 100644
--- a/apex/manifest.json
+++ b/apex/manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.rkpd",
- "version": 1
+ "version": 0
}
diff --git a/app/Android.bp b/app/Android.bp
index 77b978e..2f7aef9 100644
--- a/app/Android.bp
+++ b/app/Android.bp
@@ -59,7 +59,8 @@
android_app {
name: "rkpdapp",
sdk_version: "module_current",
- min_sdk_version: "UpsideDownCake",
+ target_sdk_version: "34",
+ min_sdk_version: "33",
updatable: false,
privileged: true,
libs: [
diff --git a/app/TEST_MAPPING b/app/TEST_MAPPING
index cb4d40b..824fc9e 100644
--- a/app/TEST_MAPPING
+++ b/app/TEST_MAPPING
@@ -4,13 +4,15 @@
"name": "RkpdAppUnitTests"
},
{
- "name": "RkpdAppGoogleUnitTests"
+ "name": "RkpdAppGoogleUnitTests",
+ "keywords": ["internal"]
},
{
"name": "RkpdAppIntegrationTests"
},
{
- "name": "RkpdAppGoogleIntegrationTests"
+ "name": "RkpdAppGoogleIntegrationTests",
+ "keywords": ["internal"]
}
],
"mainline-presubmit": [
diff --git a/app/proguard.flags b/app/proguard.flags
index 6339e99..2e7d09b 100644
--- a/app/proguard.flags
+++ b/app/proguard.flags
@@ -2,3 +2,14 @@
-keep class com.android.rkpdapp.interfaces.ServiceManagerInterface { *; }
-keep class com.android.rkpdapp.service.** { *; }
-keep class com.android.rkpdapp.utils.Settings { *; }
+
+# Required for tests that use Mockito's thenThrow with checked exceptions.
+-keepattributes Exceptions
+
+# Minimal set of keep rules for mocked methods with checked exceptions.
+# This can be relaxed to specific packages if that simplifies testing.
+# See also https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode.
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class android.hardware.security.keymint.IRemotelyProvisionedComponent { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.IGetRegistrationCallback { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.interfaces.SystemInterface { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.provisioner.Provisioner { public <methods>; }
\ No newline at end of file
diff --git a/app/src/com/android/rkpdapp/ThreadPool.java b/app/src/com/android/rkpdapp/ThreadPool.java
index 6d4b9a1..3ed7755 100644
--- a/app/src/com/android/rkpdapp/ThreadPool.java
+++ b/app/src/com/android/rkpdapp/ThreadPool.java
@@ -17,21 +17,29 @@
package com.android.rkpdapp;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
* This class provides a global thread pool to RKPD app.
*/
public class ThreadPool {
- public static final int NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors();
+ public static final int NUMBER_OF_THREADS = 4;
/*
- * This thread pool has a minimum of 0 threads and a maximum of up to the
- * number of processors. If a thread is idle for more than 30 seconds, it is
- * terminated. RKPD is idle most of the time. So, this way we can don't keep
- * unused threads around.
- *
- * Each thread has an unbounded queue. This allows RKPD to serve requests
- * asynchronously.
+ * This thread pool has a minimum of 0 threads and a maximum of up to 4. If
+ * a thread is idle for more than 60 seconds, it is terminated. RKPD is idle
+ * most of the time. So, this way we can don't keep unused threads around.
*/
- public static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
+ public static final ExecutorService EXECUTOR;
+
+ static {
+ ThreadPoolExecutor executor =
+ new ThreadPoolExecutor(/*corePoolSize=*/ NUMBER_OF_THREADS,
+ /*maximumPoolSize=*/ NUMBER_OF_THREADS,
+ /*keepAliveTime=*/ 60L, /*unit=*/ TimeUnit.SECONDS,
+ /*workQueue=*/ new LinkedBlockingQueue<Runnable>());
+ executor.allowCoreThreadTimeOut(true);
+ EXECUTOR = executor;
+ }
}
diff --git a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
index a989cf6..8ce4917 100644
--- a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
@@ -18,8 +18,9 @@
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.net.NetworkCapabilities;
import android.net.TrafficStats;
+import android.net.Uri;
import android.util.Base64;
import android.util.Log;
@@ -37,6 +38,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.Charset;
@@ -53,19 +55,31 @@
*/
public class ServerInterface {
+ private static final int SYNC_CONNECT_TIMEOUT_MS = 1000;
private static final int TIMEOUT_MS = 20000;
private static final int BACKOFF_TIME_MS = 100;
private static final String TAG = "RkpdServerInterface";
private static final String GEEK_URL = ":fetchEekChain";
- private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?";
- private static final String CHALLENGE_PARAMETER = "challenge=";
- private static final String REQUEST_ID_PARAMETER = "request_id=";
+ private static final String CERTIFICATE_SIGNING_URL = ":signCertificates";
+ private static final String CHALLENGE_PARAMETER = "challenge";
+ private static final String REQUEST_ID_PARAMETER = "request_id";
private final Context mContext;
+ private final boolean mIsAsync;
private enum Operation {
- FETCH_GEEK,
- SIGN_CERTS;
+ FETCH_GEEK(1),
+ SIGN_CERTS(2);
+
+ private final int mTrafficTag;
+
+ Operation(int trafficTag) {
+ mTrafficTag = trafficTag;
+ }
+
+ public int getTrafficTag() {
+ return mTrafficTag;
+ }
public ProvisioningAttempt.Status getHttpErrorStatus() {
if (Objects.equals(name(), FETCH_GEEK.name())) {
@@ -95,8 +109,13 @@
}
}
- public ServerInterface(Context context) {
+ public ServerInterface(Context context, boolean isAsync) {
this.mContext = context;
+ this.mIsAsync = isAsync;
+ }
+
+ private int getConnectTimeoutMs() {
+ return mIsAsync ? TIMEOUT_MS : SYNC_CONNECT_TIMEOUT_MS;
}
/**
@@ -114,11 +133,9 @@
*/
public List<byte[]> requestSignedCertificates(byte[] csr, byte[] challenge,
ProvisioningAttempt metrics) throws RkpdException, InterruptedException {
- final String challengeParam = CHALLENGE_PARAMETER + Base64.encodeToString(challenge,
- Base64.URL_SAFE | Base64.NO_WRAP);
- final String fullUrl = CERTIFICATE_SIGNING_URL + String.join("&", challengeParam,
- REQUEST_ID_PARAMETER + generateAndLogRequestId());
- final byte[] cborBytes = connectAndGetData(metrics, fullUrl, csr, Operation.SIGN_CERTS);
+ final byte[] cborBytes =
+ connectAndGetData(metrics, generateSignCertsUrl(challenge),
+ csr, Operation.SIGN_CERTS);
List<byte[]> certChains = CborUtils.parseSignedCertificates(cborBytes);
if (certChains == null) {
metrics.setStatus(ProvisioningAttempt.Status.INTERNAL_ERROR);
@@ -143,6 +160,22 @@
return certChains;
}
+ private URL generateSignCertsUrl(byte[] challenge) throws RkpdException {
+ try {
+ return new URL(Uri.parse(Settings.getUrl(mContext)).buildUpon()
+ .appendEncodedPath(CERTIFICATE_SIGNING_URL)
+ .appendQueryParameter(CHALLENGE_PARAMETER,
+ Base64.encodeToString(challenge, Base64.URL_SAFE | Base64.NO_WRAP))
+ .appendQueryParameter(REQUEST_ID_PARAMETER, generateAndLogRequestId())
+ .build()
+ .toString()
+ // Needed due to the `:` in the URL endpoint.
+ .replaceFirst("%3A", ":"));
+ } catch (MalformedURLException e) {
+ throw new RkpdException(RkpdException.ErrorCode.HTTP_CLIENT_ERROR, "Bad URL", e);
+ }
+ }
+
private String generateAndLogRequestId() {
String reqId = UUID.randomUUID().toString();
Log.i(TAG, "request_id: " + reqId);
@@ -152,7 +185,7 @@
/**
* Calls out to the specified backend servers to retrieve an Endpoint Encryption Key and
* corresponding certificate chain to provide to KeyMint. This public key will be used to
- * perform an ECDH computation, using the shared secret to encrypt privacy sensitive components
+ * perform an ECDH computation, using the shared secret to encrypt privacy-sensitive components
* in the bundle that the server needs from the device in order to provision certificates.
*
* A challenge is also returned from the server so that it can check freshness of the follow-up
@@ -163,7 +196,8 @@
public GeekResponse fetchGeek(ProvisioningAttempt metrics)
throws RkpdException, InterruptedException {
byte[] input = CborUtils.buildProvisioningInfo(mContext);
- byte[] cborBytes = connectAndGetData(metrics, GEEK_URL, input, Operation.FETCH_GEEK);
+ byte[] cborBytes =
+ connectAndGetData(metrics, generateFetchGeekUrl(), input, Operation.FETCH_GEEK);
GeekResponse resp = CborUtils.parseGeekResponse(cborBytes);
if (resp == null) {
metrics.setStatus(ProvisioningAttempt.Status.FETCH_GEEK_HTTP_ERROR);
@@ -174,6 +208,19 @@
return resp;
}
+ private URL generateFetchGeekUrl() throws RkpdException {
+ try {
+ return new URL(Uri.parse(Settings.getUrl(mContext)).buildUpon()
+ .appendPath(GEEK_URL)
+ .build()
+ .toString()
+ // Needed due to the `:` in the URL endpoint.
+ .replaceFirst("%3A", ":"));
+ } catch (MalformedURLException e) {
+ throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR, "Bad URL", e);
+ }
+ }
+
private void checkDataBudget(ProvisioningAttempt metrics)
throws RkpdException {
if (!Settings.hasErrDataBudget(mContext, null /* curTime */)) {
@@ -186,9 +233,7 @@
private RkpdException makeNetworkError(String message,
ProvisioningAttempt metrics) {
- ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- if (networkInfo != null && networkInfo.isConnected()) {
+ if (isNetworkConnected(mContext)) {
return new RkpdException(
RkpdException.ErrorCode.NETWORK_COMMUNICATION_ERROR, message);
}
@@ -198,6 +243,18 @@
}
/**
+ * Checks whether network is connected.
+ * @return true if connected else false.
+ */
+ public static boolean isNetworkConnected(Context context) {
+ ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+ NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
+ return capabilities != null
+ && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+ }
+
+ /**
* Fetch a GEEK from the server and update SettingsManager appropriately with the return
* values. This will also delete all keys in the attestation key pool if the server has
* indicated that RKP should be turned off.
@@ -273,45 +330,52 @@
}
}
- private byte[] connectAndGetData(ProvisioningAttempt metrics, String endpoint, byte[] input,
+ private byte[] connectAndGetData(ProvisioningAttempt metrics, URL url, byte[] input,
Operation operation) throws RkpdException, InterruptedException {
- TrafficStats.setThreadStatsTag(0);
+ final int oldTrafficTag = TrafficStats.getAndSetThreadStatsTag(operation.getTrafficTag());
int backoff_time = BACKOFF_TIME_MS;
int attempt = 1;
+ RkpdException lastSeenRkpdException = null;
try (StopWatch retryTimer = new StopWatch(TAG)) {
retryTimer.start();
+ // Retry logic.
+ // Provide longer retries (up to 10s) for RkpdExceptions
+ // Provide shorter retries (once) for everything else.
while (true) {
checkDataBudget(metrics);
try {
Log.v(TAG, "Requesting data from server. Attempt " + attempt);
- return requestData(metrics, new URL(Settings.getUrl(mContext) + endpoint),
- input);
+ return requestData(metrics, url, input);
} catch (SocketTimeoutException e) {
metrics.setStatus(operation.getTimedOutStatus());
Log.e(TAG, "Server timed out. " + e.getMessage());
} catch (IOException e) {
metrics.setStatus(operation.getIoExceptionStatus());
- Log.e(TAG, "Failed to complete request from server." + e.getMessage());
+ Log.e(TAG, "Failed to complete request from server. " + e.getMessage());
} catch (RkpdException e) {
+ lastSeenRkpdException = e;
if (e.getErrorCode() == RkpdException.ErrorCode.DEVICE_NOT_REGISTERED) {
metrics.setStatus(
ProvisioningAttempt.Status.SIGN_CERTS_DEVICE_NOT_REGISTERED);
throw e;
} else {
metrics.setStatus(operation.getHttpErrorStatus());
- if (e.getErrorCode() == RkpdException.ErrorCode.HTTP_CLIENT_ERROR) {
- throw e;
- }
}
}
- if (retryTimer.getElapsedMillis() > Settings.getMaxRequestTime(mContext)) {
+ // Only RkpdExceptions should get longer retries.
+ if (retryTimer.getElapsedMillis() > Settings.getMaxRequestTime(mContext)
+ || lastSeenRkpdException == null) {
break;
- } else {
- Thread.sleep(backoff_time);
- backoff_time *= 2;
- attempt += 1;
}
+ Thread.sleep(backoff_time);
+ backoff_time *= 2;
+ attempt += 1;
}
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTrafficTag);
+ }
+ if (lastSeenRkpdException != null) {
+ throw lastSeenRkpdException;
}
Settings.incrementFailureCounter(mContext);
throw makeNetworkError("Error getting data from server.", metrics);
@@ -323,7 +387,7 @@
try (StopWatch serverWaitTimer = metrics.startServerWait()) {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
- con.setConnectTimeout(TIMEOUT_MS);
+ con.setConnectTimeout(getConnectTimeoutMs());
con.setReadTimeout(TIMEOUT_MS);
con.setDoOutput(true);
diff --git a/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java b/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
index 38766b1..3ba716c 100644
--- a/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
@@ -19,8 +19,11 @@
import android.annotation.TestApi;
import android.hardware.security.keymint.IRemotelyProvisionedComponent;
import android.os.ServiceManager;
+import android.util.Log;
import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
/**
* Provides convenience methods for interfacing with ServiceManager class and its static functions.
@@ -28,33 +31,54 @@
public class ServiceManagerInterface {
private static final String TAG = "RkpdSvcManagerInterface";
private static SystemInterface[] sInstances;
+ private static Map<String, IRemotelyProvisionedComponent> sBinders;
private ServiceManagerInterface() {
}
- private static SystemInterface createSystemInterface(String serviceName) {
+ private static SystemInterface tryCreateSystemInterface(IRemotelyProvisionedComponent binder,
+ String serviceName) {
+ try {
+ return new SystemInterface(binder, serviceName);
+ } catch (UnsupportedOperationException e) {
+ Log.i(TAG, serviceName + " is unsupported.");
+ return null;
+ }
+ }
+
+ private static IRemotelyProvisionedComponent getBinder(String serviceName) {
IRemotelyProvisionedComponent binder = IRemotelyProvisionedComponent.Stub.asInterface(
ServiceManager.waitForDeclaredService(serviceName));
if (binder == null) {
throw new IllegalArgumentException("Cannot find any implementation for " + serviceName);
}
- return new SystemInterface(binder, serviceName);
+ return binder;
}
/**
* Gets all the instances on this device for IRemotelyProvisionedComponent as an array. The
* returned values each contain a binder for interacting with the instance.
*
- * For testing purposes, the instances may be overridden by setInstances
+ * For testing purposes, the instances may be overridden by either setInstances or setBinders.
*/
public static SystemInterface[] getAllInstances() {
+ if (sBinders != null) {
+ return sBinders.entrySet().stream()
+ .map(x -> tryCreateSystemInterface(x.getValue(), x.getKey()))
+ .filter(Objects::nonNull)
+ .toArray(SystemInterface[]::new);
+ }
if (sInstances != null) {
return sInstances;
}
String irpcInterface = IRemotelyProvisionedComponent.DESCRIPTOR;
return Arrays.stream(ServiceManager.getDeclaredInstances(irpcInterface))
- .map(x -> createSystemInterface(irpcInterface + "/" + x))
+ .map(x -> {
+ String serviceName = irpcInterface + "/" + x;
+ return tryCreateSystemInterface(getBinder(serviceName), serviceName);
+ })
+ .filter(Objects::nonNull)
.toArray(SystemInterface[]::new);
}
@@ -62,10 +86,17 @@
* Get a specific system interface instance for a given IRemotelyProvisionedComponent.
* If the given serviceName does not map to a known IRemotelyProvisionedComponent, this
* method throws IllegalArgumentException.
+ * If the given serviceName is not supported, this method throws UnsupportedOperationException.
*
- * For testing purposes, the instances may be overridden by setInstances.
+ * For testing purposes, the instances may be overridden by either setInstances or setBinders.
*/
public static SystemInterface getInstance(String serviceName) {
+ if (sBinders != null) {
+ if (sBinders.containsKey(serviceName)) {
+ return new SystemInterface(sBinders.get(serviceName), serviceName);
+ }
+ throw new IllegalArgumentException("Cannot find any binder for " + serviceName);
+ }
if (sInstances != null) {
for (SystemInterface i : sInstances) {
if (i.getServiceName().equals(serviceName)) {
@@ -75,11 +106,16 @@
throw new IllegalArgumentException("Cannot find any implementation for " + serviceName);
}
- return createSystemInterface(serviceName);
+ return new SystemInterface(getBinder(serviceName), serviceName);
}
@TestApi
public static void setInstances(SystemInterface[] instances) {
sInstances = instances;
}
+
+ @TestApi
+ public static void setBinders(Map<String, IRemotelyProvisionedComponent> binders) {
+ sBinders = binders;
+ }
}
diff --git a/app/src/com/android/rkpdapp/interfaces/SystemInterface.java b/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
index 87cba0a..9bca6e3 100644
--- a/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
@@ -182,13 +182,13 @@
int batchSize = mBinder.getHardwareInfo().supportedNumKeysInCsr;
- if (batchSize <= RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR) {
+ if (batchSize < RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR) {
Log.w(TAG, "HAL returned a batch size that's too small (" + batchSize
+ "), defaulting to " + RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR);
return RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR;
}
- if (batchSize >= maxBatchSize) {
+ if (batchSize > maxBatchSize) {
Log.w(TAG, "HAL returned a batch size that's too large (" + batchSize
+ "), defaulting to " + maxBatchSize);
return maxBatchSize;
diff --git a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
index 4d24219..6613f30 100644
--- a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
+++ b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
@@ -192,8 +192,8 @@
transportType, getIntStatus(), mHttpStatusError);
RkpdStatsLog.write(RkpdStatsLog.REMOTE_KEY_PROVISIONING_TIMING,
mServerWaitTimer.getElapsedMillis(), mBinderWaitTimer.getElapsedMillis(),
- mLockWaitTimer.getElapsedMillis(),
- mTotalTimer.getElapsedMillis(), transportType, mRemotelyProvisionedComponent);
+ mLockWaitTimer.getElapsedMillis(), mTotalTimer.getElapsedMillis(), transportType,
+ mRemotelyProvisionedComponent, mCause, getIntStatus());
}
private static Enablement getEnablementForComponent(String serviceName) {
diff --git a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
index 04ca1be..f3da031 100644
--- a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
@@ -36,6 +36,8 @@
import com.android.rkpdapp.utils.Settings;
import java.time.Instant;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
import co.nstant.in.cbor.CborException;
@@ -47,6 +49,7 @@
public class PeriodicProvisioner extends Worker {
public static final String UNIQUE_WORK_NAME = "ProvisioningJob";
private static final String TAG = "RkpdPeriodicProvisioner";
+ private static final boolean IS_ASYNC = true;
private final Context mContext;
private final ProvisionedKeyDao mKeyDao;
@@ -71,7 +74,7 @@
return Result.success();
}
- if (Settings.getDefaultUrl().isEmpty()) {
+ if (Settings.getUrl(mContext).isEmpty()) {
Log.i(TAG, "Stopping periodic provisioner: system has no configured server endpoint");
WorkManager.getInstance(mContext).cancelWorkById(getId());
return Result.success();
@@ -85,7 +88,7 @@
// Fetch geek from the server and figure out whether provisioning needs to be stopped.
GeekResponse response;
try {
- response = new ServerInterface(mContext).fetchGeekAndUpdate(metrics);
+ response = new ServerInterface(mContext, IS_ASYNC).fetchGeekAndUpdate(metrics);
} catch (InterruptedException | RkpdException e) {
Log.e(TAG, "Error fetching configuration from the RKP server", e);
return Result.failure();
@@ -102,9 +105,9 @@
}
Log.i(TAG, "Total services found implementing IRPC: " + irpcs.length);
- Provisioner provisioner = new Provisioner(mContext, mKeyDao);
- Result result = Result.success();
- for (SystemInterface irpc : irpcs) {
+ Provisioner provisioner = new Provisioner(mContext, mKeyDao, IS_ASYNC);
+ final AtomicBoolean result = new AtomicBoolean(true);
+ Arrays.stream(irpcs).parallel().forEach(irpc -> {
Log.i(TAG, "Starting provisioning for " + irpc);
try {
provisioner.provisionKeys(metrics, irpc, response);
@@ -112,13 +115,13 @@
Log.i(TAG, "Successfully provisioned " + irpc);
} catch (CborException e) {
Log.e(TAG, "Error parsing CBOR for " + irpc, e);
- result = Result.failure();
+ result.set(false);
} catch (InterruptedException | RkpdException e) {
Log.e(TAG, "Error provisioning keys for " + irpc, e);
- result = Result.failure();
+ result.set(false);
}
- }
- return result;
+ });
+ return result.get() ? Result.success() : Result.failure();
}
}
diff --git a/app/src/com/android/rkpdapp/provisioner/Provisioner.java b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
index eb63b50..19db3a7 100644
--- a/app/src/com/android/rkpdapp/provisioner/Provisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
@@ -53,10 +53,13 @@
private final Context mContext;
private final ProvisionedKeyDao mKeyDao;
+ private final boolean mIsAsync;
- public Provisioner(final Context applicationContext, ProvisionedKeyDao keyDao) {
+ public Provisioner(final Context applicationContext, ProvisionedKeyDao keyDao,
+ boolean isAsync) {
mContext = applicationContext;
mKeyDao = keyDao;
+ mIsAsync = isAsync;
}
/**
@@ -124,7 +127,7 @@
throws RkpdException, CborException, InterruptedException {
int provisionedSoFar = 0;
List<byte[]> certChains = new ArrayList<>(keysGenerated.size());
- int maxBatchSize = 0;
+ int maxBatchSize;
try {
maxBatchSize = systemInterface.getBatchSize();
} catch (RemoteException e) {
@@ -154,7 +157,7 @@
throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
"Failed to serialize payload");
}
- return new ServerInterface(mContext).requestSignedCertificates(certRequest,
+ return new ServerInterface(mContext, mIsAsync).requestSignedCertificates(certRequest,
response.getChallenge(), metrics);
}
@@ -162,9 +165,11 @@
List<RkpKey> keysGenerated) throws RkpdException {
List<ProvisionedKey> provisionedKeys = new ArrayList<>();
for (byte[] chain : certChains) {
- X509Certificate cert = X509Utils.formatX509Certs(chain)[0];
- long expirationDate = cert.getNotAfter().getTime();
- byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(cert);
+ X509Certificate[] certChain = X509Utils.formatX509Certs(chain);
+ X509Certificate leafCertificate = certChain[0];
+ long expirationDate = X509Utils.getExpirationTimeForCertificateChain(certChain)
+ .getTime();
+ byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(leafCertificate);
if (rawPublicKey == null) {
Log.e(TAG, "Skipping malformed public key.");
continue;
diff --git a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
index 544d59d..c92cbd7 100644
--- a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
+++ b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
@@ -39,6 +39,7 @@
/** Provides the implementation for IRemoteProvisioning.aidl */
public class RemoteProvisioningService extends Service {
public static final String TAG = "com.android.rkpdapp";
+ private static final boolean IS_ASYNC = false;
private final IRemoteProvisioning.Stub mBinder = new RemoteProvisioningBinder();
@Override
@@ -58,7 +59,7 @@
final Context context = getApplicationContext();
RkpdClientOperation metric = RkpdClientOperation.getRegistration(callerUid, irpcName);
try (metric) {
- if (Settings.getDefaultUrl().isEmpty()) {
+ if (Settings.getUrl(context).isEmpty()) {
callback.onError("RKP is disabled. System configured with no default URL.");
metric.setResult(RkpdClientOperation.Result.RKP_UNSUPPORTED);
return;
@@ -75,9 +76,9 @@
}
ProvisionedKeyDao dao = RkpdDatabase.getDatabase(context).provisionedKeyDao();
- Provisioner provisioner = new Provisioner(context, dao);
+ Provisioner provisioner = new Provisioner(context, dao, IS_ASYNC);
IRegistration.Stub registration = new RegistrationBinder(context, callerUid,
- systemInterface, dao, new ServerInterface(context), provisioner,
+ systemInterface, dao, new ServerInterface(context, IS_ASYNC), provisioner,
ThreadPool.EXECUTOR);
metric.setResult(RkpdClientOperation.Result.SUCCESS);
callback.onSuccess(registration);
diff --git a/app/src/com/android/rkpdapp/utils/X509Utils.java b/app/src/com/android/rkpdapp/utils/X509Utils.java
index 4e12872..61b55c2 100644
--- a/app/src/com/android/rkpdapp/utils/X509Utils.java
+++ b/app/src/com/android/rkpdapp/utils/X509Utils.java
@@ -40,6 +40,7 @@
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Date;
import java.util.Set;
/**
@@ -158,4 +159,16 @@
return false;
}
}
+
+ /**
+ * Gets the date when the entire cert chain expires. This can be calculated by
+ * checking the individual certificate times and returning the minimum of all.
+ * @return Expiration time for the certificate chain
+ */
+ public static Date getExpirationTimeForCertificateChain(X509Certificate[] certChain) {
+ return Arrays.stream(certChain)
+ .map(X509Certificate::getNotAfter)
+ .min(Date::compareTo)
+ .orElse(null);
+ }
}
diff --git a/app/tests/e2e/Android.bp b/app/tests/e2e/Android.bp
index 1bba10c..baa003d 100644
--- a/app/tests/e2e/Android.bp
+++ b/app/tests/e2e/Android.bp
@@ -26,7 +26,7 @@
"androidx.test.rules",
"androidx.work_work-testing",
"platform-test-annotations",
- "truth-prebuilt",
+ "truth",
],
platform_apis: true,
test_suites: [
@@ -34,6 +34,6 @@
"device-tests",
"mts-rkpd",
],
- min_sdk_version: "UpsideDownCake",
+ min_sdk_version: "33",
instrumentation_for: "rkpdapp",
}
diff --git a/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java b/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
index 5a8e728..6f46896 100644
--- a/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
+++ b/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
@@ -41,11 +41,12 @@
import com.android.rkpdapp.database.ProvisionedKey;
import com.android.rkpdapp.database.ProvisionedKeyDao;
import com.android.rkpdapp.database.RkpdDatabase;
+import com.android.rkpdapp.interfaces.ServerInterface;
import com.android.rkpdapp.interfaces.ServiceManagerInterface;
import com.android.rkpdapp.interfaces.SystemInterface;
import com.android.rkpdapp.provisioner.PeriodicProvisioner;
import com.android.rkpdapp.testutil.FakeRkpServer;
-import com.android.rkpdapp.testutil.NetworkUtils;
+import com.android.rkpdapp.testutil.SystemInterfaceSelector;
import com.android.rkpdapp.testutil.SystemPropertySetter;
import com.android.rkpdapp.utils.Settings;
import com.android.rkpdapp.utils.X509Utils;
@@ -108,15 +109,20 @@
@BeforeClass
public static void init() {
sContext = ApplicationProvider.getApplicationContext();
-
- assume()
- .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
- .that(SystemProperties.get("remote_provisioning.hostname"))
- .isNotEmpty();
}
@Before
public void setUp() throws Exception {
+ assume()
+ .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
+ .that(SystemProperties.get("remote_provisioning.hostname"))
+ .isNotEmpty();
+
+ assume()
+ .withMessage("RKP Integration tests rely on network availability.")
+ .that(ServerInterface.isNetworkConnected(sContext))
+ .isTrue();
+
Settings.clearPreferences(sContext);
mKeyDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
@@ -124,7 +130,8 @@
mKeyStore.load(null);
mKeyDao.deleteAllKeys();
- SystemInterface systemInterface = ServiceManagerInterface.getInstance(mServiceName);
+ SystemInterface systemInterface =
+ SystemInterfaceSelector.getSystemInterfaceForServiceName(mServiceName);
ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
}
@@ -132,8 +139,10 @@
public void tearDown() throws Exception {
Settings.clearPreferences(sContext);
- mKeyStore.deleteEntry(getTestKeyAlias());
- mKeyDao.deleteAllKeys();
+ if (mKeyDao != null) {
+ mKeyStore.deleteEntry(getTestKeyAlias());
+ mKeyDao.deleteAllKeys();
+ }
ServiceManagerInterface.setInstances(null);
}
@@ -221,21 +230,17 @@
// Verify that if the system is set to rkp only, key creation fails when RKP is unable
// to get keys.
- try {
+ try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+ FakeRkpServer.Response.INTERNAL_ERROR)) {
Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
- Duration.ofDays(1), "bad url");
+ Duration.ofDays(1), server.getUrl());
Settings.setMaxRequestTime(sContext, 100);
createKeystoreKeyBackedByRkp();
assertWithMessage("Should have gotten a KeyStoreException").fail();
} catch (ProviderException e) {
assertThat(e.getCause()).isInstanceOf(KeyStoreException.class);
- if (NetworkUtils.isNetworkConnected(sContext)) {
- assertThat(((KeyStoreException) e.getCause()).getErrorCode())
- .isEqualTo(ResponseCode.OUT_OF_KEYS_TRANSIENT_ERROR);
- } else {
- assertThat(((KeyStoreException) e.getCause()).getErrorCode())
- .isEqualTo(ResponseCode.OUT_OF_KEYS_PENDING_INTERNET_CONNECTIVITY);
- }
+ assertThat(((KeyStoreException) e.getCause()).getErrorCode())
+ .isEqualTo(ResponseCode.OUT_OF_KEYS_TRANSIENT_ERROR);
}
}
@@ -247,14 +252,17 @@
.that(SystemProperties.getBoolean(getRkpOnlyProp(), false))
.isFalse();
- Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
- Duration.ofDays(1), "bad url");
+ try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+ FakeRkpServer.Response.INTERNAL_ERROR)) {
+ Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+ Duration.ofDays(1), server.getUrl());
- createKeystoreKey();
+ createKeystoreKey();
- // Ensure the key has a cert, but it didn't come from rkpd.
- assertThat(mKeyStore.getCertificateChain(getTestKeyAlias())).isNotEmpty();
- assertThat(mKeyDao.getTotalKeysForIrpc(mServiceName)).isEqualTo(0);
+ // Ensure the key has a cert, but it didn't come from rkpd.
+ assertThat(mKeyStore.getCertificateChain(getTestKeyAlias())).isNotEmpty();
+ assertThat(mKeyDao.getTotalKeysForIrpc(mServiceName)).isEqualTo(0);
+ }
}
@Test
@@ -275,8 +283,9 @@
@Test
public void testRetryableRkpError() throws Exception {
- try {
- Settings.setDeviceConfig(sContext, 1, Duration.ofDays(1), "bad url");
+ try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+ FakeRkpServer.Response.INTERNAL_ERROR)) {
+ Settings.setDeviceConfig(sContext, 1, Duration.ofDays(1), server.getUrl());
Settings.setMaxRequestTime(sContext, 100);
createKeystoreKeyBackedByRkp();
Assert.fail("Expected a keystore exception");
diff --git a/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java b/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
index 51b5e5b..c1762d5 100644
--- a/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
+++ b/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
@@ -38,9 +38,11 @@
import com.android.rkpdapp.database.ProvisionedKey;
import com.android.rkpdapp.database.ProvisionedKeyDao;
import com.android.rkpdapp.database.RkpdDatabase;
+import com.android.rkpdapp.interfaces.ServerInterface;
import com.android.rkpdapp.interfaces.ServiceManagerInterface;
import com.android.rkpdapp.interfaces.SystemInterface;
import com.android.rkpdapp.provisioner.PeriodicProvisioner;
+import com.android.rkpdapp.testutil.SystemInterfaceSelector;
import com.android.rkpdapp.testutil.TestDatabase;
import com.android.rkpdapp.testutil.TestProvisionedKeyDao;
import com.android.rkpdapp.utils.Settings;
@@ -58,7 +60,6 @@
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.spec.ECGenParameterSpec;
-import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.Executors;
@@ -89,32 +90,42 @@
@BeforeClass
public static void init() {
sContext = ApplicationProvider.getApplicationContext();
-
- assume()
- .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
- .that(SystemProperties.get("remote_provisioning.hostname"))
- .isNotEmpty();
}
@Before
public void setUp() throws Exception {
+ assume()
+ .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
+ .that(SystemProperties.get("remote_provisioning.hostname"))
+ .isNotEmpty();
+
+ assume()
+ .withMessage("RKP Integration tests rely on network availability.")
+ .that(ServerInterface.isNetworkConnected(sContext))
+ .isTrue();
+
Settings.clearPreferences(sContext);
mRealDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
mRealDao.deleteAllKeys();
mTestDao = Room.databaseBuilder(sContext, TestDatabase.class, DB_NAME).build().dao();
- SystemInterface systemInterface = ServiceManagerInterface.getInstance(mServiceName);
- ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
mProvisioner = TestWorkerBuilder.from(
sContext,
PeriodicProvisioner.class,
Executors.newSingleThreadExecutor()).build();
+
+ SystemInterface systemInterface =
+ SystemInterfaceSelector.getSystemInterfaceForServiceName(mServiceName);
+ ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
}
@After
public void tearDown() throws Exception {
Settings.clearPreferences(sContext);
- mRealDao.deleteAllKeys();
+
+ if (mRealDao != null) {
+ mRealDao.deleteAllKeys();
+ }
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
@@ -144,8 +155,6 @@
public void provisionThenExpireThenProvisionAgain() throws Exception {
assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
- final Instant expiry = Instant.now().plus(Duration.ofHours(1));
-
List<ProvisionedKey> keys = mTestDao.getAllKeys();
// Expire a key
@@ -177,6 +186,8 @@
StatsProcessor.PoolStats updatedPool = StatsProcessor.processPool(mRealDao, mServiceName,
Settings.getExtraSignedKeysAvailable(sContext),
Settings.getExpirationTime(sContext));
- assertThat(updatedPool.toString()).isEqualTo(pool.toString());
+
+ assertThat(updatedPool.keysInUse + updatedPool.keysUnassigned)
+ .isEqualTo(pool.keysInUse + pool.keysUnassigned);
}
}
diff --git a/app/tests/hosttest/Android.bp b/app/tests/hosttest/Android.bp
index 282951c..c0bfd87 100644
--- a/app/tests/hosttest/Android.bp
+++ b/app/tests/hosttest/Android.bp
@@ -29,7 +29,7 @@
"host-libprotobuf-java-full",
"platformprotos",
"tradefed",
- "truth-prebuilt",
+ "truth",
],
static_libs: [
"cts-statsd-atom-host-test-utils",
diff --git a/app/tests/hosttest/AndroidTest.xml b/app/tests/hosttest/AndroidTest.xml
index 5e79b2e..10c71c2 100644
--- a/app/tests/hosttest/AndroidTest.xml
+++ b/app/tests/hosttest/AndroidTest.xml
@@ -26,4 +26,11 @@
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
<option name="jar" value="RkpdAppHostTests.jar" />
</test>
+
+ <!-- Only run if RKPD mainline module is installed -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="enable" value="true" />
+ <option name="mainline-module-package-name" value="com.android.rkpd" />
+ </object>
</configuration>
diff --git a/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java b/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
index 823179d..e7bf7cc 100644
--- a/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
+++ b/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
@@ -40,6 +40,7 @@
@RunWith(DeviceJUnit4ClassRunner.class)
public final class RkpdStatsTests extends AtomsHostTest {
private static final int NO_HTTP_STATUS_ERROR = 0;
+ private static final int HTTP_STATUS_SERVER_ERROR = 500;
private static final int HTTPS_OK = 200;
private static final String RPC_DEFAULT =
"android.hardware.security.keymint.IRemotelyProvisionedComponent/default";
@@ -107,7 +108,7 @@
assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
assertThat(attempt.getStatus()).isEqualTo(
- RemoteKeyProvisioningStatus.FETCH_GEEK_IO_EXCEPTION);
+ RemoteKeyProvisioningStatus.FETCH_GEEK_HTTP_ERROR);
final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
assertThat(timing).isNotNull();
@@ -125,7 +126,7 @@
assertThat(network).isNotNull();
assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
- assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+ assertThat(network.getHttpStatusError()).isEqualTo(HTTP_STATUS_SERVER_ERROR);
}
@Test
diff --git a/app/tests/stress/Android.bp b/app/tests/stress/Android.bp
index 01267df..4320f2f 100644
--- a/app/tests/stress/Android.bp
+++ b/app/tests/stress/Android.bp
@@ -24,7 +24,7 @@
"androidx.test.core",
"androidx.test.rules",
"platform-test-annotations",
- "truth-prebuilt",
+ "truth",
],
platform_apis: true,
test_suites: [
@@ -32,6 +32,6 @@
"device-tests",
"mts-rkpd",
],
- min_sdk_version: "UpsideDownCake",
+ min_sdk_version: "33",
instrumentation_for: "rkpdapp",
}
diff --git a/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java b/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
index ce64d91..4ab5438 100644
--- a/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
+++ b/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
@@ -82,8 +82,10 @@
}
private RegistrationBinder createRegistrationBinder() {
+ boolean isAsync = false;
return new RegistrationBinder(mContext, Process.myUid(), mIrpcHal, mKeyDao,
- new ServerInterface(mContext), new Provisioner(mContext, mKeyDao), mExecutor);
+ new ServerInterface(mContext, isAsync), new Provisioner(mContext, mKeyDao, isAsync),
+ mExecutor);
}
private void getKeyHelper(int keyId) {
diff --git a/app/tests/unit/Android.bp b/app/tests/unit/Android.bp
index 19eea6b..2a9ae30 100644
--- a/app/tests/unit/Android.bp
+++ b/app/tests/unit/Android.bp
@@ -30,7 +30,7 @@
"libnanohttpd",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
- "truth-prebuilt",
+ "truth",
"rkpdapp-tink-prebuilt-test-only",
"bouncycastle-unbundled",
],
@@ -40,7 +40,7 @@
"device-tests",
"mts-rkpd",
],
- min_sdk_version: "UpsideDownCake",
+ min_sdk_version: "33",
instrumentation_for: "rkpdapp",
}
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
index 005c4f4..a278f50 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
@@ -24,7 +24,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Context;
@@ -46,11 +45,13 @@
import com.android.rkpdapp.interfaces.ServiceManagerInterface;
import com.android.rkpdapp.interfaces.SystemInterface;
import com.android.rkpdapp.provisioner.PeriodicProvisioner;
+import com.android.rkpdapp.service.RegistrationBinder;
import com.android.rkpdapp.testutil.FakeRkpServer;
import com.android.rkpdapp.testutil.SystemPropertySetter;
import com.android.rkpdapp.utils.Settings;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -75,6 +76,9 @@
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
+
+ Assume.assumeFalse(Settings.getDefaultUrl().isEmpty());
+
RkpdDatabase.getDatabase(mContext).provisionedKeyDao().deleteAllKeys();
mProvisioner = TestWorkerBuilder.from(
mContext,
@@ -85,7 +89,7 @@
.setExecutor(new SynchronousExecutor())
.build();
WorkManagerTestInitHelper.initializeTestWorkManager(mContext, config);
-
+ Settings.clearPreferences(mContext);
}
@After
@@ -172,8 +176,8 @@
ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.failure());
- // we should have failed before making any local HAl calls
- verifyNoMoreInteractions(mockHal);
+ // we should have failed before trying to generate any keys
+ verify(mockHal, never()).generateKey(any());
}
}
@@ -193,8 +197,8 @@
ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
- // since RKP is disabled, there should be no interactions with the HAL
- verifyNoMoreInteractions(mockHal);
+ // since RKP is disabled, there should be no keys generated
+ verify(mockHal, never()).generateKey(any());
}
// when RKP is detected as disabled, the provisioner is supposed to delete all keys
@@ -205,9 +209,12 @@
public void provisioningExpiresOldKeys() throws Exception {
ProvisionedKeyDao dao = RkpdDatabase.getDatabase(mContext).provisionedKeyDao();
ProvisionedKey oldKey = new ProvisionedKey(new byte[1], "fake-irpc", new byte[2],
- new byte[3], Instant.now().minusSeconds(120));
+ new byte[3],
+ Instant.now().minus(RegistrationBinder.MIN_KEY_LIFETIME.multipliedBy(2)));
+ // Add 2 hours so that this key does not get deleted in case getKeyWorker comes alive.
ProvisionedKey freshKey = new ProvisionedKey(new byte[11], "fake-irpc", new byte[12],
- new byte[13], Instant.now().plusSeconds(120));
+ new byte[13],
+ Instant.now().plus(RegistrationBinder.MIN_KEY_LIFETIME.multipliedBy(2)));
dao.insertKeys(List.of(oldKey, freshKey));
assertThat(dao.getTotalKeysForIrpc("fake-irpc")).isEqualTo(2);
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
index 24a48f1..384b172 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
@@ -73,7 +73,7 @@
ProvisionedKeyDao keyDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
keyDao.deleteAllKeys();
- mProvisioner = new Provisioner(sContext, keyDao);
+ mProvisioner = new Provisioner(sContext, keyDao, false);
}
@After
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
index 15a6204..81465f1 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
@@ -58,6 +58,7 @@
service.attach(mContext, ActivityThread.currentActivityThread(),
"RemoteProvisioningService", null, mock(Application.class), null);
mBinder = IRemoteProvisioning.Stub.asInterface(service.onBind(null));
+ Settings.clearPreferences(mContext);
}
@After
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
index d7971df..896a4b3 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
@@ -21,7 +21,7 @@
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.net.NetworkCapabilities;
import android.util.Base64;
import androidx.test.core.app.ApplicationProvider;
@@ -61,7 +61,7 @@
@Before
public void setUp() {
Settings.clearPreferences(sContext);
- mServerInterface = new ServerInterface(sContext);
+ mServerInterface = new ServerInterface(sContext, false);
}
@After
@@ -81,7 +81,8 @@
ProvisioningAttempt.createScheduledAttemptMetrics(sContext));
assertWithMessage("Expected RkpdException.").fail();
} catch (RkpdException e) {
- // should throw this
+ assertThat(e.getErrorCode()).isEqualTo(RkpdException.ErrorCode.HTTP_SERVER_ERROR);
+ assertThat(e).hasMessageThat().contains("HTTP error status encountered");
}
}
@@ -183,6 +184,7 @@
FakeRkpServer.Response.SIGN_CERTS_USER_UNAUTHORIZED)) {
Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+ Settings.setMaxRequestTime(sContext, 100);
ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
sContext);
mServerInterface.requestSignedCertificates(new byte[0], new byte[0], metrics);
@@ -227,11 +229,18 @@
@Test
public void testDataBudgetEmptyFetchGeekNetworkConnected() throws Exception {
- // Check the data budget in order to initialize a rolling window.
- assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
- Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
- ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(sContext);
- try {
+ try (FakeRkpServer server = new FakeRkpServer(
+ FakeRkpServer.Response.FETCH_EEK_OK,
+ FakeRkpServer.Response.SIGN_CERTS_OK_VALID_CBOR)) {
+ Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
+ TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+
+ // Check the data budget in order to initialize a rolling window.
+ assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
+ Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
+ ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
+ sContext);
+
// We are okay in mocking connectivity failure since err data budget is the first thing
// to be checked.
mockConnectivityFailure(ConnectivityState.CONNECTED);
@@ -246,15 +255,21 @@
@Test
public void testDataBudgetEmptyFetchGeekNetworkDisconnected() throws Exception {
- // Check the data budget in order to initialize a rolling window.
- try {
- // We are okay in mocking connectivity failure since err data budget is the first thing
- // to be checked.
- mockConnectivityFailure(ConnectivityState.DISCONNECTED);
+ try (FakeRkpServer server = new FakeRkpServer(
+ FakeRkpServer.Response.FETCH_EEK_OK,
+ FakeRkpServer.Response.SIGN_CERTS_OK_VALID_CBOR)) {
+ Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
+ TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+
+ // Check the data budget in order to initialize a rolling window.
assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
sContext);
+
+ // We are okay in mocking connectivity failure since err data budget is the first thing
+ // to be checked.
+ mockConnectivityFailure(ConnectivityState.DISCONNECTED);
mServerInterface.fetchGeek(metrics);
assertWithMessage("Network transaction should not have proceeded.").fail();
} catch (RkpdException e) {
@@ -376,12 +391,16 @@
private void mockConnectivityFailure(ConnectivityState state) {
ConnectivityManager mockedConnectivityManager = Mockito.mock(ConnectivityManager.class);
- NetworkInfo mockedNetwork = Mockito.mock(NetworkInfo.class);
Mockito.when(sContext.getSystemService(ConnectivityManager.class))
.thenReturn(mockedConnectivityManager);
- Mockito.when(mockedConnectivityManager.getActiveNetworkInfo()).thenReturn(mockedNetwork);
- Mockito.when(mockedNetwork.isConnected()).thenReturn(state == ConnectivityState.CONNECTED);
+ NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ if (state == ConnectivityState.CONNECTED) {
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+ }
+ Mockito.when(mockedConnectivityManager.getNetworkCapabilities(Mockito.any()))
+ .thenReturn(builder.build());
}
private enum ConnectivityState {
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
index 92dc16f..c201e11 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
@@ -25,6 +25,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -53,8 +54,10 @@
import com.android.rkpdapp.metrics.ProvisioningAttempt;
import com.android.rkpdapp.utils.CborUtils;
+import com.google.common.collect.ImmutableMap;
import com.google.crypto.tink.subtle.Ed25519Sign;
+import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
@@ -93,6 +96,12 @@
Assume.assumeTrue(ServiceManager.isDeclared(SERVICE));
}
+ @After
+ public void cleanUp() {
+ ServiceManagerInterface.setBinders(null);
+ ServiceManagerInterface.setInstances(null);
+ }
+
@Test
public void testGetDeclaredInstances() {
SystemInterface[] instances = ServiceManagerInterface.getAllInstances();
@@ -109,8 +118,32 @@
try {
ServiceManagerInterface.getInstance("non-existent");
fail("Getting the declared service 'non-existent' should fail due to SEPolicy.");
- } catch (RuntimeException e) {
- assertThat(e).isInstanceOf(SecurityException.class);
+ } catch (IllegalArgumentException | SecurityException e) {
+ // Pre-android V, we'd get SecurityException. Post-V we get IllegalArgumentException
+ }
+ }
+
+ @Test
+ public void testGetAllInstancesWithUnsupportedService() throws RemoteException {
+ IRemotelyProvisionedComponent mockedBinder = mock(IRemotelyProvisionedComponent.class);
+ doThrow(new UnsupportedOperationException()).when(mockedBinder).getHardwareInfo();
+ ServiceManagerInterface.setBinders(ImmutableMap.of(SERVICE, mockedBinder));
+
+ SystemInterface[] instances = ServiceManagerInterface.getAllInstances();
+ assertThat(instances).isEmpty();
+ }
+
+ @Test
+ public void testGetInstanceWithUnsupportedService() throws RemoteException {
+ IRemotelyProvisionedComponent mockedBinder = mock(IRemotelyProvisionedComponent.class);
+ doThrow(new UnsupportedOperationException()).when(mockedBinder).getHardwareInfo();
+ ServiceManagerInterface.setBinders(ImmutableMap.of(SERVICE, mockedBinder));
+
+ try {
+ SystemInterface ignored = ServiceManagerInterface.getInstance(SERVICE);
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ // pass
}
}
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java b/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
index d478db7..bbce2d9 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
@@ -144,6 +144,12 @@
public static X509Certificate signPublicKey(KeyPair issuerKeyPair, PublicKey publicKeyToSign)
throws Exception {
+ return signPublicKey(issuerKeyPair, publicKeyToSign,
+ Instant.now().plus(Duration.ofDays(1)));
+ }
+
+ public static X509Certificate signPublicKey(KeyPair issuerKeyPair, PublicKey publicKeyToSign,
+ Instant expirationInstant) throws Exception {
X500Principal issuer = new X500Principal("CN=TEE");
BigInteger serial = BigInteger.ONE;
X500Principal subject = new X500Principal("CN=TEE");
@@ -153,7 +159,7 @@
certificateBuilder.setIssuerDN(issuer);
certificateBuilder.setSerialNumber(serial);
certificateBuilder.setNotBefore(Date.from(now));
- certificateBuilder.setNotAfter(Date.from(now.plus(Duration.ofDays(1))));
+ certificateBuilder.setNotAfter(Date.from(expirationInstant));
certificateBuilder.setSignatureAlgorithm("SHA256WITHECDSA");
certificateBuilder.setSubjectDN(subject);
certificateBuilder.setPublicKey(publicKeyToSign);
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
index d4f9d0a..83378e4 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
@@ -39,6 +39,9 @@
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
@RunWith(AndroidJUnit4.class)
public class X509UtilsTest {
@@ -133,6 +136,36 @@
assertThat(X509Utils.isCertChainValid(certChain)).isFalse();
}
+ @Test
+ public void testCertChainExpirationTimeWhenRootExpiresLater() throws Exception {
+ KeyPair root = generateEcdsaKeyPair();
+ KeyPair leaf = generateEcdsaKeyPair();
+ X509Certificate[] certs = new X509Certificate[2];
+
+ // root cert expires later.
+ certs[0] = signPublicKey(root, leaf.getPublic(), Instant.now().plus(Duration.ofDays(2)));
+ certs[1] = signPublicKey(root, root.getPublic(), Instant.now().plus(Duration.ofDays(1)));
+
+ Date expirationTime = X509Utils.getExpirationTimeForCertificateChain(certs);
+ assertThat(expirationTime).isEqualTo(certs[1].getNotAfter());
+ assertThat(expirationTime).isNotEqualTo(certs[0].getNotAfter());
+ }
+
+ @Test
+ public void testCertChainExpirationTimeWhenLeafExpiresLater() throws Exception {
+ KeyPair root = generateEcdsaKeyPair();
+ KeyPair leaf = generateEcdsaKeyPair();
+ X509Certificate[] certs = new X509Certificate[2];
+
+ // leaf cert expires later.
+ certs[0] = signPublicKey(root, leaf.getPublic(), Instant.now().plus(Duration.ofDays(1)));
+ certs[1] = signPublicKey(root, root.getPublic(), Instant.now().plus(Duration.ofDays(2)));
+
+ Date expirationTime = X509Utils.getExpirationTimeForCertificateChain(certs);
+ assertThat(expirationTime).isEqualTo(certs[0].getNotAfter());
+ assertThat(expirationTime).isNotEqualTo(certs[1].getNotAfter());
+ }
+
private X509Certificate generateCertificateFromEncodedBytes(String encodedCert)
throws CertificateException {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
diff --git a/app/tests/util/Android.bp b/app/tests/util/Android.bp
index 6b8daa8..c12f372 100644
--- a/app/tests/util/Android.bp
+++ b/app/tests/util/Android.bp
@@ -25,7 +25,7 @@
"androidx.room_room-runtime",
"libnanohttpd",
"libprotobuf-java-lite",
- "truth-prebuilt",
+ "truth",
],
plugins: [
"androidx.room_room-compiler-plugin",
diff --git a/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java b/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java
deleted file mode 100644
index 77a5d96..0000000
--- a/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2023 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.rkpdapp.testutil;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-
-public class NetworkUtils {
- public static boolean isNetworkConnected(Context context) {
- ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- if (networkInfo != null && networkInfo.isConnected()) {
- return true;
- }
- return false;
- }
-}
diff --git a/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java b/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java
new file mode 100644
index 0000000..0f49691
--- /dev/null
+++ b/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.rkpdapp.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import android.hardware.security.keymint.IRemotelyProvisionedComponent;
+
+import com.android.rkpdapp.interfaces.ServiceManagerInterface;
+import com.android.rkpdapp.interfaces.SystemInterface;
+
+public class SystemInterfaceSelector {
+ /**
+ * Gets the system interface object for provided service name.
+ */
+ public static SystemInterface getSystemInterfaceForServiceName(String serviceName) {
+ SystemInterface matchingInterface = null;
+ for (SystemInterface systemInterface: ServiceManagerInterface.getAllInstances()) {
+ if (systemInterface.getServiceName().equals(serviceName)) {
+ matchingInterface = systemInterface;
+ }
+ }
+ if (matchingInterface == null) {
+ assertThat(serviceName).isEqualTo(IRemotelyProvisionedComponent.DESCRIPTOR + "/avf");
+ assume().withMessage("AVF is not supported by this system").fail();
+ }
+ return matchingInterface;
+ }
+}
diff --git a/system-server/tests/unit/Android.bp b/system-server/tests/unit/Android.bp
index 3aac9e7..a44c774 100644
--- a/system-server/tests/unit/Android.bp
+++ b/system-server/tests/unit/Android.bp
@@ -24,7 +24,7 @@
"androidx.test.runner",
"mockito-target",
"service-rkp.impl",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.mock",
diff --git a/system-server/tests/unit/AndroidTest.xml b/system-server/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..517d827
--- /dev/null
+++ b/system-server/tests/unit/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?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="Unit tests for the rkpd system server.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="service-rkp-unittest.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.security.rkp.service.test" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+
+ <!-- Only run if RKPD mainline module is installed -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="enable" value="true" />
+ <option name="mainline-module-package-name" value="com.android.rkpd" />
+ <option name="mainline-module-package-name" value="com.google.android.rkpd" />
+ </object>
+</configuration>