[attestation] Validate vendor module loaded by client VM in RKP VM

This cl added the following tasks to the RKP VM:

- Parses a client VM DICE chain containing an additional vendor
module entry.
- Validates the code hash in the vendor module DICE entry against
the code hash read from the device tree.

The cl also adds a CTS test that triggers the VM attestation from
a VM with vendor module.

Bug: 330678211
Test: atest MicrodroidTests
Change-Id: Id56c6edd8baa32bae6a8ad7b5bca7b18ce167022
diff --git a/rialto/src/fdt.rs b/rialto/src/fdt.rs
index 09cdd36..b220f41 100644
--- a/rialto/src/fdt.rs
+++ b/rialto/src/fdt.rs
@@ -24,3 +24,8 @@
     let node = node.next_compatible(cstr!("google,open-dice"))?.ok_or(FdtError::NotFound)?;
     node.first_reg()?.try_into()
 }
+
+pub(crate) fn read_vendor_hashtree_root_digest(fdt: &Fdt) -> libfdt::Result<Option<&[u8]>> {
+    let node = fdt.node(cstr!("/avf"))?.ok_or(FdtError::NotFound)?;
+    node.getprop(cstr!("vendor_hashtree_descriptor_root_digest"))
+}
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index 48b69b3..025edff 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -26,7 +26,7 @@
 
 use crate::communication::VsockStream;
 use crate::error::{Error, Result};
-use crate::fdt::read_dice_range_from;
+use crate::fdt::{read_dice_range_from, read_vendor_hashtree_root_digest};
 use alloc::boxed::Box;
 use bssl_sys::CRYPTO_library_init;
 use ciborium_io::Write;
@@ -39,7 +39,7 @@
 use log::{debug, error, info};
 use service_vm_comm::{ServiceVmRequest, VmType};
 use service_vm_fake_chain::service_vm;
-use service_vm_requests::process_request;
+use service_vm_requests::{process_request, RequestContext};
 use virtio_drivers::{
     device::socket::{VsockAddr, VMADDR_CID_HOST},
     transport::{pci::bus::PciRoot, DeviceType, Transport},
@@ -174,11 +174,14 @@
     debug!("PCI root: {pci_root:#x?}");
     let socket_device = find_socket_device::<HalImpl>(&mut pci_root)?;
     debug!("Found socket device: guest cid = {:?}", socket_device.guest_cid());
+    let vendor_hashtree_root_digest = read_vendor_hashtree_root_digest(fdt)?;
+    let request_context =
+        RequestContext { dice_artifacts: bcc_handover.as_ref(), vendor_hashtree_root_digest };
 
     let mut vsock_stream = VsockStream::new(socket_device, host_addr())?;
     while let ServiceVmRequest::Process(req) = vsock_stream.read_request()? {
         info!("Received request: {}", req.name());
-        let response = process_request(req, bcc_handover.as_ref());
+        let response = process_request(req, &request_context);
         info!("Sending response: {}", response.name());
         vsock_stream.write_response(&response)?;
         vsock_stream.flush()?;
diff --git a/service_vm/comm/src/message.rs b/service_vm/comm/src/message.rs
index 9f83b78..c9aa711 100644
--- a/service_vm/comm/src/message.rs
+++ b/service_vm/comm/src/message.rs
@@ -158,6 +158,12 @@
 
     /// The DICE chain from the client VM is invalid.
     InvalidDiceChain,
+
+    /// Cannot find the vendor hash tree root digest in the device tree.
+    NoVendorHashTreeRootDigestInDT,
+
+    /// The vendor partition loaded by the client VM is invalid.
+    InvalidVendorPartition,
 }
 
 impl fmt::Display for RequestProcessingError {
@@ -186,6 +192,12 @@
             Self::InvalidDiceChain => {
                 write!(f, "The DICE chain from the client VM is invalid")
             }
+            Self::NoVendorHashTreeRootDigestInDT => {
+                write!(f, "Cannot find the vendor hash tree root digest in the device tree")
+            }
+            Self::InvalidVendorPartition => {
+                write!(f, "The vendor partition loaded by the client VM is invalid")
+            }
         }
     }
 }
diff --git a/service_vm/requests/src/api.rs b/service_vm/requests/src/api.rs
index 315d2af..9eca20f 100644
--- a/service_vm/requests/src/api.rs
+++ b/service_vm/requests/src/api.rs
@@ -21,22 +21,38 @@
 use service_vm_comm::{Request, Response};
 
 /// Processes a request and returns the corresponding response.
