Merge "pvmfw: Handle dependent nodes in VM DTBO" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index ec9042c..5b0c000 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -62,6 +62,9 @@
       // TODO(b/325610326): Add this target to presubmit once there is enough
       // SLO data for it.
       "name": "AvfRkpdAppIntegrationTests"
+    },
+    {
+      "name": "AvfRkpdVmAttestationTestApp"
     }
   ],
   "postsubmit": [
diff --git a/compos/common/Android.bp b/compos/common/Android.bp
index 01ab7c9..72cb5e1 100644
--- a/compos/common/Android.bp
+++ b/compos/common/Android.bp
@@ -20,6 +20,7 @@
         "libnum_traits",
         "librustutils",
         "libvmclient",
+        "libplatformproperties_rust",
     ],
     proc_macros: ["libnum_derive"],
     apex_available: [
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index 077a0ef..ffdd0ea 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -35,6 +35,7 @@
 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
 use glob::glob;
 use log::{info, warn};
+use platformproperties::hypervisorproperties;
 use rustutils::system_properties;
 use std::fs::File;
 use std::path::{Path, PathBuf};
@@ -232,7 +233,7 @@
 
 fn want_protected_vm() -> Result<bool> {
     let have_protected_vm =
-        system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)?;
+        hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false);
     if have_protected_vm {
         info!("Starting protected VM");
         return Ok(true);
@@ -243,8 +244,7 @@
         bail!("Protected VM not supported, unable to start VM");
     }
 
