[test] Add e2e instrumentation test for VM attestation
This cl adds an e2e test to check the VM attestation with the
following steps:
- Provisioning a pair of mock keys in virtualizationservice.
- Requesting VM attestation from a running VM and tracing this
step.
- Validating the attestation result.
To facilitate the testing process, two new APIs have been added.
The first API allows for the provisioning of mock keys, while the
second API facilitates the request for attestation in testing mode.
The new test has been added to busytown config at cl/605577401.
Bug: 318333789
Test: atest VmAttestationTestApp
Change-Id: Icf9d39c704af316f7625cf9d057d255243eca445
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
new file mode 100644
index 0000000..8f5fb41
--- /dev/null
+++ b/service_vm/test_apk/Android.bp
@@ -0,0 +1,44 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "VmAttestationTestApp",
+ test_suites: [
+ "general-tests",
+ ],
+ srcs: ["src/java/**/*.java"],
+ static_libs: [
+ "MicrodroidDeviceTestHelper",
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+ "com.android.virt.vm_attestation.testservice-java",
+ "truth",
+ ],
+ jni_libs: ["libvm_attestation_test_payload"],
+ jni_uses_platform_apis: true,
+ use_embedded_native_libs: true,
+ sdk_version: "test_current",
+ compile_multilib: "first",
+}
+
+rust_defaults {
+ name: "vm_attestation_test_payload_defaults",
+ crate_name: "vm_attestation_test_payload",
+ defaults: ["avf_build_flags_rust"],
+ srcs: ["src/native/main.rs"],
+ prefer_rlib: true,
+ rustlibs: [
+ "com.android.virt.vm_attestation.testservice-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "libavflog",
+ "liblog_rust",
+ "libvm_payload_bindgen",
+ ],
+}
+
+rust_ffi {
+ name: "libvm_attestation_test_payload",
+ defaults: ["vm_attestation_test_payload_defaults"],
+}
diff --git a/service_vm/test_apk/AndroidManifest.xml b/service_vm/test_apk/AndroidManifest.xml
new file mode 100644
index 0000000..b998b7f
--- /dev/null
+++ b/service_vm/test_apk/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.virt.vm_attestation.testapp">
+ <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+ <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+
+ <application />
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.virt.vm_attestation.testapp"
+ android:label="Microdroid VM attestation" />
+</manifest>
diff --git a/service_vm/test_apk/AndroidTest.xml b/service_vm/test_apk/AndroidTest.xml
new file mode 100644
index 0000000..18b4e46
--- /dev/null
+++ b/service_vm/test_apk/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Microdroid VM remote attestation tests.">
+ <option name="config-descriptor:metadata" key="component" value="security" />
+ <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="test-file-name" value="VmAttestationTestApp.apk" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="mkdir -p /data/local/tmp/cts/microdroid" />
+ <option name="teardown-command" value="rm -rf /data/local/tmp/cts/microdroid" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.virt.vm_attestation.testapp" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="shell-timeout" value="300000" />
+ <option name="test-timeout" value="300000" />
+ </test>
+</configuration>
diff --git a/service_vm/test_apk/aidl/Android.bp b/service_vm/test_apk/aidl/Android.bp
new file mode 100644
index 0000000..3ecce46
--- /dev/null
+++ b/service_vm/test_apk/aidl/Android.bp
@@ -0,0 +1,18 @@
+package {
+ default_team: "trendy_team_virtualization",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+ name: "com.android.virt.vm_attestation.testservice",
+ srcs: ["com/android/virt/vm_attestation/testservice/**/*.aidl"],
+ unstable: true,
+ backend: {
+ java: {
+ gen_rpc: true,
+ },
+ rust: {
+ enabled: true,
+ },
+ },
+}
diff --git a/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
new file mode 100644
index 0000000..94a7b8d
--- /dev/null
+++ b/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.virt.vm_attestation.testservice;
+
+/** {@hide} */
+interface IAttestationService {
+ const int PORT = 5679;
+
+ /**
+ * Requests attestation for testing.
+ *
+ * A fake key pair should be provisioned with the call to
+ * {@link VirtualMachine#enableTestAttestation()} before calling this method.
+ *
+ * The attestation result will be cached in the VM and can be validated with
+ * {@link #validateAttestationResult}.
+ */
+ void requestAttestationForTesting();
+
+ /**
+ * Validates the attestation result returned by the last call to
+ * {@link #requestAttestationForTesting}.
+ */
+ void validateAttestationResult();
+}
diff --git a/service_vm/test_apk/assets/config.json b/service_vm/test_apk/assets/config.json
new file mode 100644
index 0000000..caae3ce
--- /dev/null
+++ b/service_vm/test_apk/assets/config.json
@@ -0,0 +1,10 @@
+{
+ "os": {
+ "name": "microdroid"
+ },
+ "task": {
+ "type": "microdroid_launcher",
+ "command": "libvm_attestation_test_payload.so"
+ },
+ "export_tombstones": true
+ }
\ No newline at end of file
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
new file mode 100644
index 0000000..80e88e9
--- /dev/null
+++ b/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
@@ -0,0 +1,93 @@
+/*
+ * 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.virt.vm_attestation.testapp;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineManager;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.virt.vm_attestation.testservice.IAttestationService;
+
+@RunWith(BlockJUnit4ClassRunner.class)
+public class VmAttestationTests extends MicrodroidDeviceTestBase {
+ private static final String TAG = "VmAttestationTest";
+ private static final String DEFAULT_CONFIG = "assets/config.json";
+
+ @Before
+ public void setup() throws IOException {
+ grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
+ grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+ // TODO(b/318333789): Test using GKI as guest kernel.
+ prepareTestSetup(true /* protectedVm */, null /* use microdroid kernel */);
+ setMaxPerformanceTaskProfile();
+ }
+
+ @Test
+ public void requestingAttestationSucceeds() throws Exception {
+ assume().withMessage("Remote attestation is not supported on CF.")
+ .that(isCuttlefish())
+ .isFalse();
+ assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+
+ VirtualMachineConfig.Builder builder =
+ newVmConfigBuilderWithPayloadConfig(DEFAULT_CONFIG)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .setVmOutputCaptured(true);
+ VirtualMachineConfig config = builder.build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("attestation_client", config);
+
+ vm.enableTestAttestation();
+ CompletableFuture<Exception> exception = new CompletableFuture<>();
+ CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+ VmEventListener listener =
+ new VmEventListener() {
+ @Override
+ public void onPayloadReady(VirtualMachine vm) {
+ payloadReady.complete(true);
+ try {
+ IAttestationService service =
+ IAttestationService.Stub.asInterface(
+ vm.connectToVsockServer(IAttestationService.PORT));
+ android.os.Trace.beginSection("runningVmRequestsAttestation");
+ service.requestAttestationForTesting();
+ android.os.Trace.endSection();
+ service.validateAttestationResult();
+ } catch (Exception e) {
+ exception.complete(e);
+ } finally {
+ forceStop(vm);
+ }
+ }
+ };
+
+ listener.runToFinish(TAG, vm);
+ assertThat(payloadReady.getNow(false)).isTrue();
+ assertThat(exception.getNow(null)).isNull();
+ }
+}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
new file mode 100644
index 0000000..26b8062
--- /dev/null
+++ b/service_vm/test_apk/src/native/main.rs
@@ -0,0 +1,271 @@
+// Copyright 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.
+
+//! Main executable of VM attestation for end-to-end testing.
+
+use anyhow::{anyhow, ensure, Result};
+use avflog::LogResult;
+use com_android_virt_vm_attestation_testservice::{
+ aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
+ BnAttestationService, IAttestationService, PORT,
+ },
+ binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+};
+use log::{error, info};
+use std::{
+ ffi::{c_void, CStr},
+ panic,
+ ptr::{self, NonNull},
+ result,
+ sync::{Arc, Mutex},
+};
+use vm_payload_bindgen::{
+ attestation_status_t, AIBinder, AVmAttestationResult, AVmAttestationResult_free,
+ AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
+ AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
+ AVmAttestationResult_sign, AVmPayload_notifyPayloadReady,
+ AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
+};
+
+/// Entry point of the Service VM client.
+#[allow(non_snake_case)]
+#[no_mangle]
+pub extern "C" fn AVmPayload_main() {
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_tag("service_vm_client")
+ .with_min_level(log::Level::Debug),
+ );
+ // Redirect panic messages to logcat.
+ panic::set_hook(Box::new(|panic_info| {
+ error!("{}", panic_info);
+ }));
+ if let Err(e) = try_main() {
+ error!("failed with {:?}", e);
+ std::process::exit(1);
+ }
+}
+
+fn try_main() -> Result<()> {
+ info!("Welcome to Service VM Client!");
+
+ let mut service = AttestationService::new_binder().as_binder();
+ let service = service.as_native_mut() as *mut AIBinder;
+ let param = ptr::null_mut();
+ // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
+ // is the same type as `sys::AIBinder`. It is safe for `on_ready` to be invoked at any time,
+ // with any parameter.
+ unsafe { AVmPayload_runVsockRpcServer(service, PORT.try_into()?, Some(on_ready), param) };
+}
+
+extern "C" fn on_ready(_param: *mut c_void) {
+ // SAFETY: It is safe to call `AVmPayload_notifyPayloadReady` at any time.
+ unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+struct AttestationService {
+ res: Arc<Mutex<Option<AttestationResult>>>,
+}
+
+impl Interface for AttestationService {}
+
+impl AttestationService {
+ fn new_binder() -> Strong<dyn IAttestationService> {
+ let res = Arc::new(Mutex::new(None));
+ BnAttestationService::new_binder(AttestationService { res }, BinderFeatures::default())
+ }
+}
+
+impl IAttestationService for AttestationService {
+ fn requestAttestationForTesting(&self) -> binder::Result<()> {
+ // The data below is only a placeholder generated randomly with urandom
+ let challenge = &[
+ 0x6c, 0xad, 0x52, 0x50, 0x15, 0xe7, 0xf4, 0x1d, 0xa5, 0x60, 0x7e, 0xd2, 0x7d, 0xf1,
+ 0x51, 0x67, 0xc3, 0x3e, 0x73, 0x9b, 0x30, 0xbd, 0x04, 0x20, 0x2e, 0xde, 0x3b, 0x1d,
+ 0xc8, 0x07, 0x11, 0x7b,
+ ];
+ let res = AttestationResult::request_attestation(challenge)
+ .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+ .with_log()
+ .or_service_specific_exception(-1)?;
+ *self.res.lock().unwrap() = Some(res);
+ Ok(())
+ }
+
+ fn validateAttestationResult(&self) -> binder::Result<()> {
+ // TODO(b/191073073): Returns the attestation result to the host for validation.
+ self.res.lock().unwrap().as_ref().unwrap().log().or_service_specific_exception(-1)
+ }
+}
+
+#[derive(Debug)]
+struct AttestationResult(NonNull<AVmAttestationResult>);
+
+// Safety: `AttestationResult` is not `Send` because it contains a raw pointer to a C struct.
+unsafe impl Send for AttestationResult {}
+
+impl AttestationResult {
+ fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+ let mut res: *mut AVmAttestationResult = ptr::null_mut();
+ // SAFETY: It is safe as we only read the challenge within its bounds and the
+ // function does not retain any reference to it.
+ let status = unsafe {
+ AVmPayload_requestAttestationForTesting(
+ challenge.as_ptr() as *const c_void,
+ challenge.len(),
+ &mut res,
+ )
+ };
+ if status == attestation_status_t::ATTESTATION_OK {
+ info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
+ let res = NonNull::new(res).expect("The attestation result is null");
+ Ok(Self(res))
+ } else {
+ Err(status)
+ }
+ }
+
+ fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
+ let num_certs = get_certificate_count(self.as_ref());
+ let mut certs = Vec::with_capacity(num_certs);
+ for i in 0..num_certs {
+ certs.push(get_certificate_at(self.as_ref(), i)?);
+ }
+ Ok(certs)
+ }
+
+ fn private_key(&self) -> Result<Box<[u8]>> {
+ get_private_key(self.as_ref())
+ }
+
+ fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
+ sign_with_attested_key(self.as_ref(), message)
+ }
+
+ fn log(&self) -> Result<()> {
+ let cert_chain = self.certificate_chain()?;
+ info!("Attestation result certificateChain = {:?}", cert_chain);
+
+ let private_key = self.private_key()?;
+ info!("Attestation result privateKey = {:?}", private_key);
+
+ let message = b"Hello from Service VM client";
+ info!("Signing message: {:?}", message);
+ let signature = self.sign(message)?;
+ info!("Signature: {:?}", signature);
+ Ok(())
+ }
+}
+
+impl AsRef<AVmAttestationResult> for AttestationResult {
+ fn as_ref(&self) -> &AVmAttestationResult {
+ // SAFETY: This field is private, and only populated with a successful call to
+ // `AVmPayload_requestAttestation`.
+ unsafe { self.0.as_ref() }
+ }
+}
+
+impl Drop for AttestationResult {
+ fn drop(&mut self) {
+ // SAFETY: This field is private, and only populated with a successful call to
+ // `AVmPayload_requestAttestation`, and not freed elsewhere.
+ unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
+ }
+}
+
+fn get_certificate_count(res: &AVmAttestationResult) -> usize {
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed.
+ unsafe { AVmAttestationResult_getCertificateCount(res) }
+}
+
+fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
+ let size =
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed.
+ unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
+ let mut cert = vec![0u8; size];
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed. This function only writes within the bounds of `cert`.
+ // And `cert` cannot overlap `res` because we just allocated it.
+ let size = unsafe {
+ AVmAttestationResult_getCertificateAt(
+ res,
+ index,
+ cert.as_mut_ptr() as *mut c_void,
+ cert.len(),
+ )
+ };
+ ensure!(size == cert.len());
+ Ok(cert.into_boxed_slice())
+}
+
+fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
+ let size =
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed.
+ unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
+ let mut private_key = vec![0u8; size];
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed. This function only writes within the bounds of `private_key`.
+ // And `private_key` cannot overlap `res` because we just allocated it.
+ let size = unsafe {
+ AVmAttestationResult_getPrivateKey(
+ res,
+ private_key.as_mut_ptr() as *mut c_void,
+ private_key.len(),
+ )
+ };
+ ensure!(size == private_key.len());
+ Ok(private_key.into_boxed_slice())
+}
+
+fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed.
+ let size = unsafe {
+ AVmAttestationResult_sign(
+ res,
+ message.as_ptr() as *const c_void,
+ message.len(),
+ ptr::null_mut(),
+ 0,
+ )
+ };
+ let mut signature = vec![0u8; size];
+ // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
+ // before getting freed. This function only writes within the bounds of `signature`.
+ // And `signature` cannot overlap `res` because we just allocated it.
+ let size = unsafe {
+ AVmAttestationResult_sign(
+ res,
+ message.as_ptr() as *const c_void,
+ message.len(),
+ signature.as_mut_ptr() as *mut c_void,
+ signature.len(),
+ )
+ };
+ ensure!(size <= signature.len());
+ signature.truncate(size);
+ Ok(signature.into_boxed_slice())
+}
+
+fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+ // SAFETY: The function only reads the given enum status and returns a pointer to a
+ // static string.
+ let message = unsafe { AVmAttestationResult_resultToString(status) };
+ // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+ // point to a valid C String that lives forever.
+ unsafe { CStr::from_ptr(message) }
+}