-/// This function serves as the entry point for the request processing
-/// module.
-pub fn process_request(request: Request, dice_artifacts: &dyn DiceArtifacts) -> Response {
+/// This function serves as the entry point for the request processing module.
+pub fn process_request(request: Request, context: &RequestContext) -> Response {
     match request {
         Request::Reverse(v) => Response::Reverse(reverse(v)),
-        Request::GenerateEcdsaP256KeyPair => rkp::generate_ecdsa_p256_key_pair(dice_artifacts)
-            .map_or_else(Response::Err, Response::GenerateEcdsaP256KeyPair),
+        Request::GenerateEcdsaP256KeyPair => {
+            rkp::generate_ecdsa_p256_key_pair(context.dice_artifacts)
+                .map_or_else(Response::Err, Response::GenerateEcdsaP256KeyPair)
+        }
         Request::GenerateCertificateRequest(p) => {
-            rkp::generate_certificate_request(p, dice_artifacts)
+            rkp::generate_certificate_request(p, context.dice_artifacts)
                 .map_or_else(Response::Err, Response::GenerateCertificateRequest)
         }
-        Request::RequestClientVmAttestation(p) => client_vm::request_attestation(p, dice_artifacts)
-            .map_or_else(Response::Err, Response::RequestClientVmAttestation),
+        Request::RequestClientVmAttestation(p) => client_vm::request_attestation(
+            p,
+            context.dice_artifacts,
+            context.vendor_hashtree_root_digest,
+        )
+        .map_or_else(Response::Err, Response::RequestClientVmAttestation),
     }
 }
 
+/// The context for the request processing.
+///
+/// This struct contains the reference data used during the request processing.
+pub struct RequestContext<'a> {
+    /// The reference DICE artifacts.
+    pub dice_artifacts: &'a dyn DiceArtifacts,
+
+    /// The reference hash tree root digest of the vendor partition if exists.
+    pub vendor_hashtree_root_digest: Option<&'a [u8]>,
+}
+
 fn reverse(payload: Vec<u8>) -> Vec<u8> {
     payload.into_iter().rev().collect()
 }
diff --git a/service_vm/requests/src/client_vm.rs b/service_vm/requests/src/client_vm.rs
index 15a3bd0..d2e674b 100644
--- a/service_vm/requests/src/client_vm.rs
+++ b/service_vm/requests/src/client_vm.rs
@@ -16,9 +16,7 @@
 //! client VM.
 
 use crate::cert;
-use crate::dice::{
-    validate_client_vm_dice_chain_prefix_match, ClientVmDiceChain, DiceChainEntryPayload,
-};
+use crate::dice::{ClientVmDiceChain, DiceChainEntryPayload};
 use crate::keyblob::decrypt_private_key;
 use alloc::vec::Vec;
 use bssl_avf::{rand_bytes, sha256, Digester, EcKey, PKey};
@@ -28,7 +26,7 @@
 use coset::{AsCborValue, CborSerializable, CoseSign, CoseSign1};
 use der::{Decode, Encode};
 use diced_open_dice::{DiceArtifacts, HASH_SIZE};
-use log::{error, info};
+use log::{debug, error, info};
 use microdroid_kernel_hashes::{HASH_SIZE as KERNEL_HASH_SIZE, OS_HASHES};
 use service_vm_comm::{ClientVmAttestationParams, Csr, CsrPayload, RequestProcessingError};
 use x509_cert::{certificate::Certificate, name::Name};
@@ -41,6 +39,7 @@
 pub(super) fn request_attestation(
     params: ClientVmAttestationParams,
     dice_artifacts: &dyn DiceArtifacts,
+    vendor_hashtree_root_digest_from_dt: Option<&[u8]>,
 ) -> Result<Vec<u8>> {
     let csr = Csr::from_cbor_slice(&params.csr)?;
     let cose_sign = CoseSign::from_slice(&csr.signed_csr_payload)?;
@@ -50,22 +49,11 @@
     })?;
     let csr_payload = CsrPayload::from_cbor_slice(csr_payload)?;
 
