From 1e5b402b24805c2d30336d7269ae8dd9df5d778e Mon Sep 17 00:00:00 2001 From: Alice Wang Date: Fri, 16 Feb 2024 16:03:55 +0000 Subject: [avf] Add e2e test to check AVF key provisioning This cl adds an e2e test that interacts with the real RKP server to ensure that the AVF keys are successfully provisioned on the device. The test is forked from RkpdAppIntegrationTests. The test target will be added to avf-presubmit later to ensure that it runs on a real arm64 device in pre-submit. Bug: 325610326 Test: atest AvfRkpdAppIntegrationTests Change-Id: Idb3a54bce46b3dde82df6a1bf11e110e1f033cec --- app/TEST_MAPPING | 7 + app/tests/avf/Android.bp | 29 +++++ app/tests/avf/AndroidManifest.xml | 26 ++++ app/tests/avf/AndroidTest.xml | 36 ++++++ .../avf/rkpdapp/e2etest/AvfIntegrationTest.java | 142 +++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 app/tests/avf/Android.bp create mode 100644 app/tests/avf/AndroidManifest.xml create mode 100644 app/tests/avf/AndroidTest.xml create mode 100644 app/tests/avf/src/com/android/avf/rkpdapp/e2etest/AvfIntegrationTest.java diff --git a/app/TEST_MAPPING b/app/TEST_MAPPING index 824fc9e..af4013a 100644 --- a/app/TEST_MAPPING +++ b/app/TEST_MAPPING @@ -15,6 +15,13 @@ "keywords": ["internal"] } ], + "avf-postsubmit": [ + { + // TODO(b/325610326): Add this target to presubmit once there is enough + // SLO data for it. + "name": "AvfRkpdAppIntegrationTests" + } + ], "mainline-presubmit": [ { "name": "RkpdAppGoogleUnitTests[com.google.android.rkpd.apex]" diff --git a/app/tests/avf/Android.bp b/app/tests/avf/Android.bp new file mode 100644 index 0000000..b32eed3 --- /dev/null +++ b/app/tests/avf/Android.bp @@ -0,0 +1,29 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "AvfRkpdAppIntegrationTests", + srcs: ["src/**/*.java"], + static_libs: [ + "MicrodroidDeviceTestHelper", + "Nene", + "RkpdAppTestUtil", + "androidx.test.ext.junit", + "androidx.test.core", + "androidx.test.rules", + "androidx.work_work-testing", + "compatibility-common-util-devicesidelib", + "platform-test-annotations", + "truth", + ], + platform_apis: true, + test_suites: [ + "general-tests", + "device-tests", + "mcts-rkpd", + "mts-rkpd", + ], + min_sdk_version: "33", + instrumentation_for: "rkpdapp", +} diff --git a/app/tests/avf/AndroidManifest.xml b/app/tests/avf/AndroidManifest.xml new file mode 100644 index 0000000..0924ded --- /dev/null +++ b/app/tests/avf/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/app/tests/avf/AndroidTest.xml b/app/tests/avf/AndroidTest.xml new file mode 100644 index 0000000..e57ca1d --- /dev/null +++ b/app/tests/avf/AndroidTest.xml @@ -0,0 +1,36 @@ + + + + diff --git a/app/tests/avf/src/com/android/avf/rkpdapp/e2etest/AvfIntegrationTest.java b/app/tests/avf/src/com/android/avf/rkpdapp/e2etest/AvfIntegrationTest.java new file mode 100644 index 0000000..46386a5 --- /dev/null +++ b/app/tests/avf/src/com/android/avf/rkpdapp/e2etest/AvfIntegrationTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 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.avf.rkpdapp.e2etest; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.TruthJUnit.assume; + +import android.hardware.security.keymint.IRemotelyProvisionedComponent; +import android.os.Process; +import android.os.SystemProperties; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.work.ListenableWorker; +import androidx.work.testing.TestWorkerBuilder; + +import com.android.compatibility.common.util.CddTest; +import com.android.microdroid.test.device.MicrodroidDeviceTestBase; +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.utils.Settings; +import com.android.rkpdapp.utils.X509Utils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.concurrent.Executors; + +/** + * End-to-end test for the pVM remote attestation (key provisioning/VM attestation). + * + *

To run this test, you need to: + * + * - Have an arm64 device supporting protected VMs. + * - Have a stable network connection on the device. + * - Have the RKP server hostname configured in the device. If not, you can set it using: + * $ adb shell setprop remote_provisioning.hostname remoteprovisioning.googleapis.com + */ +@RunWith(AndroidJUnit4.class) +public class AvfIntegrationTest extends MicrodroidDeviceTestBase { + private static final String SERVICE_NAME = IRemotelyProvisionedComponent.DESCRIPTOR + "/avf"; + + private ProvisionedKeyDao mKeyDao; + private PeriodicProvisioner mProvisioner; + + @Before + public void setUp() throws Exception { + assume().withMessage("AVF key provisioning is not supported on CF.") + .that(isCuttlefish()) + .isFalse(); + 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(getContext())) + .isTrue(); + + Settings.clearPreferences(getContext()); + mKeyDao = RkpdDatabase.getDatabase(getContext()).provisionedKeyDao(); + mKeyDao.deleteAllKeys(); + + mProvisioner = + TestWorkerBuilder.from( + getContext(), + PeriodicProvisioner.class, + Executors.newSingleThreadExecutor()) + .build(); + + SystemInterface systemInterface = + SystemInterfaceSelector.getSystemInterfaceForServiceName(SERVICE_NAME); + ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface}); + } + + @After + public void tearDown() throws Exception { + ServiceManagerInterface.setInstances(null); + if (mKeyDao != null) { + mKeyDao.deleteAllKeys(); + } + Settings.clearPreferences(getContext()); + } + + @Test + @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) + public void provisioningSucceeds() throws Exception { + assertWithMessage("There should be no keys in the database before provisioning") + .that(mKeyDao.getTotalKeysForIrpc(SERVICE_NAME)) + .isEqualTo(0); + + // Check provisioning succeeds. + assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success()); + int totalUnassignedKeys = mKeyDao.getTotalUnassignedKeysForIrpc(SERVICE_NAME); + assertWithMessage("There should be unassigned keys in the database after provisioning") + .that(totalUnassignedKeys) + .isGreaterThan(0); + + ProvisionedKey attestationKey = + mKeyDao.getKeyForClientAndIrpc(SERVICE_NAME, Process.SYSTEM_UID, Process.myUid()); + assertThat(attestationKey).isNull(); + // Assign a key to a new client. + attestationKey = + mKeyDao.getOrAssignKey( + SERVICE_NAME, Instant.now(), Process.SYSTEM_UID, Process.myUid()); + + // Assert. + assertThat(attestationKey).isNotNull(); + assertThat(attestationKey.irpcHal).isEqualTo(SERVICE_NAME); + assertWithMessage("One key should be assigned") + .that(mKeyDao.getTotalUnassignedKeysForIrpc(SERVICE_NAME)) + .isEqualTo(totalUnassignedKeys - 1); + + // Parsing the certificate chain successfully indicates that the chain is well-formed, + // each certificate is signed by the next one, and the root certificate is self-signed. + X509Certificate[] certs = X509Utils.formatX509Certs(attestationKey.certificateChain); + assertThat(certs.length).isGreaterThan(1); + assertThat(certs[0].getSubjectX500Principal().getName()).contains("O=AVF"); + } +} -- cgit v1.2.3