-    let have_non_protected_vm =
-        system_properties::read_bool("ro.boot.hypervisor.vm.supported", false)?;
+    let have_non_protected_vm = hypervisorproperties::hypervisor_vm_supported()?.unwrap_or(false);
     if have_non_protected_vm {
         warn!("Protected VM not supported, falling back to non-protected on debuggable build");
         return Ok(false);
diff --git a/libs/hypervisor_props/Android.bp b/libs/hypervisor_props/Android.bp
index af08b01..af6d417 100644
--- a/libs/hypervisor_props/Android.bp
+++ b/libs/hypervisor_props/Android.bp
@@ -9,7 +9,7 @@
     edition: "2021",
     rustlibs: [
         "libanyhow",
-        "librustutils",
+        "libplatformproperties_rust",
     ],
     apex_available: [
         "com.android.compos",
diff --git a/libs/hypervisor_props/src/lib.rs b/libs/hypervisor_props/src/lib.rs
index 120a48c..14614fd 100644
--- a/libs/hypervisor_props/src/lib.rs
+++ b/libs/hypervisor_props/src/lib.rs
@@ -14,18 +14,17 @@
 
 //! Access to hypervisor capabilities via system properties set by the bootloader.
 
-use anyhow::{Error, Result};
-use rustutils::system_properties;
+use anyhow::Result;
+use platformproperties::hypervisorproperties;
 
 /// Returns whether there is a hypervisor present that supports non-protected VMs.
 pub fn is_vm_supported() -> Result<bool> {
-    system_properties::read_bool("ro.boot.hypervisor.vm.supported", false).map_err(Error::new)
+    Ok(hypervisorproperties::hypervisor_vm_supported()?.unwrap_or(false))
 }
 
 /// Returns whether there is a hypervisor present that supports protected VMs.
 pub fn is_protected_vm_supported() -> Result<bool> {
-    system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)
-        .map_err(Error::new)
+    Ok(hypervisorproperties::hypervisor_protected_vm_supported()?.unwrap_or(false))
 }
 
 /// Returns whether there is a hypervisor present that supports any sort of VM, either protected
@@ -36,5 +35,5 @@
 
 /// Returns the version of the hypervisor, if there is one.
 pub fn version() -> Result<Option<String>> {
-    system_properties::read("ro.boot.hypervisor.version").map_err(Error::new)
+    Ok(hypervisorproperties::hypervisor_version()?)
 }
diff --git a/microdroid/init_debug_policy/src/init_debug_policy.rs b/microdroid/init_debug_policy/src/init_debug_policy.rs
index 90d04ac..c443088 100644
--- a/microdroid/init_debug_policy/src/init_debug_policy.rs
+++ b/microdroid/init_debug_policy/src/init_debug_policy.rs
@@ -15,7 +15,7 @@
 //! Applies debug policies when booting microdroid
 
 use rustutils::system_properties;
-use rustutils::system_properties::PropertyWatcherError;
+use rustutils::system_properties::error::PropertyWatcherError;
 use std::fs::File;
 use std::io::Read;
 
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 8f5fb41..de731f6 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -2,12 +2,11 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test {
-    name: "VmAttestationTestApp",
+java_defaults {
+    name: "vm_attestation_testapp_defaults",
     test_suites: [
         "general-tests",
     ],
-    srcs: ["src/java/**/*.java"],
     static_libs: [
         "MicrodroidDeviceTestHelper",
         "androidx.test.runner",
@@ -19,7 +18,12 @@
     jni_uses_platform_apis: true,
     use_embedded_native_libs: true,
     sdk_version: "test_current",
-    compile_multilib: "first",
+}
+
+android_test {
+    name: "VmAttestationTestApp",
+    srcs: ["src/java/com/android/virt/vm_attestation/testapp/*.java"],
+    defaults: ["vm_attestation_testapp_defaults"],
 }
 
 rust_defaults {
@@ -41,4 +45,21 @@
 rust_ffi {
     name: "libvm_attestation_test_payload",
     defaults: ["vm_attestation_test_payload_defaults"],
+    visibility: [":__subpackages__"],
+}
+
+android_test {
+    name: "AvfRkpdVmAttestationTestApp",
+    srcs: ["src/java/com/android/virt/rkpd/vm_attestation/testapp/*.java"],
+    defaults: ["vm_attestation_testapp_defaults"],
+    manifest: "AndroidManifest.rkpd.xml",
+    test_config: "AndroidTest.rkpd.xml",
+    static_libs: [
+        "RkpdAppTestUtil",
+        "androidx.work_work-testing",
+    ],
+    instrumentation_for: "rkpdapp",
+    // This app is a variation of rkpdapp, with additional permissions to run
+    // a VM. It is defined in packages/modules/RemoteKeyProvisioning.
+    data: [":avf-rkpdapp"],
 }
diff --git a/service_vm/test_apk/AndroidManifest.rkpd.xml b/service_vm/test_apk/AndroidManifest.rkpd.xml
new file mode 100644
index 0000000..6ecc5a9
--- /dev/null
+++ b/service_vm/test_apk/AndroidManifest.rkpd.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.rkpd.vm_attestation.testapp">
+
+    <instrumentation
+            android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.rkpdapp"
+            android:label="AVF rkpd app integration tests" />
+</manifest>
diff --git a/service_vm/test_apk/AndroidTest.rkpd.xml b/service_vm/test_apk/AndroidTest.rkpd.xml
new file mode 100644
index 0000000..39eca32
--- /dev/null
+++ b/service_vm/test_apk/AndroidTest.rkpd.xml
@@ -0,0 +1,38 @@
+<?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="VM attestation integration tests with the rkpd app.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <!-- Need to disable SELinux policy to allow com.android.rkpdapp to run a VM. -->
+    <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="AvfRkpdVmAttestationTestApp.apk" />
+        <option name="test-file-name" value="avf-rkpdapp.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.virt.rkpd.vm_attestation.testapp" />
+    </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/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
index 94a7b8d..34c8549 100644
--- 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
@@ -21,7 +21,31 @@
     const int PORT = 5679;
 
     /**
-     * Requests attestation for testing.
+     * The result of signing a message with the attested key.
+     */
+    parcelable SigningResult {
+        /** The DER-encoded ECDSA signature of the message. */
+        byte[] signature;
+
+        /** The DER-encoded attestation X509 certificate chain. */
+        byte[] certificateChain;
+    }
+
+    /**
+     * Requests attestation with {@link AVmPayload_requestAttestation} API and signs the
+     * given message with the attested key.
+     *
+     * The remotely provisioned keys are retrieved from RKPD and are provisioned from the
+     * real RKP server.
+     *
+     * @param challenge the challenge to include in the attestation output.
+     * @param message the message to sign.
+     * @return the result of signing the message with the attested key.
+     */
+    SigningResult signWithAttestationKey(in byte[] challenge, in byte[] message);
+
+    /**
+     * Requests attestation for testing with {@link AVmPayload_requestAttestationForTesting} API.
      *
      * A fake key pair should be provisioned with the call to
      * {@link VirtualMachine#enableTestAttestation()} before calling this method.
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
new file mode 100644
index 0000000..e7061e1
--- /dev/null
+++ b/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.rkpd.vm_attestation.testapp;
+
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+
+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.content.Context;
+import android.hardware.security.keymint.IRemotelyProvisionedComponent;
+import android.os.SystemProperties;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+
+import androidx.work.ListenableWorker;
+import androidx.work.testing.TestWorkerBuilder;
+
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+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 com.android.virt.vm_attestation.testservice.IAttestationService;
+import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+
+/**
+ * End-to-end test for the pVM remote attestation.
+ *
+ * <p>The test checks the two major steps of the pVM remote attestation:
+ *
+ * <p>1. Key provisioning: The test provisions AVF keys from the RKP server and verifies that the
+ * keys are for AVF.
+ *
+ * <p>2. VM attestation: The test creates a VM with a payload binary that requests to attest the VM,
+ * and then signs a message with the attestation key.
+ *
+ * <p>To run this test, you need to:
+ *
+ * <p>- Have an arm64 device supporting protected VMs.
+ *
+ * <p>- Have a stable network connection on the device.
+ *
+ * <p>- 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(Parameterized.class)
+public class RkpdVmAttestationTest extends MicrodroidDeviceTestBase {
+    private static final String TAG = "RkpdVmAttestationTest";
+    private static final String AVF_ATTESTATION_EXTENSION_OID = "1.3.6.1.4.1.11129.2.1.29.1";
+    private static final String SERVICE_NAME = IRemotelyProvisionedComponent.DESCRIPTOR + "/avf";
+    private static final String VM_PAYLOAD_PATH = "libvm_attestation_test_payload.so";
+    private static final String MESSAGE = "Hello RKP from AVF!";
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.virt.rkpd.vm_attestation.testapp";
+
+    private ProvisionedKeyDao mKeyDao;
+    private PeriodicProvisioner mProvisioner;
+
+    @Parameterized.Parameter(0)
+    public String mGki;
+
+    @Parameterized.Parameters(name = "gki={0}")
+    public static Collection<Object[]> params() {
+        List<Object[]> ret = new ArrayList<>();
+        ret.add(new Object[] {null /* use microdroid kernel */});
+        for (String gki : SUPPORTED_GKI_VERSIONS) {
+            ret.add(new Object[] {gki});
+        }
+        return ret;
+    }
+
+    @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(getContext()))
+                .isTrue();
+        // TODO(b/329652894): Assume that pVM remote attestation feature is supported.
+
+        prepareTestSetup(true /* protectedVm */, mGki);
+
+        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});
+
+        setMaxPerformanceTaskProfile();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        ServiceManagerInterface.setInstances(null);
+        if (mKeyDao != null) {
+            mKeyDao.deleteAllKeys();
+        }
+        Settings.clearPreferences(getContext());
+    }
+
+    @Test
+    public void usingProvisionedKeyForVmAttestationSucceeds() throws Exception {
+        // Provision keys.
+        assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
+        assertThat(mKeyDao.getTotalUnassignedKeysForIrpc(SERVICE_NAME)).isGreaterThan(0);
+
+        // Arrange.
+        Context ctx = getContext();
+        Context otherAppCtx = ctx.createPackageContext(TEST_APP_PACKAGE_NAME, 0);
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setProtectedVm(true)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setPayloadBinaryName(VM_PAYLOAD_PATH)
+                        .setVmOutputCaptured(true)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("attestation_with_rkpd_client", config);
+        byte[] challenge = new byte[32];
+        Arrays.fill(challenge, (byte) 0xab);
+
+        // Act.
+        CompletableFuture<Exception> exception = new CompletableFuture<>();
+        CompletableFuture<Boolean> payloadReady = new CompletableFuture<>();
+        CompletableFuture<SigningResult> signingResultFuture = 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));
+                            signingResultFuture.complete(
+                                    service.signWithAttestationKey(challenge, MESSAGE.getBytes()));
+                        } catch (Exception e) {
+                            exception.complete(e);
+                        } finally {
+                            forceStop(vm);
+                        }
+                    }
+                };
+        listener.runToFinish(TAG, vm);
+
+        // Assert.
+        assertThat(payloadReady.getNow(false)).isTrue();
+        assertThat(exception.getNow(null)).isNull();
+        SigningResult signingResult = signingResultFuture.getNow(null);
+        assertThat(signingResult).isNotNull();
+
+        // Parsing the certificate chain successfully indicates that the certificate
+        // chain is valid, that each certificate is signed by the next one and the last
+        // one is self-signed.
+        X509Certificate[] certs = X509Utils.formatX509Certs(signingResult.certificateChain);
+        assertThat(certs.length).isGreaterThan(2);
+        assertWithMessage("The first certificate should be generated in the RKP VM")
+                .that(certs[0].getSubjectX500Principal().getName())
+                .isEqualTo("CN=Android Protected Virtual Machine Key");
+        checkAvfAttestationExtension(certs[0], challenge);
+        assertWithMessage("The second certificate should contain AVF in the subject")
+                .that(certs[1].getSubjectX500Principal().getName())
+                .contains("O=AVF");
+
+        // Verify the signature using the public key from the leaf certificate generated
+        // in the RKP VM.
+        Signature sig = Signature.getInstance("SHA256withECDSA");
+        sig.initVerify(certs[0].getPublicKey());
+        sig.update(MESSAGE.getBytes());
+        assertThat(sig.verify(signingResult.signature)).isTrue();
+    }
+
+    private void checkAvfAttestationExtension(X509Certificate cert, byte[] challenge) {
+        byte[] extension = cert.getExtensionValue(AVF_ATTESTATION_EXTENSION_OID);
+        assertThat(extension).isNotNull();
+        // TODO(b/325610326): Use bouncycastle to parse the extension and check other fields.
+        assertWithMessage("The extension should contain the challenge")
+                .that(containsSubarray(extension, challenge))
+                .isTrue();
+    }
+
+    private boolean containsSubarray(byte[] array, byte[] subarray) {
+        for (int i = 0; i < array.length - subarray.length + 1; i++) {
+            boolean found = true;
+            for (int j = 0; j < subarray.length; j++) {
+                if (array[i + j] != subarray[j]) {
+                    found = false;
+                    break;
+                }
+            }
+            if (found) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 199b45c..a04fb1f 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -18,7 +18,7 @@
 use avflog::LogResult;
 use com_android_virt_vm_attestation_testservice::{
     aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
-        BnAttestationService, IAttestationService, PORT,
+        BnAttestationService, IAttestationService, SigningResult::SigningResult, PORT,
     },
     binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
 };
@@ -34,7 +34,7 @@
     AIBinder, AVmAttestationResult, AVmAttestationResult_free,
     AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
     AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
-    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady,
+    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
     AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
 };
 