-    // Validates the prefix of the Client VM DICE chain in the CSR.
-    let service_vm_dice_chain =
-        dice_artifacts.bcc().ok_or(RequestProcessingError::MissingDiceChain)?;
-    let service_vm_dice_chain = parse_value_array(service_vm_dice_chain, "service_vm_dice_chain")?;
-    let client_vm_dice_chain = parse_value_array(&csr.dice_cert_chain, "client_vm_dice_chain")?;
-    validate_client_vm_dice_chain_prefix_match(&client_vm_dice_chain, &service_vm_dice_chain)?;
-    // Validates the signatures in the Client VM DICE chain and extracts the partially decoded
-    // DiceChainEntryPayloads.
-    let client_vm_dice_chain =
-        ClientVmDiceChain::validate_signatures_and_parse_dice_chain(client_vm_dice_chain)?;
-
-    // The last entry in the service VM DICE chain describes the service VM, which should
-    // be signed with the same key as the kernel image.
-    let service_vm_entry = service_vm_dice_chain.last().unwrap();
-    validate_kernel_authority_hash(client_vm_dice_chain.microdroid_kernel(), service_vm_entry)?;
-    validate_kernel_code_hash(&client_vm_dice_chain)?;
+    let client_vm_dice_chain = validate_client_vm_dice_chain(
+        &csr.dice_cert_chain,
+        dice_artifacts.bcc().ok_or(RequestProcessingError::MissingDiceChain)?,
+        vendor_hashtree_root_digest_from_dt,
+    )?;
 
     // AAD is empty as defined in service_vm/comm/client_vm_csr.cddl.
     let aad = &[];
@@ -140,6 +128,83 @@
     key.ecdsa_sign(&digest)
 }
 
+fn validate_service_vm_dice_chain_length(service_vm_dice_chain: &[Value]) -> Result<()> {
+    if service_vm_dice_chain.len() < 3 {
+        // The service VM's DICE chain must contain the root key and at least two other entries
+        // that describe:
+        //   - pvmfw
+        //   - Service VM kernel
+        error!(
+            "The service VM DICE chain must contain at least three entries. Got '{}' entries",
+            service_vm_dice_chain.len()
+        );
+        return Err(RequestProcessingError::InternalError);
+    }
+    Ok(())
+}
+
+/// Validates the client VM DICE chain against the reference service VM DICE chain and
+/// the reference `vendor_hashtree_root_digest`.
+///
+/// Returns the valid `ClientVmDiceChain` if the validation succeeds.
+fn validate_client_vm_dice_chain(
+    client_vm_dice_chain: &[u8],
+    service_vm_dice_chain: &[u8],
+    vendor_hashtree_root_digest: Option<&[u8]>,
+) -> Result<ClientVmDiceChain> {
+    let service_vm_dice_chain = parse_value_array(service_vm_dice_chain, "service_vm_dice_chain")?;
+    validate_service_vm_dice_chain_length(&service_vm_dice_chain)?;
+
+    let client_vm_dice_chain = parse_value_array(client_vm_dice_chain, "client_vm_dice_chain")?;
+    validate_client_vm_dice_chain_prefix_match(&client_vm_dice_chain, &service_vm_dice_chain)?;
+
+    // Validates the signatures in the Client VM DICE chain and extracts the partially decoded
+    // DiceChainEntryPayloads.
+    let client_vm_dice_chain = ClientVmDiceChain::validate_signatures_and_parse_dice_chain(
+        client_vm_dice_chain,
+        service_vm_dice_chain.len(),
+    )?;
+    validate_vendor_partition_code_hash_if_exists(
+        &client_vm_dice_chain,
+        vendor_hashtree_root_digest,
+    )?;
+
+    // The last entry in the service VM DICE chain describes the service VM, which should
+    // be signed with the same key as the kernel image.
+    let service_vm_entry = service_vm_dice_chain.last().unwrap();
+    validate_kernel_authority_hash(client_vm_dice_chain.microdroid_kernel(), service_vm_entry)?;
+    validate_kernel_code_hash(&client_vm_dice_chain)?;
+
+    info!("The client VM DICE chain validation succeeded");
+    Ok(client_vm_dice_chain)
+}
+
+fn validate_vendor_partition_code_hash_if_exists(
+    client_vm_dice_chain: &ClientVmDiceChain,
+    vendor_hashtree_root_digest: Option<&[u8]>,
+) -> Result<()> {
+    let Some(vendor_partition) = client_vm_dice_chain.vendor_partition() else {
+        debug!("The vendor partition is not present in the Client VM DICE chain");
+        return Ok(());
+    };
+    let Some(expected_root_digest) = vendor_hashtree_root_digest else {
+        error!(
+            "The vendor partition is present in the DICE chain, \
+             but the vendor_hashtree_root_digest is not provided in the DT"
+        );
+        return Err(RequestProcessingError::NoVendorHashTreeRootDigestInDT);
+    };
+    if Digester::sha512().digest(expected_root_digest)? == vendor_partition.code_hash {
+        Ok(())
+    } else {
+        error!(
+            "The vendor partition code hash in the Client VM DICE chain does \
+             not match the expected value from the DT"
+        );
+        Err(RequestProcessingError::InvalidVendorPartition)
+    }
+}
+
 /// Validates that the authority hash of the Microdroid kernel in the Client VM DICE chain
 /// matches the authority hash of the service VM entry in the service VM DICE chain, because
 /// the Microdroid kernel is signed with the same key as the one used for the service VM.
@@ -198,3 +263,20 @@
     let service_vm = DiceChainEntryPayload::from_slice(&payload)?;
     Ok(service_vm.authority_hash)
 }
+
+fn validate_client_vm_dice_chain_prefix_match(
+    client_vm_dice_chain: &[Value],
+    service_vm_dice_chain: &[Value],
+) -> Result<()> {
+    // Ignores the last entry that describes service VM
+    let entries_up_to_pvmfw = &service_vm_dice_chain[0..(service_vm_dice_chain.len() - 1)];
+    if client_vm_dice_chain.get(0..entries_up_to_pvmfw.len()) == Some(entries_up_to_pvmfw) {
+        Ok(())
+    } else {
+        error!(
+            "The client VM's DICE chain does not match service VM's DICE chain up to \
+             the pvmfw entry"
+        );
+        Err(RequestProcessingError::InvalidDiceChain)
+    }
+}
diff --git a/service_vm/requests/src/dice.rs b/service_vm/requests/src/dice.rs
index 657e482..df29676 100644
--- a/service_vm/requests/src/dice.rs
+++ b/service_vm/requests/src/dice.rs
@@ -31,7 +31,7 @@
     Label,
 };
 use diced_open_dice::{DiceMode, HASH_SIZE};
-use log::error;
+use log::{debug, error, info};
 use service_vm_comm::RequestProcessingError;
 
 type Result<T> = result::Result<T, RequestProcessingError>;
@@ -50,7 +50,8 @@
 const SUB_COMPONENT_CODE_HASH: i64 = 3;
 const SUB_COMPONENT_AUTHORITY_HASH: i64 = 4;
 
-const MICRODROID_KERNEL_COMPONENT_NAME: &str = "vm_entry";
+const KERNEL_COMPONENT_NAME: &str = "vm_entry";
+const VENDOR_PARTITION_COMPONENT_NAME: &str = "Microdroid vendor";
 const MICRODROID_PAYLOAD_COMPONENT_NAME: &str = "Microdroid payload";
 
 /// Represents a partially decoded `DiceCertChain` from the client VM.
@@ -63,6 +64,10 @@
 #[derive(Debug, Clone)]
 pub(crate) struct ClientVmDiceChain {
     payloads: Vec<DiceChainEntryPayload>,
+    /// The index of the vendor partition entry in the DICE chain if it exists.
+    vendor_partition_index: Option<usize>,
+    /// The index of the kernel entry in the DICE chain.
+    kernel_index: usize,
 }
 
 impl ClientVmDiceChain {
@@ -75,7 +80,11 @@
     /// Returns a partially decoded client VM's DICE chain if the verification succeeds.
     pub(crate) fn validate_signatures_and_parse_dice_chain(
         mut client_vm_dice_chain: Vec<Value>,
+        service_vm_dice_chain_len: usize,
     ) -> Result<Self> {
+        let has_vendor_partition =
+            vendor_partition_exists(client_vm_dice_chain.len(), service_vm_dice_chain_len)?;
+
         let root_public_key =
             CoseKey::from_cbor_value(client_vm_dice_chain.remove(0))?.try_into()?;
 
@@ -93,45 +102,62 @@
             payloads.push(payload);
             previous_public_key = &payloads.last().unwrap().subject_public_key;
         }
-        // After successfully calling `validate_client_vm_dice_chain_prefix_match`, we can be
-        // certain that the client VM's DICE chain must contain at least three entries that
-        // describe:
-        // - pvmfw
-        // - Microdroid kernel
-        // - Apk/Apexes
-        assert!(
-            payloads.len() >= 3,
-            "The client VM DICE chain must contain at least three DiceChainEntryPayloads"
-        );
-        let chain = Self { payloads };
-        chain.validate_microdroid_components_names()?;
-        Ok(chain)
+
+        Self::build(payloads, has_vendor_partition)
     }
 