@@ -89,13 +89,8 @@
 
 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)
+        const CHALLENGE: &[u8] = &[0xaa; 32];
+        let res = AttestationResult::request_attestation_for_testing(CHALLENGE)
             .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
             .with_log()
             .or_service_specific_exception(-1)?;
@@ -103,6 +98,21 @@
         Ok(())
     }
 
+    fn signWithAttestationKey(
+        &self,
+        challenge: &[u8],
+        message: &[u8],
+    ) -> binder::Result<SigningResult> {
+        let res = AttestationResult::request_attestation(challenge)
+            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+            .with_log()
+            .or_service_specific_exception(-1)?;
+        let certificate_chain =
+            res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+        let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
+        Ok(SigningResult { certificateChain: certificate_chain, signature })
+    }
+
     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)
@@ -116,7 +126,9 @@
 unsafe impl Send for AttestationResult {}
 
 impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
+    fn request_attestation_for_testing(
+        challenge: &[u8],
+    ) -> result::Result<Self, AVmAttestationStatus> {
         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.
@@ -136,11 +148,31 @@
         }
     }
 
-    fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
+    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
+        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_requestAttestation(
+                challenge.as_ptr() as *const c_void,
+                challenge.len(),
+                &mut res,
+            )
+        };
+        if status == AVmAttestationStatus::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<u8>> {
         let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::with_capacity(num_certs);
+        let mut certs = Vec::new();
         for i in 0..num_certs {
-            certs.push(get_certificate_at(self.as_ref(), i)?);
+            certs.extend(get_certificate_at(self.as_ref(), i)?.iter());
         }
         Ok(certs)
     }
@@ -149,7 +181,7 @@
         get_private_key(self.as_ref())
     }
 
-    fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
+    fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
         sign_with_attested_key(self.as_ref(), message)
     }
 
@@ -231,7 +263,7 @@
     Ok(private_key.into_boxed_slice())
 }
 
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
+fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Vec<u8>> {
     // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
     // before getting freed.
     let size = unsafe {
@@ -258,7 +290,7 @@
     };
     ensure!(size <= signature.len());
     signature.truncate(size);
-    Ok(signature.into_boxed_slice())
+    Ok(signature)
 }
 
 fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 2a04103..732be94 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -17,6 +17,7 @@
     name: "MicrodroidTestAppsDefaults",
     test_suites: [
         "cts",
+        "vts",
         "general-tests",
     ],
     static_libs: [
diff --git a/tests/testapk/AndroidTest.xml b/tests/testapk/AndroidTest.xml
index 8a4c367..22cd0dc 100644
--- a/tests/testapk/AndroidTest.xml
+++ b/tests/testapk/AndroidTest.xml
@@ -15,6 +15,7 @@
 -->
 <configuration description="Runs Microdroid device-side tests.">
     <option name="test-suite-tag" value="cts" />
+    <option name="test-suite-tag" value="vts" />
     <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" />