-    fn validate_microdroid_components_names(&self) -> Result<()> {
-        let microdroid_kernel_name = &self.microdroid_kernel().config_descriptor.component_name;
-        if MICRODROID_KERNEL_COMPONENT_NAME != microdroid_kernel_name {
-            error!(
-                "The second to last entry in the client VM DICE chain must describe the \
-                    Microdroid kernel. Got {}",
-                microdroid_kernel_name
-            );
-            return Err(RequestProcessingError::InvalidDiceChain);
-        }
-        let microdroid_payload_name = &self.microdroid_payload().config_descriptor.component_name;
+    fn build(
+        dice_entry_payloads: Vec<DiceChainEntryPayload>,
+        has_vendor_partition: bool,
+    ) -> Result<Self> {
+        let microdroid_payload_name =
+            &dice_entry_payloads[dice_entry_payloads.len() - 1].config_descriptor.component_name;
         if MICRODROID_PAYLOAD_COMPONENT_NAME != microdroid_payload_name {
             error!(
                 "The last entry in the client VM DICE chain must describe the Microdroid \
-                    payload. Got {}",
+                 payload. Got '{}'",
                 microdroid_payload_name
             );
             return Err(RequestProcessingError::InvalidDiceChain);
         }
-        Ok(())
+
+        let (vendor_partition_index, kernel_index) = if has_vendor_partition {
+            let index = dice_entry_payloads.len() - 2;
+            let vendor_partition_name =
+                &dice_entry_payloads[index].config_descriptor.component_name;
+            if VENDOR_PARTITION_COMPONENT_NAME != vendor_partition_name {
+                error!(
+                    "The vendor partition entry in the client VM DICE chain must describe the \
+                        vendor partition. Got '{}'",
+                    vendor_partition_name,
+                );
+                return Err(RequestProcessingError::InvalidDiceChain);
+            }
+            (Some(index), index - 1)
+        } else {
+            (None, dice_entry_payloads.len() - 2)
+        };
+
+        let kernel_name = &dice_entry_payloads[kernel_index].config_descriptor.component_name;
+        if KERNEL_COMPONENT_NAME != kernel_name {
+            error!(
+                "The microdroid kernel entry in the client VM DICE chain must describe the \
+                 Microdroid kernel. Got '{}'",
+                kernel_name,
+            );
+            return Err(RequestProcessingError::InvalidDiceChain);
+        }
+
+        debug!("All entries in the client VM DICE chain have correct component names");
+        Ok(Self { payloads: dice_entry_payloads, vendor_partition_index, kernel_index })
     }
 
     pub(crate) fn microdroid_kernel(&self) -> &DiceChainEntryPayload {
-        &self.payloads[self.payloads.len() - 2]
+        &self.payloads[self.kernel_index]
+    }
+
+    pub(crate) fn vendor_partition(&self) -> Option<&DiceChainEntryPayload> {
+        self.vendor_partition_index.map(|i| &self.payloads[i])
     }
 
     pub(crate) fn microdroid_payload(&self) -> &DiceChainEntryPayload {
@@ -148,39 +174,33 @@
     }
 }
 
-/// Validates that the `client_vm_dice_chain` matches the `service_vm_dice_chain` up to the pvmfw
-/// entry.
-///
-/// Returns `Ok(())` if the verification succeeds.
-pub(crate) fn validate_client_vm_dice_chain_prefix_match(
-    client_vm_dice_chain: &[Value],
-    service_vm_dice_chain: &[Value],
-) -> Result<()> {
-    if service_vm_dice_chain.len() < 3 {
-        // The service VM's DICE chain must contain the root key and at least two other entries
-        // that describe:
-        //   - pvmfw
-        //   - Service VM kernel
-        error!("The service VM DICE chain must contain at least three entries");
-        return Err(RequestProcessingError::InternalError);
+fn vendor_partition_exists(
+    client_vm_dice_chain_len: usize,
+    service_vm_dice_chain_len: usize,
+) -> Result<bool> {
+    let entries_up_to_pvmfw_len = service_vm_dice_chain_len - 1;
+    // Client VM DICE chain = entries_up_to_pvmfw
+    //    + Vendor module entry (exists only when the vendor partition is present)
+    //    + Microdroid kernel entry (added in pvmfw)
+    //    + Apk/Apexes entry (added in microdroid)
+    match client_vm_dice_chain_len.checked_sub(entries_up_to_pvmfw_len) {
+        Some(2) => {
+            debug!("The vendor partition entry is not present in the client VM's DICE chain");
+            Ok(false)
+        }
+        Some(3) => {
+            info!("The vendor partition entry is present in the client VM's DICE chain");
+            Ok(true)
+        }
+        _ => {
+            error!(
+                "The client VM's DICE chain must contain two or three extra entries. \
+            Service VM DICE chain: {} entries, client VM DICE chain: {} entries",
+                service_vm_dice_chain_len, client_vm_dice_chain_len
+            );
+            Err(RequestProcessingError::InvalidDiceChain)
+        }
     }
-    // Ignores the last entry that describes service VM
-    let entries_up_to_pvmfw = &service_vm_dice_chain[0..(service_vm_dice_chain.len() - 1)];
-    if entries_up_to_pvmfw.len() + 2 != client_vm_dice_chain.len() {
-        // Client VM DICE chain = entries_up_to_pvmfw
-        //    + Microdroid kernel entry (added in pvmfw)
-        //    + Apk/Apexes entry (added in microdroid)
-        error!("The client VM's DICE chain must contain exactly two extra entries");
-        return Err(RequestProcessingError::InvalidDiceChain);
-    }
-    if entries_up_to_pvmfw != &client_vm_dice_chain[0..entries_up_to_pvmfw.len()] {
-        error!(
-            "The client VM's DICE chain does not match service VM's DICE chain up to \
-             the pvmfw entry"
-        );
-        return Err(RequestProcessingError::InvalidDiceChain);
-    }
-    Ok(())
 }
 
 #[derive(Debug, Clone)]
diff --git a/service_vm/requests/src/lib.rs b/service_vm/requests/src/lib.rs
index 0dfac09..36b006f 100644
--- a/service_vm/requests/src/lib.rs
+++ b/service_vm/requests/src/lib.rs
@@ -26,4 +26,4 @@
 mod pub_key;
 mod rkp;
 
-pub use api::process_request;
+pub use api::{process_request, RequestContext};
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 29e9014..966e7bf 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -247,6 +247,24 @@
 
     @Test
     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
+    public void vmAttestationWithVendorPartitionWhenSupported() throws Exception {
+        // pVM remote attestation is only supported on protected VMs.
+        assumeProtectedVM();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_REMOTE_ATTESTATION);
+        assume().withMessage("Test needs Remote Attestation support")
+                .that(getVirtualMachineManager().isRemoteAttestationSupported())
+                .isTrue();
+        File vendorDiskImage = new File("/vendor/etc/avf/microdroid/microdroid_vendor.img");
+        assumeTrue("Microdroid vendor image doesn't exist, skip", vendorDiskImage.exists());
+        VirtualMachineConfig config =
+                buildVmConfigWithVendor(vendorDiskImage, VM_ATTESTATION_PAYLOAD_PATH);
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("cts_attestation_with_vendor_module", config);
+        checkVmAttestationWithValidChallenge(vm);
+    }
+
+    @Test
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void vmAttestationWhenRemoteAttestationIsSupported() throws Exception {
         // pVM remote attestation is only supported on protected VMs.
         assumeProtectedVM();
@@ -272,6 +290,10 @@
                 .isEqualTo(AttestationStatus.ERROR_INVALID_CHALLENGE);
 
         // Check with a valid challenge.
+        checkVmAttestationWithValidChallenge(vm);
+    }
+
+    private void checkVmAttestationWithValidChallenge(VirtualMachine vm) throws Exception {
         byte[] challenge = new byte[32];
         Arrays.fill(challenge, (byte) 0xac);
         SigningResult signingResult =
@@ -2265,6 +2287,11 @@
     }
 
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
+        return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
+    }
+
+    private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage, String binaryPath)
+            throws Exception {
         assumeSupportedDevice();
         // TODO(b/325094712): Boot fails with vendor partition in Cuttlefish.
         assumeFalse(
@@ -2275,7 +2302,7 @@
                 "boot with vendor partition is failing in HWASAN enabled Microdroid.", isHwasan());
         assumeFeatureEnabled(VirtualMachineManager.FEATURE_VENDOR_MODULES);
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
+                newVmConfigBuilderWithPayloadBinary(binaryPath)
                         .setVendorDiskImage(vendorDiskImage)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();