Merge "[bssl] Check the public EC_KEY built from COSE_Key" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index a9193d7..4da96c8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -34,6 +34,9 @@
     },
     {
       "name": "libapkzip.test"
+    },
+    {
+      "name": "libsecretkeeper_comm.test"
     }
   ],
   "avf-postsubmit": [
@@ -114,6 +117,9 @@
       "path": "packages/modules/Virtualization/service_vm/requests"
     },
     {
+      "path": "packages/modules/Virtualization/virtualizationservice"
+    },
+    {
       "path": "packages/modules/Virtualization/vm"
     },
     {
diff --git a/apex/Android.bp b/apex/Android.bp
index f2a0d64..acbc0a1 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -73,11 +73,13 @@
     config_namespace: "ANDROID",
     bool_variables: [
         "release_avf_enable_device_assignment",
+        "release_avf_enable_remote_attestation",
         "release_avf_enable_vendor_modules",
     ],
     properties: [
         "arch",
         "prebuilts",
+        "vintf_fragments",
     ],
 }
 
@@ -112,7 +114,6 @@
         "vm",
     ],
     prebuilts: [
-        "com.android.virt.init.rc",
         "features_com.android.virt.xml",
         "microdroid_initrd_debuggable",
         "microdroid_initrd_normal",
@@ -142,12 +143,21 @@
         },
         release_avf_enable_vendor_modules: {
             prebuilts: [
-                "microdroid_gki_initrd_debuggable",
-                "microdroid_gki_initrd_normal",
-                "microdroid_gki_kernel",
-                "microdroid_gki.json",
+                "microdroid_gki-6.1_initrd_debuggable",
+                "microdroid_gki-6.1_initrd_normal",
+                "microdroid_gki-6.1_kernel",
+                "microdroid_gki-6.1.json",
             ],
         },
+        release_avf_enable_remote_attestation: {
+            prebuilts: ["com.android.virt.init_attestation_enabled.rc"],
+            vintf_fragments: [
+                "virtualizationservice.xml",
+            ],
+            conditions_default: {
+                prebuilts: ["com.android.virt.init.rc"],
+            },
+        },
     },
 }
 
@@ -171,7 +181,14 @@
 prebuilt_etc {
     name: "com.android.virt.init.rc",
     src: "virtualizationservice.rc",
-    filename: "init.rc",
+    filename: "virtualizationservice.rc",
+    installable: false,
+}
+
+prebuilt_etc {
+    name: "com.android.virt.init_attestation_enabled.rc",
+    src: "virtualizationservice_attestation_enabled.rc",
+    filename: "virtualizationservice.rc",
     installable: false,
 }
 
diff --git a/apex/sign_virt_apex.py b/apex/sign_virt_apex.py
index 7393636..0c5bc72 100644
--- a/apex/sign_virt_apex.py
+++ b/apex/sign_virt_apex.py
@@ -410,21 +410,35 @@
            '--output_vbmeta_image', output]
     RunCommand(args, cmd)
 
+
+gki_versions = ['6.1']
+
 # dict of (key, file) for re-sign/verification. keys are un-versioned for readability.
-virt_apex_files = {
+virt_apex_non_gki_files = {
     'kernel': 'etc/fs/microdroid_kernel',
-    'gki_kernel': 'etc/fs/microdroid_gki_kernel',
     'vbmeta.img': 'etc/fs/microdroid_vbmeta.img',
     'super.img': 'etc/fs/microdroid_super.img',
     'initrd_normal.img': 'etc/microdroid_initrd_normal.img',
-    'gki_initrd_normal.img': 'etc/microdroid_gki_initrd_normal.img',
     'initrd_debuggable.img': 'etc/microdroid_initrd_debuggable.img',
-    'gki_initrd_debuggable.img': 'etc/microdroid_gki_initrd_debuggable.img',
 }
 
-
 def TargetFiles(input_dir):
-    return {k: os.path.join(input_dir, v) for k, v in virt_apex_files.items()}
+    ret = {k: os.path.join(input_dir, v) for k, v in virt_apex_non_gki_files.items()}
+
+    for ver in gki_versions:
+        kernel        = os.path.join(input_dir, f'etc/fs/microdroid_gki-{ver}_kernel')
+        initrd_normal = os.path.join(input_dir, f'etc/microdroid_gki-{ver}_initrd_normal.img')
+        initrd_debug  = os.path.join(input_dir, f'etc/microdroid_gki-{ver}_initrd_debuggable.img')
+
+        if os.path.isfile(kernel):
+            ret[f'gki-{ver}_kernel']                = kernel
+            ret[f'gki-{ver}_initrd_normal.img']     = initrd_normal
+            ret[f'gki-{ver}_initrd_debuggable.img'] = initrd_debug
+
+    return ret
+
+def IsInitrdImage(path):
+    return path.endswith('initrd_normal.img') or path.endswith('initrd_debuggable.img')
 
 
 def SignVirtApex(args):
@@ -461,13 +475,9 @@
                      images=images,
                      wait=images_f)
 
-    has_gki_kernel = os.path.isfile(files['gki_kernel'])
-
     vbmeta_bc_f = None
     if not args.do_not_update_bootconfigs:
-        initrd_files = [files['initrd_normal.img'], files['initrd_debuggable.img']]
-        if has_gki_kernel:
-            initrd_files += [files['gki_initrd_normal.img'], files['gki_initrd_debuggable.img']]
+        initrd_files = [v for k, v in files.items() if IsInitrdImage(k)]
         vbmeta_bc_f = Async(UpdateVbmetaBootconfig, args, initrd_files,
                             files['vbmeta.img'],
                             wait=[vbmeta_f])
@@ -493,8 +503,12 @@
 
     resign_kernel('kernel', 'initrd_normal.img', 'initrd_debuggable.img')
 
-    if has_gki_kernel:
-        resign_kernel('gki_kernel', 'gki_initrd_normal.img', 'gki_initrd_debuggable.img')
+    for ver in gki_versions:
+        if f'gki-{ver}_kernel' in files:
+            resign_kernel(
+                f'gki-{ver}_kernel',
+                f'gki-{ver}_initrd_normal.img',
+                f'gki-{ver}_initrd_debuggable.img')
 
 
 def VerifyVirtApex(args):
@@ -518,12 +532,11 @@
         assert info is not None, f'no avbinfo: {file}'
         assert info['Public key (sha1)'] == pubkey_digest, f'pubkey mismatch: {file}'
 
-    for f in files.values():
-        if f in (files['initrd_normal.img'], files['initrd_debuggable.img'],
-                 files['gki_initrd_normal.img'], files['gki_initrd_debuggable.img']):
+    for k, f in files.items():
+        if IsInitrdImage(k):
             # TODO(b/245277660): Verify that ramdisks contain the correct vbmeta digest
             continue
-        if f == files['super.img']:
+        if k == 'super.img':
             Async(check_avb_pubkey, system_a_img)
         else:
             # Check pubkey for other files using avbtool
diff --git a/apex/virtualizationservice.xml b/apex/virtualizationservice.xml
index 0ce1e10..60f466f 100644
--- a/apex/virtualizationservice.xml
+++ b/apex/virtualizationservice.xml
@@ -1,6 +1,6 @@
 <manifest version="1.0" type="framework">
     <hal format="aidl">
-        <name>android.system.virtualization</name>
+        <name>android.hardware.security.keymint</name>
         <version>3</version>
         <fqname>IRemotelyProvisionedComponent/avf</fqname>
     </hal>
diff --git a/apex/virtualizationservice_attestation_enabled.rc b/apex/virtualizationservice_attestation_enabled.rc
new file mode 100644
index 0000000..8eaccae
--- /dev/null
+++ b/apex/virtualizationservice_attestation_enabled.rc
@@ -0,0 +1,22 @@
+# Copyright (C) 2021 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.
+
+service virtualizationservice /apex/com.android.virt/bin/virtualizationservice
+    class main
+    user system
+    group system
+    interface aidl android.system.virtualizationservice
+    interface aidl android.hardware.security.keymint.IRemotelyProvisionedComponent/avf
+    disabled
+    oneshot
diff --git a/libs/apexutil/Android.bp b/libs/apexutil/Android.bp
index 92d4e80..f9b72c4 100644
--- a/libs/apexutil/Android.bp
+++ b/libs/apexutil/Android.bp
@@ -6,7 +6,6 @@
     name: "libapexutil_rust.defaults",
     crate_name: "apexutil",
     defaults: ["avf_build_flags_rust"],
-    host_supported: true,
     srcs: ["src/lib.rs"],
     edition: "2021",
     rustlibs: [
@@ -31,14 +30,4 @@
     rustlibs: [
         "libhex",
     ],
-    target: {
-        host: {
-            // TODO(b/204562227): remove once the build does this automatically
-            data_libs: [
-                "libc++",
-                "libcrypto",
-                "libz",
-            ],
-        },
-    },
 }
diff --git a/libs/bssl/Android.bp b/libs/bssl/Android.bp
index ff45af9..e1f4ffd 100644
--- a/libs/bssl/Android.bp
+++ b/libs/bssl/Android.bp
@@ -46,5 +46,6 @@
     rustlibs: [
         "libbssl_avf_nostd",
         "libcoset_nostd",
+        "libspki_nostd",
     ],
 }
diff --git a/libs/bssl/error/src/lib.rs b/libs/bssl/error/src/lib.rs
index 89865d4..c0dca2e 100644
--- a/libs/bssl/error/src/lib.rs
+++ b/libs/bssl/error/src/lib.rs
@@ -81,6 +81,9 @@
     EVP_AEAD_CTX_new,
     EVP_AEAD_CTX_open,
     EVP_AEAD_CTX_seal,
+    EVP_PKEY_new,
+    EVP_PKEY_set1_EC_KEY,
+    EVP_marshal_public_key,
     HKDF,
     HMAC,
     RAND_bytes,
diff --git a/libs/bssl/src/ec_key.rs b/libs/bssl/src/ec_key.rs
index 5a22e65..a187259 100644
--- a/libs/bssl/src/ec_key.rs
+++ b/libs/bssl/src/ec_key.rs
@@ -45,7 +45,7 @@
 type Coordinate = [u8; P256_AFFINE_COORDINATE_SIZE];
 
 /// Wrapper of an `EC_KEY` object, representing a public or private EC key.
-pub struct EcKey(NonNull<EC_KEY>);
+pub struct EcKey(pub(crate) NonNull<EC_KEY>);
 
 impl Drop for EcKey {
     fn drop(&mut self) {
diff --git a/libs/bssl/src/evp.rs b/libs/bssl/src/evp.rs
new file mode 100644
index 0000000..30bfc21
--- /dev/null
+++ b/libs/bssl/src/evp.rs
@@ -0,0 +1,90 @@
+// Copyright 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.
+
+//! Wrappers of the EVP functions in BoringSSL evp.h.
+
+use crate::cbb::CbbFixed;
+use crate::ec_key::EcKey;
+use crate::util::{check_int_result, to_call_failed_error};
+use alloc::vec::Vec;
+use bssl_avf_error::{ApiName, Result};
+use bssl_ffi::{
+    CBB_flush, CBB_len, EVP_PKEY_free, EVP_PKEY_new, EVP_PKEY_set1_EC_KEY, EVP_marshal_public_key,
+    EVP_PKEY,
+};
+use core::ptr::NonNull;
+
+/// Wrapper of an `EVP_PKEY` object, representing a public or private key.
+pub struct EvpPKey {
+    pkey: NonNull<EVP_PKEY>,
+    /// Since this struct owns the inner key, the inner key remains valid as
+    /// long as the pointer to `EVP_PKEY` is valid.
+    _inner_key: EcKey,
+}
+
+impl Drop for EvpPKey {
+    fn drop(&mut self) {
+        // SAFETY: It is safe because `EVP_PKEY` has been allocated by BoringSSL and isn't
+        // used after this.
+        unsafe { EVP_PKEY_free(self.pkey.as_ptr()) }
+    }
+}
+
+/// Creates a new empty `EVP_PKEY`.
+fn new_pkey() -> Result<NonNull<EVP_PKEY>> {
+    // SAFETY: The returned pointer is checked below.
+    let key = unsafe { EVP_PKEY_new() };
+    NonNull::new(key).ok_or(to_call_failed_error(ApiName::EVP_PKEY_new))
+}
+
+impl TryFrom<EcKey> for EvpPKey {
+    type Error = bssl_avf_error::Error;
+
+    fn try_from(key: EcKey) -> Result<Self> {
+        let pkey = new_pkey()?;
+        // SAFETY: The function only sets the inner key of the initialized and
+        // non-null `EVP_PKEY` to point to the given `EC_KEY`. It only reads from
+        // and writes to the initialized `EVP_PKEY`.
+        // Since this struct owns the inner key, the inner key remains valid as
+        // long as `EVP_PKEY` is valid.
+        let ret = unsafe { EVP_PKEY_set1_EC_KEY(pkey.as_ptr(), key.0.as_ptr()) };
+        check_int_result(ret, ApiName::EVP_PKEY_set1_EC_KEY)?;
+        Ok(Self { pkey, _inner_key: key })
+    }
+}
+
+impl EvpPKey {
+    /// Returns a DER-encoded SubjectPublicKeyInfo structure as specified
+    /// in RFC 5280 s4.1.2.7:
+    ///
+    /// https://www.rfc-editor.org/rfc/rfc5280.html#section-4.1.2.7
+    pub fn subject_public_key_info(&self) -> Result<Vec<u8>> {
+        const CAPACITY: usize = 256;
+        let mut buf = [0u8; CAPACITY];
+        let mut cbb = CbbFixed::new(buf.as_mut());
+        // SAFETY: The function only write bytes to the buffer managed by the valid `CBB`.
+        // The inner key in `EVP_PKEY` was set to a valid key when the object was created.
+        // As this struct owns the inner key, the inner key is guaranteed to be valid
+        // throughout the execution of the function.
+        let ret = unsafe { EVP_marshal_public_key(cbb.as_mut(), self.pkey.as_ptr()) };
+        check_int_result(ret, ApiName::EVP_marshal_public_key)?;
+        // SAFETY: This is safe because the CBB pointer is a valid pointer initialized with
+        // `CBB_init_fixed()`.
+        check_int_result(unsafe { CBB_flush(cbb.as_mut()) }, ApiName::CBB_flush)?;
+        // SAFETY: This is safe because the CBB pointer is initialized with `CBB_init_fixed()`,
+        // and it has been flushed, thus it has no active children.
+        let len = unsafe { CBB_len(cbb.as_ref()) };
+        Ok(buf.get(0..len).ok_or(to_call_failed_error(ApiName::CBB_len))?.to_vec())
+    }
+}
diff --git a/libs/bssl/src/lib.rs b/libs/bssl/src/lib.rs
index 8e3abcf..e378386 100644
--- a/libs/bssl/src/lib.rs
+++ b/libs/bssl/src/lib.rs
@@ -24,6 +24,7 @@
 mod digest;
 mod ec_key;
 mod err;
+mod evp;
 mod hkdf;
 mod hmac;
 mod rand;
@@ -37,6 +38,7 @@
 pub use cbs::Cbs;
 pub use digest::Digester;
 pub use ec_key::{EcKey, ZVec};
+pub use evp::EvpPKey;
 pub use hkdf::hkdf;
 pub use hmac::hmac_sha256;
 pub use rand::rand_bytes;
diff --git a/libs/bssl/tests/eckey_test.rs b/libs/bssl/tests/eckey_test.rs
index 3dd243c..968af63 100644
--- a/libs/bssl/tests/eckey_test.rs
+++ b/libs/bssl/tests/eckey_test.rs
@@ -12,8 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use bssl_avf::{sha256, ApiName, EcKey, EcdsaError, Error, Result};
+use bssl_avf::{sha256, ApiName, EcKey, EcdsaError, Error, EvpPKey, Result};
 use coset::CborSerializable;
+use spki::{
+    der::{AnyRef, Decode},
+    AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfo,
+};
+
+/// OID value for general-use NIST EC keys held in PKCS#8 and X.509; see RFC 5480 s2.1.1.
+const X509_NIST_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
+
+/// OID value in `AlgorithmIdentifier.parameters` for P-256; see RFC 5480 s2.1.1.1.
+const ALGO_PARAM_P256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
 
 const MESSAGE1: &[u8] = b"test message 1";
 const MESSAGE2: &[u8] = b"test message 2";
@@ -30,6 +40,23 @@
 }
 
 #[test]
+fn subject_public_key_info_serialization() -> Result<()> {
+    let mut ec_key = EcKey::new_p256()?;
+    ec_key.generate_key()?;
+    let pkey: EvpPKey = ec_key.try_into()?;
+    let subject_public_key_info = pkey.subject_public_key_info()?;
+
+    let subject_public_key_info = SubjectPublicKeyInfo::from_der(&subject_public_key_info).unwrap();
+    let expected_algorithm = AlgorithmIdentifier {
+        oid: X509_NIST_OID,
+        parameters: Some(AnyRef::from(&ALGO_PARAM_P256_OID)),
+    };
+    assert_eq!(expected_algorithm, subject_public_key_info.algorithm);
+    assert!(!subject_public_key_info.subject_public_key.to_vec().is_empty());
+    Ok(())
+}
+
+#[test]
 fn cose_public_key_serialization() -> Result<()> {
     let mut ec_key = EcKey::new_p256()?;
     ec_key.generate_key()?;
diff --git a/libs/dice/open_dice/src/bcc.rs b/libs/dice/open_dice/src/bcc.rs
index 199e1a9..9c9545b 100644
--- a/libs/dice/open_dice/src/bcc.rs
+++ b/libs/dice/open_dice/src/bcc.rs
@@ -20,7 +20,7 @@
     DiceAndroidConfigValues, DiceAndroidFormatConfigDescriptor, DiceAndroidHandoverMainFlow,
     DiceAndroidHandoverParse, DiceAndroidMainFlow, DICE_ANDROID_CONFIG_COMPONENT_NAME,
     DICE_ANDROID_CONFIG_COMPONENT_VERSION, DICE_ANDROID_CONFIG_RESETTABLE,
-    DICE_ANDROID_CONFIG_SECURITY_VERSION,
+    DICE_ANDROID_CONFIG_RKP_VM_MARKER, DICE_ANDROID_CONFIG_SECURITY_VERSION,
 };
 use std::{ffi::CStr, ptr};
 
@@ -36,6 +36,8 @@
     pub resettable: bool,
     /// Monotonically increasing version of the component.
     pub security_version: Option<u64>,
+    /// Whether the component can take part in running the RKP VM.
+    pub rkp_vm_marker: bool,
 }
 
 /// Formats a configuration descriptor following the Android Profile for DICE specification.
@@ -58,6 +60,9 @@
         configs |= DICE_ANDROID_CONFIG_SECURITY_VERSION;
         version
     });
+    if values.rkp_vm_marker {
+        configs |= DICE_ANDROID_CONFIG_RKP_VM_MARKER;
+    }
 
     let values =
         DiceAndroidConfigValues { configs, component_name, component_version, security_version };
diff --git a/libs/hyp/src/hypervisor.rs b/libs/hyp/src/hypervisor.rs
index 309f967..3d42ccb 100644
--- a/libs/hyp/src/hypervisor.rs
+++ b/libs/hyp/src/hypervisor.rs
@@ -34,6 +34,8 @@
 use smccc::hvc64;
 use uuid::Uuid;
 
+use self::common::DeviceAssigningHypervisor;
+
 enum HypervisorBackend {
     RegularKvm,
     Gunyah,
@@ -122,3 +124,8 @@
 pub fn get_mem_sharer() -> Option<&'static dyn MemSharingHypervisor> {
     get_hypervisor().as_mem_sharer()
 }
+
+/// Gets the device assigning hypervisor singleton, if any.
+pub fn get_device_assigner() -> Option<&'static dyn DeviceAssigningHypervisor> {
+    get_hypervisor().as_device_assigner()
+}
diff --git a/libs/hyp/src/hypervisor/common.rs b/libs/hyp/src/hypervisor/common.rs
index 70fdd0a..2ea18f3 100644
--- a/libs/hyp/src/hypervisor/common.rs
+++ b/libs/hyp/src/hypervisor/common.rs
@@ -31,6 +31,11 @@
     fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
         None
     }
+
+    /// Returns the hypervisor's device assigning implementation, if any.
+    fn as_device_assigner(&self) -> Option<&dyn DeviceAssigningHypervisor> {
+        None
+    }
 }
 
 pub trait MmioGuardedHypervisor {
@@ -73,3 +78,11 @@
     /// Returns the memory protection granule size in bytes.
     fn granule(&self) -> Result<usize>;
 }
+
+pub trait DeviceAssigningHypervisor {
+    /// Returns MMIO token.
+    fn get_phys_mmio_token(&self, base_ipa: u64, size: u64) -> Result<u64>;
+
+    /// Returns DMA token as a tuple of (phys_iommu_id, phys_sid).
+    fn get_phys_iommu_token(&self, pviommu_id: u64, vsid: u64) -> Result<(u64, u64)>;
+}
diff --git a/libs/hyp/src/hypervisor/kvm.rs b/libs/hyp/src/hypervisor/kvm.rs
index 5835346..720318e 100644
--- a/libs/hyp/src/hypervisor/kvm.rs
+++ b/libs/hyp/src/hypervisor/kvm.rs
@@ -14,7 +14,9 @@
 
 //! Wrappers around calls to the KVM hypervisor.
 
-use super::common::{Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor};
+use super::common::{
+    DeviceAssigningHypervisor, Hypervisor, MemSharingHypervisor, MmioGuardedHypervisor,
+};
 use crate::error::{Error, Result};
 use crate::util::page_address;
 use core::fmt::{self, Display, Formatter};
@@ -70,6 +72,9 @@
 const VENDOR_HYP_KVM_MMIO_GUARD_MAP_FUNC_ID: u32 = 0xc6000007;
 const VENDOR_HYP_KVM_MMIO_GUARD_UNMAP_FUNC_ID: u32 = 0xc6000008;
 
+const VENDOR_HYP_KVM_DEV_REQ_MMIO_FUNC_ID: u32 = 0xc6000012;
+const VENDOR_HYP_KVM_DEV_REQ_DMA_FUNC_ID: u32 = 0xc6000013;
+
 pub(super) struct RegularKvmHypervisor;
 
 impl RegularKvmHypervisor {
@@ -90,6 +95,10 @@
     fn as_mem_sharer(&self) -> Option<&dyn MemSharingHypervisor> {
         Some(self)
     }
+
+    fn as_device_assigner(&self) -> Option<&dyn DeviceAssigningHypervisor> {
+        Some(self)
+    }
 }
 
 impl MmioGuardedHypervisor for ProtectedKvmHypervisor {
@@ -153,6 +162,26 @@
     }
 }
 
+impl DeviceAssigningHypervisor for ProtectedKvmHypervisor {
+    fn get_phys_mmio_token(&self, base_ipa: u64, size: u64) -> Result<u64> {
+        let mut args = [0u64; 17];
+        args[0] = base_ipa;
+        args[1] = size;
+
+        let ret = checked_hvc64_expect_results(VENDOR_HYP_KVM_DEV_REQ_MMIO_FUNC_ID, args)?;
+        Ok(ret[0])
+    }
+
+    fn get_phys_iommu_token(&self, pviommu_id: u64, vsid: u64) -> Result<(u64, u64)> {
+        let mut args = [0u64; 17];
+        args[0] = pviommu_id;
+        args[1] = vsid;
+
+        let ret = checked_hvc64_expect_results(VENDOR_HYP_KVM_DEV_REQ_DMA_FUNC_ID, args)?;
+        Ok((ret[0], ret[1]))
+    }
+}
+
 fn checked_hvc64_expect_zero(function: u32, args: [u64; 17]) -> Result<()> {
     success_or_error_64(hvc64(function, args)[0]).map_err(|e| Error::KvmError(e, function))
 }
@@ -160,3 +189,9 @@
 fn checked_hvc64(function: u32, args: [u64; 17]) -> Result<u64> {
     positive_or_error_64(hvc64(function, args)[0]).map_err(|e| Error::KvmError(e, function))
 }
+
+fn checked_hvc64_expect_results(function: u32, args: [u64; 17]) -> Result<[u64; 17]> {
+    let [ret, results @ ..] = hvc64(function, args);
+    success_or_error_64(ret).map_err(|e| Error::KvmError(e, function))?;
+    Ok(results)
+}
diff --git a/libs/hyp/src/lib.rs b/libs/hyp/src/lib.rs
index 486a181..505aade 100644
--- a/libs/hyp/src/lib.rs
+++ b/libs/hyp/src/lib.rs
@@ -21,6 +21,8 @@
 mod util;
 
 pub use error::{Error, Result};
-pub use hypervisor::{get_mem_sharer, get_mmio_guard, KvmError, MMIO_GUARD_GRANULE_SIZE};
+pub use hypervisor::{
+    get_device_assigner, get_mem_sharer, get_mmio_guard, KvmError, MMIO_GUARD_GRANULE_SIZE,
+};
 
 use hypervisor::GeniezoneError;
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index c93cb4c..c1caa56 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -332,22 +332,6 @@
     ],
 }
 
-android_filesystem {
-    name: "microdroid_gki_modules-6.1-arm64",
-    deps: [
-        "microdroid_gki_kernel_modules-6.1-arm64",
-    ],
-    type: "compressed_cpio",
-}
-
-android_filesystem {
-    name: "microdroid_gki_modules-6.1-x86_64",
-    deps: [
-        "microdroid_gki_kernel_modules-6.1-x86_64",
-    ],
-    type: "compressed_cpio",
-}
-
 genrule {
     name: "microdroid_bootconfig_arm64_gen",
     srcs: [
@@ -410,6 +394,16 @@
             partitions: ["microdroid_vendor"],
         },
     },
+    // TODO(b/312809093): Remove hard-coded property after figuring out the
+    // long-term solution for microdroid vendor partition SPL. The hard-coded
+    // value is the minimum value of SPL that microdroid vendor partition will
+    // have. It's for passing the check 'IsStandaloneImageRollback'.
+    avb_properties: [
+        {
+            key: "com.android.build.microdroid-vendor.security_patch",
+            value: "2023-12-05",
+        },
+    ],
 }
 
 prebuilt_etc {
@@ -418,11 +412,6 @@
 }
 
 prebuilt_etc {
-    name: "microdroid_gki.json",
-    src: "microdroid_gki.json",
-}
-
-prebuilt_etc {
     name: "microdroid_manifest",
     src: "microdroid_manifest.xml",
     filename: "manifest.xml",
@@ -450,11 +439,8 @@
 // python -c "import hashlib; print(hashlib.sha256(b'initrd_normal').hexdigest())"
 initrd_normal_salt = "8041a07d54ac82290f6d90bac1fa8d7fdbc4db974d101d60faf294749d1ebaf8"
 
-avb_gen_vbmeta_image {
-    name: "microdroid_initrd_normal_hashdesc",
-    src: ":microdroid_initrd_normal",
-    partition_name: "initrd_normal",
-    salt: initrd_normal_salt,
+avb_gen_vbmeta_image_defaults {
+    name: "microdroid_initrd_defaults",
     enabled: false,
     arch: {
         // Microdroid kernel is only available in these architectures.
@@ -467,63 +453,38 @@
     },
 }
 
-avb_gen_vbmeta_image {
-    name: "microdroid_gki_initrd_normal_hashdesc",
-    src: ":microdroid_gki_initrd_normal",
+avb_gen_vbmeta_image_defaults {
+    name: "microdroid_initrd_normal_defaults",
+    defaults: ["microdroid_initrd_defaults"],
     partition_name: "initrd_normal",
     salt: initrd_normal_salt,
-    enabled: false,
-    arch: {
-        // Microdroid kernel is only available in these architectures.
-        arm64: {
-            enabled: true,
-        },
-        x86_64: {
-            enabled: true,
-        },
-    },
+}
+
+avb_gen_vbmeta_image {
+    name: "microdroid_initrd_normal_hashdesc",
+    defaults: ["microdroid_initrd_normal_defaults"],
+    src: ":microdroid_initrd_normal",
 }
 
 // python -c "import hashlib; print(hashlib.sha256(b'initrd_debug').hexdigest())"
 initrd_debug_salt = "8ab9dc9cb7e6456700ff6ef18c6b4c3acc24c5fa5381b829563f8d7a415d869a"
 
-avb_gen_vbmeta_image {
-    name: "microdroid_initrd_debug_hashdesc",
-    src: ":microdroid_initrd_debuggable",
+avb_gen_vbmeta_image_defaults {
+    name: "microdroid_initrd_debug_defaults",
+    defaults: ["microdroid_initrd_defaults"],
     partition_name: "initrd_debug",
     salt: initrd_debug_salt,
-    enabled: false,
-    arch: {
-        // Microdroid kernel is only available in these architectures.
-        arm64: {
-            enabled: true,
-        },
-        x86_64: {
-            enabled: true,
-        },
-    },
 }
 
 avb_gen_vbmeta_image {
-    name: "microdroid_gki_initrd_debug_hashdesc",
-    src: ":microdroid_gki_initrd_debuggable",
-    partition_name: "initrd_debug",
-    salt: initrd_debug_salt,
-    enabled: false,
-    arch: {
-        // Microdroid kernel is only available in these architectures.
-        arm64: {
-            enabled: true,
-        },
-        x86_64: {
-            enabled: true,
-        },
-    },
+    name: "microdroid_initrd_debug_hashdesc",
+    defaults: ["microdroid_initrd_debug_defaults"],
+    src: ":microdroid_initrd_debuggable",
 }
 
 soong_config_module_type {
-    name: "flag_aware_avb_add_hash_footer",
-    module_type: "avb_add_hash_footer",
+    name: "flag_aware_avb_add_hash_footer_defaults",
+    module_type: "avb_add_hash_footer_defaults",
     config_namespace: "ANDROID",
     bool_variables: [
         "release_avf_enable_llpvm_changes",
@@ -534,28 +495,21 @@
     ],
 }
 
-flag_aware_avb_add_hash_footer {
-    name: "microdroid_kernel_signed",
+flag_aware_avb_add_hash_footer_defaults {
+    name: "microdroid_kernel_signed_defaults",
     src: ":empty_file",
-    filename: "microdroid_kernel",
     partition_name: "boot",
     private_key: ":microdroid_sign_key",
     salt: bootloader_salt,
     enabled: false,
     arch: {
         arm64: {
-            src: ":microdroid_kernel_prebuilts-6.1-arm64",
             enabled: true,
         },
         x86_64: {
-            src: ":microdroid_kernel_prebuilts-6.1-x86_64",
             enabled: true,
         },
     },
-    include_descriptors_from_images: [
-        ":microdroid_initrd_normal_hashdesc",
-        ":microdroid_initrd_debug_hashdesc",
-    ],
     // Below are properties that are conditionally set depending on value of build flags.
     soong_config_variables: {
         release_avf_enable_llpvm_changes: {
@@ -570,40 +524,22 @@
     },
 }
 
-flag_aware_avb_add_hash_footer {
-    name: "microdroid_gki_kernel_signed",
-    src: ":empty_file",
-    filename: "microdroid_gki_kernel",
-    partition_name: "boot",
-    private_key: ":microdroid_sign_key",
-    salt: bootloader_salt,
-    enabled: false,
+avb_add_hash_footer {
+    name: "microdroid_kernel_signed",
+    defaults: ["microdroid_kernel_signed_defaults"],
+    filename: "microdroid_kernel",
     arch: {
         arm64: {
-            src: ":microdroid_gki_kernel_prebuilts-6.1-arm64",
-            enabled: true,
+            src: ":microdroid_kernel_prebuilts-6.1-arm64",
         },
         x86_64: {
-            src: ":microdroid_gki_kernel_prebuilts-6.1-x86_64",
-            enabled: true,
+            src: ":microdroid_kernel_prebuilts-6.1-x86_64",
         },
     },
     include_descriptors_from_images: [
-        ":microdroid_gki_initrd_normal_hashdesc",
-        ":microdroid_gki_initrd_debug_hashdesc",
+        ":microdroid_initrd_normal_hashdesc",
+        ":microdroid_initrd_debug_hashdesc",
     ],
-    // Below are properties that are conditionally set depending on value of build flags.
-    soong_config_variables: {
-        release_avf_enable_llpvm_changes: {
-            rollback_index: 1,
-            props: [
-                {
-                    name: "com.android.virt.cap",
-                    value: "secretkeeper_protection",
-                },
-            ],
-        },
-    },
 }
 
 prebuilt_etc {
@@ -620,16 +556,54 @@
     },
 }
 
+///////////////////////////////////////
+// GKI-6.1 modules
+///////////////////////////////////////
 prebuilt_etc {
-    name: "microdroid_gki_kernel",
+    name: "microdroid_gki-6.1.json",
+    src: "microdroid_gki-6.1.json",
+}
+
+avb_add_hash_footer {
+    name: "microdroid_gki-6.1_kernel_signed",
+    defaults: ["microdroid_kernel_signed_defaults"],
+    filename: "microdroid_gki-6.1_kernel",
+    arch: {
+        arm64: {
+            src: ":microdroid_gki_kernel_prebuilts-6.1-arm64",
+        },
+        x86_64: {
+            src: ":microdroid_gki_kernel_prebuilts-6.1-x86_64",
+        },
+    },
+    include_descriptors_from_images: [
+        ":microdroid_gki-6.1_initrd_normal_hashdesc",
+        ":microdroid_gki-6.1_initrd_debug_hashdesc",
+    ],
+}
+
+prebuilt_etc {
+    name: "microdroid_gki-6.1_kernel",
     src: ":empty_file",
     relative_install_path: "fs",
     arch: {
         arm64: {
-            src: ":microdroid_gki_kernel_signed",
+            src: ":microdroid_gki-6.1_kernel_signed",
         },
         x86_64: {
-            src: ":microdroid_gki_kernel_signed",
+            src: ":microdroid_gki-6.1_kernel_signed",
         },
     },
 }
+
+avb_gen_vbmeta_image {
+    name: "microdroid_gki-6.1_initrd_normal_hashdesc",
+    defaults: ["microdroid_initrd_normal_defaults"],
+    src: ":microdroid_gki-6.1_initrd_normal",
+}
+
+avb_gen_vbmeta_image {
+    name: "microdroid_gki-6.1_initrd_debug_hashdesc",
+    defaults: ["microdroid_initrd_debug_defaults"],
+    src: ":microdroid_gki-6.1_initrd_debuggable",
+}
diff --git a/microdroid/initrd/Android.bp b/microdroid/initrd/Android.bp
index 6cd84fa..8df4c0f 100644
--- a/microdroid/initrd/Android.bp
+++ b/microdroid/initrd/Android.bp
@@ -41,7 +41,7 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_gen_arm64",
+    name: "microdroid_gki-6.1_initrd_gen_arm64",
     srcs: [
         ":microdroid_ramdisk",
         ":microdroid_fstab_ramdisk",
@@ -52,7 +52,7 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_gen_x86_64",
+    name: "microdroid_gki-6.1_initrd_gen_x86_64",
     srcs: [
         ":microdroid_ramdisk",
         ":microdroid_fstab_ramdisk",
@@ -96,13 +96,13 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_debuggable_arm64",
+    name: "microdroid_gki-6.1_initrd_debuggable_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
-        ":microdroid_gki_initrd_gen_arm64",
+        ":microdroid_gki-6.1_initrd_gen_arm64",
         ":microdroid_bootconfig_debuggable_src",
     ] + bootconfigs_arm64,
-    out: ["microdroid_gki_initrd_debuggable_arm64"],
+    out: ["microdroid_gki-6.1_initrd_debuggable_arm64"],
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
@@ -118,13 +118,13 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_debuggable_x86_64",
+    name: "microdroid_gki-6.1_initrd_debuggable_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
-        ":microdroid_gki_initrd_gen_x86_64",
+        ":microdroid_gki-6.1_initrd_gen_x86_64",
         ":microdroid_bootconfig_debuggable_src",
     ] + bootconfigs_x86_64,
-    out: ["microdroid_gki_initrd_debuggable_x86_64"],
+    out: ["microdroid_gki-6.1_initrd_debuggable_x86_64"],
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
@@ -140,13 +140,13 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_normal_arm64",
+    name: "microdroid_gki-6.1_initrd_normal_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
-        ":microdroid_gki_initrd_gen_arm64",
+        ":microdroid_gki-6.1_initrd_gen_arm64",
         ":microdroid_bootconfig_normal_src",
     ] + bootconfigs_arm64,
-    out: ["microdroid_gki_initrd_normal_arm64"],
+    out: ["microdroid_gki-6.1_initrd_normal_arm64"],
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
@@ -162,13 +162,13 @@
 }
 
 genrule {
-    name: "microdroid_gki_initrd_normal_x86_64",
+    name: "microdroid_gki-6.1_initrd_normal_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
-        ":microdroid_gki_initrd_gen_x86_64",
+        ":microdroid_gki-6.1_initrd_gen_x86_64",
         ":microdroid_bootconfig_normal_src",
     ] + bootconfigs_x86_64,
-    out: ["microdroid_gki_initrd_normal_x86_64"],
+    out: ["microdroid_gki-6.1_initrd_normal_x86_64"],
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
@@ -188,18 +188,18 @@
 }
 
 prebuilt_etc {
-    name: "microdroid_gki_initrd_debuggable",
+    name: "microdroid_gki-6.1_initrd_debuggable",
     // We don't have ramdisk for architectures other than x86_64 & arm64
     src: ":empty_file",
     arch: {
         x86_64: {
-            src: ":microdroid_gki_initrd_debuggable_x86_64",
+            src: ":microdroid_gki-6.1_initrd_debuggable_x86_64",
         },
         arm64: {
-            src: ":microdroid_gki_initrd_debuggable_arm64",
+            src: ":microdroid_gki-6.1_initrd_debuggable_arm64",
         },
     },
-    filename: "microdroid_gki_initrd_debuggable.img",
+    filename: "microdroid_gki-6.1_initrd_debuggable.img",
 }
 
 prebuilt_etc {
@@ -218,16 +218,16 @@
 }
 
 prebuilt_etc {
-    name: "microdroid_gki_initrd_normal",
+    name: "microdroid_gki-6.1_initrd_normal",
     // We don't have ramdisk for architectures other than x86_64 & arm64
     src: ":empty_file",
     arch: {
         x86_64: {
-            src: ":microdroid_gki_initrd_normal_x86_64",
+            src: ":microdroid_gki-6.1_initrd_normal_x86_64",
         },
         arm64: {
-            src: ":microdroid_gki_initrd_normal_arm64",
+            src: ":microdroid_gki-6.1_initrd_normal_arm64",
         },
     },
-    filename: "microdroid_gki_initrd_normal.img",
+    filename: "microdroid_gki-6.1_initrd_normal.img",
 }
diff --git a/microdroid/microdroid_gki.json b/microdroid/microdroid_gki-6.1.json
similarity index 84%
rename from microdroid/microdroid_gki.json
rename to microdroid/microdroid_gki-6.1.json
index d7ba53e..2115e51 100644
--- a/microdroid/microdroid_gki.json
+++ b/microdroid/microdroid_gki-6.1.json
@@ -1,5 +1,5 @@
 {
-  "kernel": "/apex/com.android.virt/etc/fs/microdroid_gki_kernel",
+  "kernel": "/apex/com.android.virt/etc/fs/microdroid_gki-6.1_kernel",
   "disks": [
     {
       "partitions": [
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index 51796f1..4813b35 100644
--- a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -59,8 +59,8 @@
          * Sequence of DER-encoded X.509 certificates that make up the attestation
          * key's certificate chain.
          *
-         * The certificate chain starts with a root certificate and ends with a leaf
-         * certificate covering the attested public key.
+         * The certificate chain starts with a leaf certificate covering the attested
+         * public key and ends with a root certificate.
          */
         Certificate[] certificateChain;
     }
diff --git a/pvmfw/Android.bp b/pvmfw/Android.bp
index 103619f..f49bbce 100644
--- a/pvmfw/Android.bp
+++ b/pvmfw/Android.bp
@@ -45,22 +45,69 @@
     cmd: "touch $(out)",
 }
 
-rust_test {
-    name: "libpvmfw.bootargs.test",
-    host_supported: true,
-    // For now, only bootargs.rs is written to be conditionally compiled with std.
-    srcs: ["src/bootargs.rs"],
+rust_defaults {
+    name: "libpvmfw.test.defaults",
     defaults: ["avf_build_flags_rust"],
     test_suites: ["general-tests"],
     test_options: {
         unit_test: true,
     },
+    prefer_rlib: true,
     rustlibs: [
         "libcstr",
+    ],
+}
+
+rust_test {
+    name: "libpvmfw.bootargs.test",
+    host_supported: true,
+    // For now, only bootargs.rs is written to be conditionally compiled with std.
+    srcs: ["src/bootargs.rs"],
+    defaults: ["libpvmfw.test.defaults"],
+    rustlibs: [
         "libzeroize",
     ],
 }
 
+rust_test {
+    name: "libpvmfw.device_assignment.test",
+    srcs: ["src/device_assignment.rs"],
+    defaults: ["libpvmfw.test.defaults"],
+    rustlibs: [
+        "liblibfdt",
+        "liblog_rust",
+        "libpvmfw_fdt_template",
+    ],
+    data: [
+        ":test_pvmfw_devices_vm_dtbo",
+        ":test_pvmfw_devices_vm_dtbo_without_symbols",
+        ":test_pvmfw_devices_with_rng",
+        ":test_pvmfw_devices_with_rng_iommu",
+        ":test_pvmfw_devices_with_multiple_devices_iommus",
+        ":test_pvmfw_devices_with_iommu_sharing",
+        ":test_pvmfw_devices_with_iommu_id_conflict",
+    ],
+    // To use libpvmfw_fdt_template for testing
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
+
+rust_test {
+    name: "libpvmfw.dice.test",
+    srcs: ["src/dice.rs"],
+    defaults: ["libpvmfw.test.defaults"],
+    rustlibs: [
+        "libcbor_util",
+        "libciborium",
+        "libdiced_open_dice_nostd",
+        "libpvmfw_avb_nostd",
+    ],
+}
+
 genrule {
     name: "test_pvmfw_devices_vm_dtbo",
     defaults: ["dts_to_dtb"],
@@ -110,39 +157,6 @@
     out: ["test_pvmfw_devices_with_iommu_id_conflict.dtb"],
 }
 
-rust_test {
-    name: "libpvmfw.device_assignment.test",
-    srcs: ["src/device_assignment.rs"],
-    defaults: ["avf_build_flags_rust"],
-    test_suites: ["general-tests"],
-    test_options: {
-        unit_test: true,
-    },
-    prefer_rlib: true,
-    rustlibs: [
-        "libcstr",
-        "liblibfdt",
-        "liblog_rust",
-        "libpvmfw_fdt_template",
-    ],
-    data: [
-        ":test_pvmfw_devices_vm_dtbo",
-        ":test_pvmfw_devices_vm_dtbo_without_symbols",
-        ":test_pvmfw_devices_with_rng",
-        ":test_pvmfw_devices_with_rng_iommu",
-        ":test_pvmfw_devices_with_multiple_devices_iommus",
-        ":test_pvmfw_devices_with_iommu_sharing",
-        ":test_pvmfw_devices_with_iommu_id_conflict",
-    ],
-    // To use libpvmfw_fdt_template for testing
-    enabled: false,
-    target: {
-        android_arm64: {
-            enabled: true,
-        },
-    },
-}
-
 cc_binary {
     name: "pvmfw",
     defaults: ["vmbase_elf_defaults"],
diff --git a/pvmfw/TEST_MAPPING b/pvmfw/TEST_MAPPING
index f21318e..e948400 100644
--- a/pvmfw/TEST_MAPPING
+++ b/pvmfw/TEST_MAPPING
@@ -10,6 +10,9 @@
     },
     {
       "name" : "libpvmfw.device_assignment.test"
+    },
+    {
+      "name" : "libpvmfw.dice.test"
     }
   ]
 }
diff --git a/pvmfw/avb/src/error.rs b/pvmfw/avb/src/error.rs
index af38c54..0f052e8 100644
--- a/pvmfw/avb/src/error.rs
+++ b/pvmfw/avb/src/error.rs
@@ -19,21 +19,20 @@
 
 /// Wrapper around `avb::SlotVerifyError` to add custom pvmfw errors.
 /// It is the error thrown by the payload verification API `verify_payload()`.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq)]
 pub enum PvmfwVerifyError {
-    /// Passthrough avb::SlotVerifyError.
-    AvbError(avb::SlotVerifyError),
+    /// Passthrough `avb::SlotVerifyError` with no `SlotVerifyData`.
+    AvbError(avb::SlotVerifyError<'static>),
     /// VBMeta has invalid descriptors.
     InvalidDescriptors(avb::IoError),
     /// Unknown vbmeta property.
     UnknownVbmetaProperty,
 }
 
-/// It's always possible to convert from an `avb::SlotVerifyError` since we are
-/// a superset.
-impl From<avb::SlotVerifyError> for PvmfwVerifyError {
+impl From<avb::SlotVerifyError<'_>> for PvmfwVerifyError {
     fn from(error: avb::SlotVerifyError) -> Self {
-        Self::AvbError(error)
+        // We don't use verification data on failure, drop it to get a `'static` lifetime.
+        Self::AvbError(error.without_verify_data())
     }
 }
 
diff --git a/pvmfw/avb/src/ops.rs b/pvmfw/avb/src/ops.rs
index c7b8b01..aee93c8 100644
--- a/pvmfw/avb/src/ops.rs
+++ b/pvmfw/avb/src/ops.rs
@@ -94,7 +94,7 @@
     pub(crate) fn verify_partition(
         &mut self,
         partition_name: &CStr,
-    ) -> Result<AvbSlotVerifyDataWrap, avb::SlotVerifyError> {
+    ) -> Result<AvbSlotVerifyDataWrap, avb::SlotVerifyError<'static>> {
         let requested_partitions = [partition_name.as_ptr(), ptr::null()];
         let ab_suffix = CStr::from_bytes_with_nul(NULL_BYTE).unwrap();
         let mut out_data = MaybeUninit::uninit();
@@ -292,7 +292,7 @@
 pub(crate) struct AvbSlotVerifyDataWrap(*mut AvbSlotVerifyData);
 
 impl TryFrom<*mut AvbSlotVerifyData> for AvbSlotVerifyDataWrap {
-    type Error = avb::SlotVerifyError;
+    type Error = avb::SlotVerifyError<'static>;
 
     fn try_from(data: *mut AvbSlotVerifyData) -> Result<Self, Self::Error> {
         is_not_null(data).map_err(|_| avb::SlotVerifyError::Io)?;
diff --git a/pvmfw/avb/src/verify.rs b/pvmfw/avb/src/verify.rs
index 492d387..3274033 100644
--- a/pvmfw/avb/src/verify.rs
+++ b/pvmfw/avb/src/verify.rs
@@ -94,7 +94,7 @@
 
 fn verify_only_one_vbmeta_exists(
     vbmeta_images: &[AvbVBMetaData],
-) -> Result<(), avb::SlotVerifyError> {
+) -> Result<(), avb::SlotVerifyError<'static>> {
     if vbmeta_images.len() == 1 {
         Ok(())
     } else {
@@ -104,7 +104,7 @@
 
 fn verify_vbmeta_is_from_kernel_partition(
     vbmeta_image: &AvbVBMetaData,
-) -> Result<(), avb::SlotVerifyError> {
+) -> Result<(), avb::SlotVerifyError<'static>> {
     match (vbmeta_image.partition_name as *const c_char).try_into() {
         Ok(PartitionName::Kernel) => Ok(()),
         _ => Err(avb::SlotVerifyError::InvalidMetadata),
@@ -113,7 +113,7 @@
 
 fn verify_vbmeta_has_only_one_hash_descriptor(
     descriptors: &Descriptors,
-) -> Result<(), avb::SlotVerifyError> {
+) -> Result<(), avb::SlotVerifyError<'static>> {
     if descriptors.num_hash_descriptor() == 1 {
         Ok(())
     } else {
@@ -125,7 +125,7 @@
     loaded_partitions: &[AvbPartitionData],
     partition_name: PartitionName,
     expected_len: usize,
-) -> Result<(), avb::SlotVerifyError> {
+) -> Result<(), avb::SlotVerifyError<'static>> {
     if loaded_partitions.len() != 1 {
         // Only one partition should be loaded in each verify result.
         return Err(avb::SlotVerifyError::Io);
@@ -140,7 +140,7 @@
     if loaded_partition.data_size == expected_len {
         Ok(())
     } else {
-        Err(avb::SlotVerifyError::Verification)
+        Err(avb::SlotVerifyError::Verification(None))
     }
 }
 
@@ -202,7 +202,7 @@
         } else if let Ok(result) = ops.verify_partition(PartitionName::InitrdDebug.as_cstr()) {
             (DebugLevel::Full, result, PartitionName::InitrdDebug)
         } else {
-            return Err(avb::SlotVerifyError::Verification.into());
+            return Err(avb::SlotVerifyError::Verification(None).into());
         };
     let loaded_partitions = initrd_verify_result.loaded_partitions()?;
     verify_loaded_partition_has_expected_length(
diff --git a/pvmfw/avb/tests/api_test.rs b/pvmfw/avb/tests/api_test.rs
index 6344433..84f83c2 100644
--- a/pvmfw/avb/tests/api_test.rs
+++ b/pvmfw/avb/tests/api_test.rs
@@ -211,7 +211,7 @@
         &load_latest_signed_kernel()?,
         /* initrd= */ &fs::read(UNSIGNED_TEST_IMG_PATH)?,
         &load_trusted_public_key()?,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )
 }
 
@@ -234,7 +234,7 @@
         &kernel,
         &load_latest_initrd_normal()?,
         &load_trusted_public_key()?,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )
 }
 
@@ -301,7 +301,7 @@
         &load_latest_signed_kernel()?,
         &initrd,
         &load_trusted_public_key()?,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )
 }
 
@@ -340,13 +340,13 @@
         &kernel,
         &load_latest_initrd_normal()?,
         &empty_public_key,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )?;
     assert_payload_verification_with_initrd_fails(
         &kernel,
         &load_latest_initrd_normal()?,
         &load_trusted_public_key()?,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )
 }
 
@@ -384,7 +384,7 @@
         &kernel,
         &load_latest_initrd_normal()?,
         &load_trusted_public_key()?,
-        avb::SlotVerifyError::Verification.into(),
+        avb::SlotVerifyError::Verification(None).into(),
     )
 }
 
diff --git a/pvmfw/src/dice.rs b/pvmfw/src/dice.rs
index 112c24c..99bf589 100644
--- a/pvmfw/src/dice.rs
+++ b/pvmfw/src/dice.rs
@@ -14,16 +14,13 @@
 
 //! Support for DICE derivation and BCC generation.
 
-use core::ffi::c_void;
 use core::mem::size_of;
-use core::slice;
 use cstr::cstr;
 use diced_open_dice::{
     bcc_format_config_descriptor, bcc_handover_main_flow, hash, Config, DiceConfigValues, DiceMode,
     Hash, InputValues, HIDDEN_SIZE,
 };
-use pvmfw_avb::{DebugLevel, Digest, VerifiedBootData};
-use vmbase::memory::flushed_zeroize;
+use pvmfw_avb::{Capability, DebugLevel, Digest, VerifiedBootData};
 
 fn to_dice_mode(debug_level: DebugLevel) -> DiceMode {
     match debug_level {
@@ -46,6 +43,7 @@
     pub auth_hash: Hash,
     pub mode: DiceMode,
     pub security_version: u64,
+    pub rkp_vm_marker: bool,
 }
 
 impl PartialInputs {
@@ -55,8 +53,9 @@
         let mode = to_dice_mode(data.debug_level);
         // We use rollback_index from vbmeta as the security_version field in dice certificate.
         let security_version = data.rollback_index;
+        let rkp_vm_marker = data.has_capability(Capability::RemoteAttest);
 
-        Ok(Self { code_hash, auth_hash, mode, security_version })
+        Ok(Self { code_hash, auth_hash, mode, security_version, rkp_vm_marker })
     }
 
     pub fn write_next_bcc(
@@ -66,15 +65,7 @@
         next_bcc: &mut [u8],
     ) -> diced_open_dice::Result<()> {
         let mut config_descriptor_buffer = [0; 128];
-        let config_values = DiceConfigValues {
-            component_name: Some(cstr!("vm_entry")),
-            security_version: if cfg!(llpvm_changes) { Some(self.security_version) } else { None },
-            ..Default::default()
-        };
-
-        let config_descriptor_size =
-            bcc_format_config_descriptor(&config_values, &mut config_descriptor_buffer)?;
-        let config = &config_descriptor_buffer[..config_descriptor_size];
+        let config = self.generate_config_descriptor(&mut config_descriptor_buffer)?;
 
         let dice_inputs = InputValues::new(
             self.code_hash,
@@ -86,17 +77,138 @@
         let _ = bcc_handover_main_flow(current_bcc_handover, &dice_inputs, next_bcc)?;
         Ok(())
     }
+
+    fn generate_config_descriptor<'a>(
+        &self,
+        config_descriptor_buffer: &'a mut [u8],
+    ) -> diced_open_dice::Result<&'a [u8]> {
+        let config_values = DiceConfigValues {
+            component_name: Some(cstr!("vm_entry")),
+            security_version: if cfg!(dice_changes) { Some(self.security_version) } else { None },
+            rkp_vm_marker: self.rkp_vm_marker,
+            ..Default::default()
+        };
+        let config_descriptor_size =
+            bcc_format_config_descriptor(&config_values, config_descriptor_buffer)?;
+        let config = &config_descriptor_buffer[..config_descriptor_size];
+        Ok(config)
+    }
 }
 
 /// Flushes data caches over the provided address range.
 ///
 /// # Safety
 ///
-/// The provided address and size must be to a valid address range (typically on the stack, .bss,
-/// .data, or provided BCC).
+/// The provided address and size must be to an address range that is valid for read and write
+/// (typically on the stack, .bss, .data, or provided BCC) from a single allocation
+/// (e.g. stack array).
 #[no_mangle]
-unsafe extern "C" fn DiceClearMemory(_ctx: *mut c_void, size: usize, addr: *mut c_void) {
-    // SAFETY: We must trust that the slice will be valid arrays/variables on the C code stack.
+#[cfg(not(test))]
+unsafe extern "C" fn DiceClearMemory(
+    _ctx: *mut core::ffi::c_void,
+    size: usize,
+    addr: *mut core::ffi::c_void,
+) {
+    use core::slice;
+    use vmbase::memory::flushed_zeroize;
+
+    // SAFETY: We require our caller to provide a valid range within a single object. The open-dice
+    // always calls this on individual stack-allocated arrays which ensures that.
     let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
     flushed_zeroize(region)
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use ciborium::Value;
+    use std::collections::HashMap;
+    use std::vec;
+
+    const COMPONENT_NAME_KEY: i64 = -70002;
+    const COMPONENT_VERSION_KEY: i64 = -70003;
+    const RESETTABLE_KEY: i64 = -70004;
+    const SECURITY_VERSION_KEY: i64 = -70005;
+    const RKP_VM_MARKER_KEY: i64 = -70006;
+
+    const BASE_VB_DATA: VerifiedBootData = VerifiedBootData {
+        debug_level: DebugLevel::None,
+        kernel_digest: [1u8; size_of::<Digest>()],
+        initrd_digest: Some([2u8; size_of::<Digest>()]),
+        public_key: b"public key",
+        capabilities: vec![],
+        rollback_index: 42,
+    };
+
+    #[test]
+    fn base_data_conversion() {
+        let vb_data = BASE_VB_DATA;
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+
+        assert_eq!(inputs.mode, DiceMode::kDiceModeNormal);
+        assert_eq!(inputs.security_version, 42);
+        assert!(!inputs.rkp_vm_marker);
+
+        // TODO(b/313608219): Consider checks for code_hash and possibly auth_hash.
+    }
+
+    #[test]
+    fn debuggable_conversion() {
+        let vb_data = VerifiedBootData { debug_level: DebugLevel::Full, ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+
+        assert_eq!(inputs.mode, DiceMode::kDiceModeDebug);
+    }
+
+    #[test]
+    fn rkp_vm_conversion() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+
+        assert!(inputs.rkp_vm_marker);
+    }
+
+    #[test]
+    fn base_config_descriptor() {
+        let vb_data = BASE_VB_DATA;
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs);
+
+        assert_eq!(config_map.get(&COMPONENT_NAME_KEY).unwrap().as_text().unwrap(), "vm_entry");
+        assert_eq!(config_map.get(&COMPONENT_VERSION_KEY), None);
+        assert_eq!(config_map.get(&RESETTABLE_KEY), None);
+        if cfg!(dice_changes) {
+            assert_eq!(
+                config_map.get(&SECURITY_VERSION_KEY).unwrap().as_integer().unwrap(),
+                42.into()
+            );
+        } else {
+            assert_eq!(config_map.get(&SECURITY_VERSION_KEY), None);
+        }
+        assert_eq!(config_map.get(&RKP_VM_MARKER_KEY), None);
+    }
+
+    #[test]
+    fn config_descriptor_with_rkp_vm() {
+        let vb_data =
+            VerifiedBootData { capabilities: vec![Capability::RemoteAttest], ..BASE_VB_DATA };
+        let inputs = PartialInputs::new(&vb_data).unwrap();
+        let config_map = decode_config_descriptor(&inputs);
+
+        assert!(config_map.get(&RKP_VM_MARKER_KEY).unwrap().is_null());
+    }
+
+    fn decode_config_descriptor(inputs: &PartialInputs) -> HashMap<i64, Value> {
+        let mut buffer = [0; 128];
+        let config_descriptor = inputs.generate_config_descriptor(&mut buffer).unwrap();
+
+        let cbor_map =
+            cbor_util::deserialize::<Value>(config_descriptor).unwrap().into_map().unwrap();
+
+        cbor_map
+            .into_iter()
+            .map(|(k, v)| ((k.into_integer().unwrap().try_into().unwrap()), v))
+            .collect()
+    }
+}
diff --git a/rialto/Android.bp b/rialto/Android.bp
index 728c1eb..1ab02e9 100644
--- a/rialto/Android.bp
+++ b/rialto/Android.bp
@@ -109,17 +109,21 @@
         "android.system.virtualizationservice-rust",
         "libandroid_logger",
         "libanyhow",
+        "libbssl_avf_nostd",
         "libciborium",
         "libclient_vm_csr",
+        "libcoset",
         "libdiced_sample_inputs",
         "liblibc",
         "liblog_rust",
         "libservice_vm_comm",
         "libservice_vm_manager",
         "libvmclient",
+        "libx509_parser",
     ],
     data: [
         ":rialto_unsigned",
+        ":test_rkp_cert_chain",
     ],
     test_suites: ["general-tests"],
     enabled: false,
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 0f59350..85c3efe 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -22,22 +22,32 @@
     binder::{ParcelFileDescriptor, ProcessState},
 };
 use anyhow::{bail, Context, Result};
+use bssl_avf::{sha256, EcKey, EvpPKey};
 use ciborium::value::Value;
 use client_vm_csr::generate_attestation_key_and_csr;
+use coset::{CborSerializable, CoseMac0, CoseSign};
 use log::info;
 use service_vm_comm::{
-    ClientVmAttestationParams, EcdsaP256KeyPair, GenerateCertificateRequestParams, Request,
-    RequestProcessingError, Response, VmType,
+    ClientVmAttestationParams, Csr, CsrPayload, EcdsaP256KeyPair, GenerateCertificateRequestParams,
+    Request, Response, VmType,
 };
 use service_vm_manager::ServiceVm;
+use std::fs;
 use std::fs::File;
 use std::io;
 use std::panic;
 use std::path::PathBuf;
 use vmclient::VmInstance;
+use x509_parser::{
+    certificate::X509Certificate,
+    der_parser::{der::parse_der, oid, oid::Oid},
+    prelude::FromDer,
+    x509::{AlgorithmIdentifier, SubjectPublicKeyInfo, X509Version},
+};
 
 const UNSIGNED_RIALTO_PATH: &str = "/data/local/tmp/rialto_test/arm64/rialto_unsigned.bin";
 const INSTANCE_IMG_PATH: &str = "/data/local/tmp/rialto_test/arm64/instance.img";
+const TEST_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
 
 #[test]
 fn process_requests_in_protected_vm() -> Result<()> {
@@ -55,7 +65,7 @@
     check_processing_reverse_request(&mut vm)?;
     let key_pair = check_processing_generating_key_pair_request(&mut vm)?;
     check_processing_generating_certificate_request(&mut vm, &key_pair.maced_public_key)?;
-    check_attestation_request(&mut vm, &key_pair.key_blob)?;
+    check_attestation_request(&mut vm, &key_pair)?;
     Ok(())
 }
 
@@ -110,7 +120,10 @@
     }
 }
 
-fn check_attestation_request(vm: &mut ServiceVm, key_blob: &[u8]) -> Result<()> {
+fn check_attestation_request(
+    vm: &mut ServiceVm,
+    remotely_provisioned_key_pair: &EcdsaP256KeyPair,
+) -> Result<()> {
     /// The following data was generated randomly with urandom.
     const CHALLENGE: [u8; 16] = [
         0x7d, 0x86, 0x58, 0x79, 0x3a, 0x09, 0xdf, 0x1c, 0xa5, 0x80, 0x80, 0x15, 0x2b, 0x13, 0x17,
@@ -118,10 +131,18 @@
     ];
     let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()?;
     let attestation_data = generate_attestation_key_and_csr(&CHALLENGE, &dice_artifacts)?;
+    let cert_chain = fs::read(TEST_CERT_CHAIN_PATH)?;
+    let (remaining, cert) = X509Certificate::from_der(&cert_chain)?;
 
+    // Builds the mock parameters for the client VM attestation.
+    // The `csr` and `remotely_provisioned_key_blob` parameters are extracted from the same
+    // libraries as in production.
+    // The `remotely_provisioned_cert` parameter is an RKP certificate extracted from a test
+    // certificate chain retrieved from RKPD.
     let params = ClientVmAttestationParams {
-        csr: attestation_data.csr.into_cbor_vec()?,
-        remotely_provisioned_key_blob: key_blob.to_vec(),
+        csr: attestation_data.csr.clone().into_cbor_vec()?,
+        remotely_provisioned_key_blob: remotely_provisioned_key_pair.key_blob.to_vec(),
+        remotely_provisioned_cert: cert_chain[..(cert_chain.len() - remaining.len())].to_vec(),
     };
     let request = Request::RequestClientVmAttestation(params);
 
@@ -129,12 +150,76 @@
     info!("Received response: {response:?}.");
 
     match response {
-        // TODO(b/309441500): Check the certificate once it is implemented.
-        Response::Err(RequestProcessingError::OperationUnimplemented) => Ok(()),
+        Response::RequestClientVmAttestation(certificate) => {
+            check_certificate_for_client_vm(
+                &certificate,
+                &remotely_provisioned_key_pair.maced_public_key,
+                &attestation_data.csr,
+                &cert,
+            )?;
+            Ok(())
+        }
         _ => bail!("Incorrect response type: {response:?}"),
     }
 }
 
+fn check_certificate_for_client_vm(
+    certificate: &[u8],
+    maced_public_key: &[u8],
+    csr: &Csr,
+    parent_certificate: &X509Certificate,
+) -> Result<()> {
+    let cose_mac = CoseMac0::from_slice(maced_public_key)?;
+    let authority_public_key = EcKey::from_cose_public_key(&cose_mac.payload.unwrap()).unwrap();
+    let (remaining, cert) = X509Certificate::from_der(certificate)?;
+    assert!(remaining.is_empty());
+
+    // Checks the certificate signature against the authority public key.
+    const ECDSA_WITH_SHA_256: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .2);
+    let expected_algorithm =
+        AlgorithmIdentifier { algorithm: ECDSA_WITH_SHA_256, parameters: None };
+    assert_eq!(expected_algorithm, cert.signature_algorithm);
+    let digest = sha256(cert.tbs_certificate.as_ref()).unwrap();
+    authority_public_key
+        .ecdsa_verify(cert.signature_value.as_ref(), &digest)
+        .expect("Failed to verify the certificate signature with the authority public key");
+
+    // Checks that the certificate's subject public key is equal to the key in the CSR.
+    let cose_sign = CoseSign::from_slice(&csr.signed_csr_payload)?;
+    let csr_payload =
+        cose_sign.payload.as_ref().and_then(|v| CsrPayload::from_cbor_slice(v).ok()).unwrap();
+    let subject_public_key = EcKey::from_cose_public_key(&csr_payload.public_key).unwrap();
+    let expected_spki_data =
+        EvpPKey::try_from(subject_public_key).unwrap().subject_public_key_info().unwrap();
+    let (remaining, expected_spki) = SubjectPublicKeyInfo::from_der(&expected_spki_data)?;
+    assert!(remaining.is_empty());
+    assert_eq!(&expected_spki, cert.public_key());
+
+    // Checks the certificate extension.
+    const ATTESTATION_EXTENSION_OID: Oid<'static> = oid!(1.3.6 .1 .4 .1 .11129 .2 .1 .29 .1);
+    let extensions = cert.extensions();
+    assert_eq!(1, extensions.len());
+    let extension = &extensions[0];
+    assert_eq!(ATTESTATION_EXTENSION_OID, extension.oid);
+    assert!(!extension.critical);
+    let (remaining, extension) = parse_der(extension.value)?;
+    assert!(remaining.is_empty());
+    let attestation_ext = extension.as_sequence()?;
+    assert_eq!(1, attestation_ext.len());
+    assert_eq!(csr_payload.challenge, attestation_ext[0].as_slice()?);
+
+    // Checks other fields on the certificate
+    assert_eq!(X509Version::V3, cert.version());
+    assert_eq!(parent_certificate.validity(), cert.validity());
+    assert_eq!(
+        String::from("CN=Android Protected Virtual Machine Key"),
+        cert.subject().to_string()
+    );
+    assert_eq!(parent_certificate.subject(), cert.issuer());
+
+    Ok(())
+}
+
 /// TODO(b/300625792): Check the CSR with libhwtrust once the CSR is complete.
 fn check_csr(csr: Vec<u8>) -> Result<()> {
     let mut reader = io::Cursor::new(csr);
diff --git a/secretkeeper/comm/Android.bp b/secretkeeper/comm/Android.bp
new file mode 100644
index 0000000..cb3e713
--- /dev/null
+++ b/secretkeeper/comm/Android.bp
@@ -0,0 +1,52 @@
+/*
+ * 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libsecretkeeper_comm.defaults",
+    crate_name: "secretkeeper_comm",
+    defaults: ["avf_build_flags_rust"],
+    edition: "2021",
+    lints: "android",
+    rustlibs: [
+        "libciborium",
+        "libcoset",
+    ],
+    proc_macros: ["libenumn"],
+    vendor_available: true,
+}
+
+rust_library {
+    name: "libsecretkeeper_comm_nostd",
+    defaults: ["libsecretkeeper_comm.defaults"],
+    srcs: ["src/lib.rs"],
+}
+
+rust_test {
+    name: "libsecretkeeper_comm.test",
+    defaults: [
+        "libsecretkeeper_comm.defaults",
+        "rdroidtest.defaults",
+    ],
+    srcs: ["tests/*.rs"],
+    test_suites: ["general-tests"],
+    rustlibs: [
+        "libsecretkeeper_comm_nostd",
+    ],
+}
diff --git a/secretkeeper/comm/src/cbor_convert.rs b/secretkeeper/comm/src/cbor_convert.rs
new file mode 100644
index 0000000..ab6ca3f
--- /dev/null
+++ b/secretkeeper/comm/src/cbor_convert.rs
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+//! Implements various useful CBOR conversion method.
+
+use crate::data_types::error::Error;
+use alloc::vec::Vec;
+use ciborium::Value;
+
+/// Decodes the provided binary CBOR-encoded value and returns a
+/// [`ciborium::Value`] struct wrapped in Result.
+pub fn value_from_bytes(mut bytes: &[u8]) -> Result<Value, Error> {
+    let value = ciborium::de::from_reader(&mut bytes).map_err(|_| Error::ConversionError)?;
+    // Ciborium tries to read one Value, but doesn't care if there is trailing data after it. We do
+    if !bytes.is_empty() {
+        return Err(Error::ConversionError);
+    }
+    Ok(value)
+}
+
+/// Encodes a [`ciborium::Value`] into bytes.
+pub fn value_to_bytes(value: &Value) -> Result<Vec<u8>, Error> {
+    let mut bytes: Vec<u8> = Vec::new();
+    ciborium::ser::into_writer(&value, &mut bytes).map_err(|_| Error::UnexpectedError)?;
+    Ok(bytes)
+}
+
+// Useful to convert [`ciborium::Value`] to integer, we return largest integer range for
+// convenience, callers should downcast into appropriate type.
+pub fn value_to_integer(value: &Value) -> Result<i128, Error> {
+    let num = value.as_integer().ok_or(Error::ConversionError)?.into();
+    Ok(num)
+}
diff --git a/secretkeeper/comm/src/data_types/error.rs b/secretkeeper/comm/src/data_types/error.rs
new file mode 100644
index 0000000..6a5e24f
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/error.rs
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+//! Error-like data structures. See `ResponsePacketError` in the CDDL
+
+// derive(N) generates a method that is missing a docstring.
+#![allow(missing_docs)]
+
+use crate::cbor_convert::value_to_integer;
+use crate::data_types::response::Response;
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use ciborium::Value;
+use enumn::N;
+
+/// 'Error code' corresponding to successful response.
+pub const ERROR_OK: u16 = 0; // All real errors must have non-zero error_codes
+
+/// Errors from Secretkeeper API. Keep in sync with `ErrorCode` defined for Secretkeeper HAL
+/// at SecretManagement.cddl
+#[derive(Clone, Copy, Debug, Eq, N, PartialEq)]
+pub enum SecretkeeperError {
+    // This is the Error code used if no other error codes explains the issue.
+    UnexpectedServerError = 1,
+    // Indicates the Request was malformed & hence couldn't be served.
+    RequestMalformed = 2,
+    // TODO(b/291228655): Add other errors such as DicePolicyError.
+}
+
+// [`SecretkeeperError`] is a valid [`Response`] type.
+// For more information see `ErrorCode` in SecretManagement.cddl alongside ISecretkeeper.aidl
+impl Response for SecretkeeperError {
+    fn new(response_cbor: Vec<Value>) -> Result<Box<Self>, Error> {
+        // TODO(b/291228655): This method currently discards the second value in response_cbor,
+        // which contains additional human-readable context in error. Include it!
+        if response_cbor.is_empty() || response_cbor.len() > 2 {
+            return Err(Error::ResponseMalformed);
+        }
+        let error_code: u16 = value_to_integer(&response_cbor[0])?.try_into()?;
+        SecretkeeperError::n(error_code)
+            .map_or_else(|| Err(Error::ResponseMalformed), |sk_err| Ok(Box::new(sk_err)))
+    }
+
+    fn error_code(&self) -> u16 {
+        *self as u16
+    }
+}
+
+/// Errors thrown internally by the library.
+#[derive(Debug, PartialEq)]
+pub enum Error {
+    /// Request was malformed.
+    RequestMalformed,
+    /// Response received from the server was malformed.
+    ResponseMalformed,
+    /// An error happened when serializing to/from a [`Value`].
+    CborValueError,
+    /// An error happened while casting a type to different type,
+    /// including one [`Value`] type to another.
+    ConversionError,
+    /// These are unexpected errors, which should never really happen.
+    UnexpectedError,
+}
+
+impl From<ciborium::value::Error> for Error {
+    fn from(_e: ciborium::value::Error) -> Self {
+        Self::CborValueError
+    }
+}
+
+impl From<ciborium::Value> for Error {
+    fn from(_e: ciborium::Value) -> Self {
+        Self::ConversionError
+    }
+}
+
+impl From<core::num::TryFromIntError> for Error {
+    fn from(_e: core::num::TryFromIntError) -> Self {
+        Self::ConversionError
+    }
+}
diff --git a/secretkeeper/comm/src/data_types/mod.rs b/secretkeeper/comm/src/data_types/mod.rs
new file mode 100644
index 0000000..096777f
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/mod.rs
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+//! Implements the data structures specified by SecretManagement.cddl in Secretkeeper HAL.
+//!  Data structures specified by SecretManagement.cddl in Secretkeeper HAL.
+//!  Note this library must stay in sync with:
+//!      platform/hardware/interfaces/security/\
+//!      secretkeeper/aidl/android/hardware/security/secretkeeper/SecretManagement.cddl
+
+pub mod error;
+pub mod packet;
+pub mod request;
+pub mod request_response_impl;
+pub mod response;
diff --git a/secretkeeper/comm/src/data_types/packet.rs b/secretkeeper/comm/src/data_types/packet.rs
new file mode 100644
index 0000000..7a1e575
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/packet.rs
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+//! Defines the packet structures passed between functional layer & the layer below.
+
+pub use ciborium::Value;
+
+use crate::cbor_convert::{value_from_bytes, value_to_bytes, value_to_integer};
+use crate::data_types::error::Error;
+use crate::data_types::error::ERROR_OK;
+use crate::data_types::request_response_impl::Opcode;
+use alloc::vec::Vec;
+
+/// Encapsulate Request-like data that functional layer operates on. All structures
+/// that implements `data_types::request::Request` can be serialized to [`ResponsePacket`].
+/// Similarly all [`RequestPacket`] can be deserialized to concrete Requests.
+/// Keep in sync with HAL spec (in particular RequestPacket):
+///     security/secretkeeper/aidl/android/hardware/security/secretkeeper/SecretManagement.cddl
+#[derive(Clone, Debug, PartialEq)]
+pub struct RequestPacket(Vec<Value>);
+
+impl RequestPacket {
+    /// Construct a [`RequestPacket`] from array of `ciborium::Value`
+    pub fn from(request_cbor: Vec<Value>) -> Self {
+        Self(request_cbor)
+    }
+
+    /// Get the containing CBOR. This can be used for getting concrete response objects.
+    /// Keep in sync with [`crate::data_types::request::Request::serialize_to_packet()`]
+    pub fn into_inner(self) -> Vec<Value> {
+        self.0
+    }
+
+    /// Extract [`Opcode`] corresponding to this packet. As defined in by the spec, this is
+    /// the first value in the CBOR array.
+    pub fn opcode(&self) -> Result<Opcode, Error> {
+        if self.0.is_empty() {
+            return Err(Error::RequestMalformed);
+        }
+        let num: u16 = value_to_integer(&self.0[0])?.try_into()?;
+
+        Opcode::n(num).ok_or(Error::RequestMalformed)
+    }
+
+    /// Serialize the [`ResponsePacket`] to bytes
+    pub fn into_bytes(self) -> Result<Vec<u8>, Error> {
+        value_to_bytes(&Value::Array(self.0))
+    }
+
+    /// Deserialize the bytes into [`ResponsePacket`]
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
+        Ok(RequestPacket(value_from_bytes(bytes)?.into_array()?))
+    }
+}
+
+/// Encapsulate Response like data that the functional layer operates on. All structures
+/// that implements `data_types::response::Response` can be serialized to [`ResponsePacket`].
+/// Similarly all [`ResponsePacket`] can be deserialized to concrete Response.
+#[derive(Clone, Debug, PartialEq)]
+pub struct ResponsePacket(Vec<Value>);
+
+impl ResponsePacket {
+    /// Construct a [`ResponsePacket`] from array of `ciborium::Value`
+    pub fn from(response_cbor: Vec<Value>) -> Self {
+        Self(response_cbor)
+    }
+
+    /// Get raw content. This can be used for getting concrete response objects.
+    /// Keep in sync with `crate::data_types::response::Response::serialize_to_packet`
+    pub fn into_inner(self) -> Vec<Value> {
+        self.0
+    }
+
+    /// A [`ResponsePacket`] encapsulates different types of responses, find which one!
+    pub fn response_type(&self) -> Result<ResponseType, Error> {
+        if self.0.is_empty() {
+            return Err(Error::ResponseMalformed);
+        }
+        let error_code: u16 = value_to_integer(&self.0[0])?.try_into()?;
+        if error_code == ERROR_OK {
+            Ok(ResponseType::Success)
+        } else {
+            Ok(ResponseType::Error)
+        }
+    }
+
+    /// Serialize the [`ResponsePacket`] to bytes
+    pub fn into_bytes(self) -> Result<Vec<u8>, Error> {
+        value_to_bytes(&Value::Array(self.0))
+    }
+
+    /// Deserialize the bytes into [`ResponsePacket`]
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
+        Ok(ResponsePacket(value_from_bytes(bytes)?.into_array()?))
+    }
+}
+
+/// Responses can be different type - `Success`-like or `Error`-like.
+#[derive(Debug, Eq, PartialEq)]
+pub enum ResponseType {
+    /// Indicates successful operation. See `ResponsePacketSuccess` in SecretManagement.cddl
+    Success,
+    /// Indicate failed operation. See `ResponsePacketError` in SecretManagement.cddl
+    Error,
+}
diff --git a/secretkeeper/comm/src/data_types/request.rs b/secretkeeper/comm/src/data_types/request.rs
new file mode 100644
index 0000000..0d54bcd
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/request.rs
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+//! Defines the shared behaviour of all request like data structures.
+
+use crate::data_types::error::Error;
+use crate::data_types::packet::RequestPacket;
+use crate::data_types::request_response_impl::Opcode;
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use ciborium::Value;
+
+/// Collection of methods defined for Secretkeeper's request-like data structures,
+/// e.g. `GetVersionRequestPacket` in the HAL spec.
+///
+/// Keep in sync with SecretManagement.cddl, in particular `RequestPacket` type.
+pub trait Request {
+    /// [`Opcode`] of the request: Each Request type is associated with an opcode. See `Opcode` in
+    /// SecretManagement.cddl.
+    const OPCODE: Opcode;
+
+    /// Constructor of the [`Request`] object. Implementation of this constructor should check
+    /// the args' type adheres to the HAL spec.
+    ///
+    /// # Arguments
+    /// * `args` - The vector of arguments associated with this request. Each argument is a
+    ///   `ciborium::Value` type. See `Params` in `RequestPacket` in SecretManagement.cddl
+    fn new(args: Vec<Value>) -> Result<Box<Self>, Error>;
+
+    /// Get the 'arguments' of this request.
+    fn args(&self) -> Vec<Value>;
+
+    /// Serialize the request to a [`RequestPacket`], which, as per SecretManagement.cddl is:
+    /// ```
+    ///      RequestPacket<Opcode, Params> = [
+    ///         Opcode,
+    ///         Params
+    ///      ]
+    /// ```
+    fn serialize_to_packet(&self) -> RequestPacket {
+        let mut res = self.args();
+        res.insert(0, Value::from(Self::OPCODE as u16));
+        RequestPacket::from(res)
+    }
+
+    /// Construct the [`Request`] struct from given [`RequestPacket`].
+    fn deserialize_from_packet(packet: RequestPacket) -> Result<Box<Self>, Error> {
+        let mut req = packet.into_inner();
+        if req.get(0) != Some(&Value::from(Self::OPCODE as u16)) {
+            return Err(Error::RequestMalformed);
+        }
+        req.remove(0);
+        Self::new(req)
+    }
+}
diff --git a/secretkeeper/comm/src/data_types/request_response_impl.rs b/secretkeeper/comm/src/data_types/request_response_impl.rs
new file mode 100644
index 0000000..a7d29cc
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/request_response_impl.rs
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+//! Implementation of request & response like data structures.
+
+// derive(N) generates a method that is missing a docstring.
+#![allow(missing_docs)]
+
+use crate::cbor_convert::value_to_integer;
+use crate::data_types::error::Error;
+use crate::data_types::error::ERROR_OK;
+use crate::data_types::request::Request;
+use crate::data_types::response::Response;
+use alloc::boxed::Box;
+use alloc::vec;
+use alloc::vec::Vec;
+use ciborium::Value;
+use enumn::N;
+
+/// Set of all possible `Opcode` supported by SecretManagement API of the HAL.
+/// See `Opcode` in SecretManagement.cddl
+#[derive(Clone, Copy, Debug, N, PartialEq)]
+#[non_exhaustive]
+pub enum Opcode {
+    /// Get version of the SecretManagement API.
+    GetVersion = 1,
+    /// Store a secret
+    StoreSecret = 2,
+    /// Get the secret
+    GetSecret = 3,
+}
+
+/// Corresponds to `GetVersionRequestPacket` defined in SecretManagement.cddl
+#[derive(Debug, Eq, PartialEq)]
+pub struct GetVersionRequest;
+
+impl Request for GetVersionRequest {
+    const OPCODE: Opcode = Opcode::GetVersion;
+
+    fn new(args: Vec<Value>) -> Result<Box<Self>, Error> {
+        if !args.is_empty() {
+            return Err(Error::RequestMalformed);
+        }
+        Ok(Box::new(Self))
+    }
+
+    fn args(&self) -> Vec<Value> {
+        Vec::new()
+    }
+}
+
+/// Success response corresponding to `GetVersionResponsePacket`.
+#[derive(Debug, Eq, PartialEq)]
+pub struct GetVersionResponse {
+    /// Version of SecretManagement API
+    version: u64,
+}
+
+impl GetVersionResponse {
+    pub fn new(version: u64) -> Self {
+        Self { version }
+    }
+    pub fn version(&self) -> u64 {
+        self.version
+    }
+}
+
+impl Response for GetVersionResponse {
+    fn new(res: Vec<Value>) -> Result<Box<Self>, Error> {
+        if res.len() != 2 {
+            return Err(Error::ResponseMalformed);
+        }
+        let error_code: u16 = value_to_integer(&res[0])?.try_into()?;
+        if error_code != ERROR_OK {
+            return Err(Error::ResponseMalformed);
+        }
+        let version: u64 = value_to_integer(&res[1])?.try_into()?;
+        Ok(Box::new(Self::new(version)))
+    }
+
+    fn result(&self) -> Vec<Value> {
+        vec![self.version.into()]
+    }
+}
diff --git a/secretkeeper/comm/src/data_types/response.rs b/secretkeeper/comm/src/data_types/response.rs
new file mode 100644
index 0000000..e975ebc
--- /dev/null
+++ b/secretkeeper/comm/src/data_types/response.rs
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+//! Defines the shared behaviour of all response like data structures.
+
+use crate::data_types::error::{Error, ERROR_OK};
+use crate::data_types::packet::ResponsePacket;
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use ciborium::Value;
+
+/// Shared behaviour of all Secretkeeper's response-like data structures,
+/// e.g. `GetVersionResponsePacket`. Note - A valid [`Response`] can be error as well, like
+/// `SecretkeeperError::RequestMalformed`.
+///
+/// Keep in sync with SecretManagement.cddl, in particular `ResponsePacket` type.
+pub trait Response {
+    /// Constructor of the Response object.
+    /// # Arguments
+    /// * `response_cbor`: A vector of `[ciborium::Value]` such that:
+    /// ```
+    ///     For success-like responses:
+    ///         ResponsePacketSuccess = [
+    ///             0,                          ; Indicates successful Response
+    ///             result : Result
+    ///         ]
+    ///     For error responses:
+    ///         ResponsePacketError = [
+    ///             error_code: ErrorCode,      ; Indicate the error
+    ///             error_message: tstr         ; Additional human-readable context
+    ///         ]
+    /// ```
+    /// See ResponsePacket<Result> in SecretManagement.cddl alongside ISecretkeeper.aidl
+    fn new(response_cbor: Vec<Value>) -> Result<Box<Self>, Error>;
+
+    /// The result in the `Response`. By default this is empty, but [`Response`] structures like
+    /// `GetVersionResponse` must overwrite these to return the expected non-empty result.
+    fn result(&self) -> Vec<Value> {
+        Vec::new()
+    }
+
+    /// Error code corresponding to the response. The default value is 0 but that will work only
+    /// for successful responses. Error-like response structures must overwrite this method.
+    fn error_code(&self) -> u16 {
+        ERROR_OK // Indicates success
+    }
+
+    /// Serialize the response to a [`ResponsePacket`].
+    fn serialize_to_packet(&self) -> ResponsePacket {
+        let mut res = self.result();
+        res.insert(0, Value::from(self.error_code()));
+        ResponsePacket::from(res)
+    }
+
+    /// Construct the response struct from given [`ResponsePacket`].
+    fn deserialize_from_packet(packet: ResponsePacket) -> Result<Box<Self>, Error> {
+        let res = packet.into_inner();
+        // Empty response packet is not allowed, all responses in Secretkeeper HAL at least
+        // have `error_code` or '0'; so throw an error!
+        if res.is_empty() {
+            return Err(Error::ResponseMalformed);
+        }
+        Self::new(res)
+    }
+}
diff --git a/secretkeeper/comm/src/lib.rs b/secretkeeper/comm/src/lib.rs
new file mode 100644
index 0000000..9a10ac0
--- /dev/null
+++ b/secretkeeper/comm/src/lib.rs
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+//! This library exposes data structures and methods that can be used by Secretkeeper HAL & client
+//! implementation. This is compatible with Secretkeeper HAL specification.
+
+#![no_std]
+extern crate alloc;
+
+mod cbor_convert;
+pub mod data_types;
diff --git a/secretkeeper/comm/tests/data_types.rs b/secretkeeper/comm/tests/data_types.rs
new file mode 100644
index 0000000..68964fd
--- /dev/null
+++ b/secretkeeper/comm/tests/data_types.rs
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+
+//! Unit tests for testing serialization & deserialization of exported data_types.
+
+use ciborium::Value;
+use secretkeeper_comm::data_types::error::{Error, SecretkeeperError, ERROR_OK};
+use secretkeeper_comm::data_types::packet::{RequestPacket, ResponsePacket, ResponseType};
+use secretkeeper_comm::data_types::request::Request;
+use secretkeeper_comm::data_types::request_response_impl::Opcode;
+use secretkeeper_comm::data_types::request_response_impl::{GetVersionRequest, GetVersionResponse};
+use secretkeeper_comm::data_types::response::Response;
+
+#[cfg(test)]
+rdroidtest::test_main!();
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rdroidtest::test;
+
+    test!(request_serialization_deserialization);
+    fn request_serialization_deserialization() {
+        let req = GetVersionRequest {};
+        let packet = req.serialize_to_packet();
+        assert_eq!(packet.opcode().unwrap(), Opcode::GetVersion);
+        assert_eq!(
+            RequestPacket::from_bytes(&packet.clone().into_bytes().unwrap()).unwrap(),
+            packet
+        );
+        let req_deserialized = *GetVersionRequest::deserialize_from_packet(packet).unwrap();
+        assert_eq!(req, req_deserialized);
+    }
+
+    test!(success_response_serialization_deserialization);
+    fn success_response_serialization_deserialization() {
+        let response = GetVersionResponse::new(1);
+        let packet = response.serialize_to_packet();
+        assert_eq!(packet.response_type().unwrap(), ResponseType::Success);
+        assert_eq!(
+            ResponsePacket::from_bytes(&packet.clone().into_bytes().unwrap()).unwrap(),
+            packet
+        );
+        let response_deserialized = *GetVersionResponse::deserialize_from_packet(packet).unwrap();
+        assert_eq!(response, response_deserialized);
+    }
+
+    test!(error_response_serialization_deserialization);
+    fn error_response_serialization_deserialization() {
+        let response = SecretkeeperError::RequestMalformed;
+        let packet = response.serialize_to_packet();
+        assert_eq!(packet.response_type().unwrap(), ResponseType::Error);
+        assert_eq!(
+            ResponsePacket::from_bytes(&packet.clone().into_bytes().unwrap()).unwrap(),
+            packet
+        );
+        let response_deserialized = *SecretkeeperError::deserialize_from_packet(packet).unwrap();
+        assert_eq!(response, response_deserialized);
+    }
+
+    test!(request_creation);
+    fn request_creation() {
+        let req: GetVersionRequest = *Request::new(vec![]).unwrap();
+        assert_eq!(req, GetVersionRequest {});
+    }
+
+    test!(response_creation);
+    fn response_creation() {
+        let res: GetVersionResponse =
+            *Response::new(vec![Value::from(ERROR_OK), Value::from(5)]).unwrap();
+        assert_eq!(res.version(), 5);
+    }
+
+    test!(invalid_get_version_request_creation);
+    fn invalid_get_version_request_creation() {
+        // A request with non-zero arg is considered invalid.
+        assert_eq!(
+            <GetVersionRequest as Request>::new(vec![Value::Null]).unwrap_err(),
+            Error::RequestMalformed
+        );
+    }
+
+    test!(invalid_get_version_response_creation);
+    fn invalid_get_version_response_creation() {
+        // A response with non-zero error_code is an invalid success response.
+        assert_eq!(
+            <GetVersionResponse as Response>::new(vec![
+                Value::from(SecretkeeperError::RequestMalformed as u16),
+                Value::from(5)
+            ])
+            .unwrap_err(),
+            Error::ResponseMalformed
+        );
+
+        // A response with incorrect size of array is invalid.
+        assert_eq!(
+            <GetVersionResponse as Response>::new(vec![
+                Value::from(ERROR_OK),
+                Value::from(5),
+                Value::from(7)
+            ])
+            .unwrap_err(),
+            Error::ResponseMalformed
+        );
+
+        // A response with incorrect type is invalid.
+        <GetVersionResponse as Response>::new(vec![Value::from(ERROR_OK), Value::from("a tstr")])
+            .unwrap_err();
+    }
+
+    test!(invalid_error_response_creation);
+    fn invalid_error_response_creation() {
+        // A response with ERROR_OK(0) as the error_code is an invalid error response.
+        assert_eq!(
+            <SecretkeeperError as Response>::new(vec![Value::from(ERROR_OK)]).unwrap_err(),
+            Error::ResponseMalformed
+        );
+    }
+}
diff --git a/secretkeeper/dice_policy/src/lib.rs b/secretkeeper/dice_policy/src/lib.rs
index 2e91305..076ba3b 100644
--- a/secretkeeper/dice_policy/src/lib.rs
+++ b/secretkeeper/dice_policy/src/lib.rs
@@ -213,10 +213,10 @@
         ConstraintType::GreaterOrEqual => {
             let value_in_node = value_in_node
                 .as_integer()
-                .ok_or(anyhow!("Mismatch type: expected a cbor integer"))?;
+                .ok_or(anyhow!("Mismatch type: expected a CBOR integer"))?;
             let value_min = value_in_constraint
                 .as_integer()
-                .ok_or(anyhow!("Mismatch type: expected a cbor integer"))?;
+                .ok_or(anyhow!("Mismatch type: expected a CBOR integer"))?;
             ensure!(value_in_node >= value_min);
         }
     };
@@ -260,9 +260,9 @@
         Value::Bytes(b) => value_from_bytes(b)?
             .into_map()
             .map(Cow::Owned)
-            .map_err(|e| anyhow!("Expected a cbor map: {:?}", e)),
+            .map_err(|e| anyhow!("Expected a CBOR map: {:?}", e)),
         Value::Map(map) => Ok(Cow::Borrowed(map)),
-        _ => bail!("/Expected a cbor map {:?}", cbor_map),
+        _ => bail!("Expected a CBOR map {:?}", cbor_map),
     }
 }
 
diff --git a/service_vm/comm/Android.bp b/service_vm/comm/Android.bp
index 6e05587..bdfc099 100644
--- a/service_vm/comm/Android.bp
+++ b/service_vm/comm/Android.bp
@@ -24,6 +24,7 @@
         "libbssl_avf_error_nostd",
         "libciborium_nostd",
         "libcoset_nostd",
+        "libder_nostd",
         "liblog_rust_nostd",
         "libserde_nostd",
     ],
diff --git a/service_vm/comm/src/message.rs b/service_vm/comm/src/message.rs
index 6dd0ccd..87c8378 100644
--- a/service_vm/comm/src/message.rs
+++ b/service_vm/comm/src/message.rs
@@ -66,6 +66,13 @@
 
     /// The key blob retrieved from RKPD by virtualizationservice.
     pub remotely_provisioned_key_blob: Vec<u8>,
+
+    /// The leaf certificate of the certificate chain retrieved from RKPD by
+    /// virtualizationservice.
+    ///
+    /// This certificate is a DER-encoded X.509 certificate that includes the remotely
+    /// provisioned public key.
+    pub remotely_provisioned_cert: Vec<u8>,
 }
 
 /// Represents a response to a request sent to the service VM.
@@ -120,6 +127,9 @@
 
     /// The requested operation has not been implemented.
     OperationUnimplemented,
+
+    /// An error happened during the DER encoding/decoding.
+    DerError,
 }
 
 impl fmt::Display for RequestProcessingError {
@@ -142,6 +152,9 @@
             Self::OperationUnimplemented => {
                 write!(f, "The requested operation has not been implemented")
             }
+            Self::DerError => {
+                write!(f, "An error happened during the DER encoding/decoding")
+            }
         }
     }
 }
@@ -166,6 +179,14 @@
     }
 }
 
+#[cfg(not(feature = "std"))]
+impl From<der::Error> for RequestProcessingError {
+    fn from(e: der::Error) -> Self {
+        error!("DER encoding/decoding error: {e}");
+        Self::DerError
+    }
+}
+
 /// Represents the params passed to GenerateCertificateRequest
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct GenerateCertificateRequestParams {
diff --git a/service_vm/requests/Android.bp b/service_vm/requests/Android.bp
index ecede8b..52b54b4 100644
--- a/service_vm/requests/Android.bp
+++ b/service_vm/requests/Android.bp
@@ -21,10 +21,13 @@
         "libcbor_util_nostd",
         "libciborium_nostd",
         "libcoset_nostd",
+        "libder_nostd",
         "libdiced_open_dice_nostd",
         "liblog_rust_nostd",
         "libserde_nostd",
         "libservice_vm_comm_nostd",
+        "libspki_nostd",
+        "libx509_cert_nostd",
         "libzeroize_nostd",
     ],
 }
diff --git a/service_vm/requests/src/cert.rs b/service_vm/requests/src/cert.rs
new file mode 100644
index 0000000..68ca382
--- /dev/null
+++ b/service_vm/requests/src/cert.rs
@@ -0,0 +1,130 @@
+// Copyright 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.
+
+//! Generation of certificates and attestation extensions.
+
+use alloc::vec;
+use der::{
+    asn1::{BitStringRef, ObjectIdentifier, UIntRef},
+    oid::AssociatedOid,
+    Decode, Sequence,
+};
+use spki::{AlgorithmIdentifier, SubjectPublicKeyInfo};
+use x509_cert::{
+    certificate::{Certificate, TbsCertificate, Version},
+    ext::Extension,
+    name::Name,
+    time::Validity,
+};
+
+/// OID value for ECDSA with SHA-256, see RFC 5912 s6.
+const ECDSA_WITH_SHA_256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
+
+/// OID value for the protected VM remote attestation extension.
+///
+/// This OID value was added at cl/584542390.
+const AVF_ATTESTATION_EXTENSION_V1: ObjectIdentifier =
+    ObjectIdentifier::new_unwrap("1.3.6.1.4.1.11129.2.1.29.1");
+
+/// Attestation extension contents
+///
+/// ```asn1
+/// AttestationDescription ::= SEQUENCE {
+///     attestationChallenge       OCTET_STRING,
+/// }
+/// ```
+/// TODO(b/312448064): Add VM root of trust and payload information to the extension.
+#[derive(Debug, Clone, Sequence)]
+pub(crate) struct AttestationExtension<'a> {
+    #[asn1(type = "OCTET STRING")]
+    attestation_challenge: &'a [u8],
+}
+
+impl<'a> AssociatedOid for AttestationExtension<'a> {
+    const OID: ObjectIdentifier = AVF_ATTESTATION_EXTENSION_V1;
+}
+
+impl<'a> AttestationExtension<'a> {
+    pub(crate) fn new(challenge: &'a [u8]) -> Self {
+        Self { attestation_challenge: challenge }
+    }
+}
+
+/// Builds an X.509 `Certificate` as defined in RFC 5280 Section 4.1:
+///
+/// ```asn1
+/// Certificate  ::=  SEQUENCE  {
+///   tbsCertificate       TBSCertificate,
+///   signatureAlgorithm   AlgorithmIdentifier,
+///   signature            BIT STRING
+/// }
+/// ```
+pub(crate) fn build_certificate<'a>(
+    tbs_cert: TbsCertificate<'a>,
+    signature: &'a [u8],
+) -> der::Result<Certificate<'a>> {
+    Ok(Certificate {
+        signature_algorithm: tbs_cert.signature,
+        tbs_certificate: tbs_cert,
+        signature: BitStringRef::new(0, signature)?,
+    })
+}
+
+/// Builds an X.509 `TbsCertificate` as defined in RFC 5280 Section 4.1:
+///
+/// ```asn1
+/// TBSCertificate  ::=  SEQUENCE  {
+///   version         [0]  EXPLICIT Version DEFAULT v1,
+///   serialNumber         CertificateSerialNumber,
+///   signature            AlgorithmIdentifier,
+///   issuer               Name,
+///   validity             Validity,
+///   subject              Name,
+///   subjectPublicKeyInfo SubjectPublicKeyInfo,
+///   issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
+///                        -- If present, version MUST be v2 or v3
+///   subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
+///                        -- If present, version MUST be v2 or v3
+///   extensions      [3]  Extensions OPTIONAL
+///                        -- If present, version MUST be v3 --
+/// }
+/// ```
+pub(crate) fn build_tbs_certificate<'a>(
+    serial_number: &'a [u8],
+    issuer: Name<'a>,
+    subject: Name<'a>,
+    validity: Validity,
+    subject_public_key_info: &'a [u8],
+    attestation_ext: &'a [u8],
+) -> der::Result<TbsCertificate<'a>> {
+    let signature = AlgorithmIdentifier { oid: ECDSA_WITH_SHA_256, parameters: None };
+    let subject_public_key_info = SubjectPublicKeyInfo::from_der(subject_public_key_info)?;
+    let extensions = vec![Extension {
+        extn_id: AttestationExtension::OID,
+        critical: false,
+        extn_value: attestation_ext,
+    }];
+    Ok(TbsCertificate {
+        version: Version::V3,
+        serial_number: UIntRef::new(serial_number)?,
+        signature,
+        issuer,
+        validity,
+        subject,
+        subject_public_key_info,
+        issuer_unique_id: None,
+        subject_unique_id: None,
+        extensions: Some(extensions),
+    })
+}
diff --git a/service_vm/requests/src/client_vm.rs b/service_vm/requests/src/client_vm.rs
index 612605f..e1f345c 100644
--- a/service_vm/requests/src/client_vm.rs
+++ b/service_vm/requests/src/client_vm.rs
@@ -15,14 +15,17 @@
 //! This module contains functions related to the attestation of the
 //! client VM.
 
+use crate::cert;
 use crate::keyblob::decrypt_private_key;
 use alloc::vec::Vec;
-use bssl_avf::{sha256, EcKey};
+use bssl_avf::{rand_bytes, sha256, EcKey, EvpPKey};
 use core::result;
 use coset::{CborSerializable, CoseSign};
+use der::{Decode, Encode};
 use diced_open_dice::DiceArtifacts;
 use log::error;
 use service_vm_comm::{ClientVmAttestationParams, Csr, CsrPayload, RequestProcessingError};
+use x509_cert::{certificate::Certificate, name::Name};
 
 type Result<T> = result::Result<T, RequestProcessingError>;
 
@@ -50,21 +53,41 @@
     cose_sign.verify_signature(ATTESTATION_KEY_SIGNATURE_INDEX, aad, |signature, message| {
         ecdsa_verify(&ec_public_key, signature, message)
     })?;
+    let subject_public_key_info = EvpPKey::try_from(ec_public_key)?.subject_public_key_info()?;
 
     // TODO(b/278717513): Compare client VM's DICE chain in the `csr` up to pvmfw
     // cert with RKP VM's DICE chain.
 
+    // Builds the TBSCertificate.
+    // The serial number can be up to 20 bytes according to RFC5280 s4.1.2.2.
+    // In this case, a serial number with a length of 20 bytes is used to ensure that each
+    // certificate signed by RKP VM has a unique serial number.
+    let mut serial_number = [0u8; 20];
+    rand_bytes(&mut serial_number)?;
+    let subject = Name::encode_from_string("CN=Android Protected Virtual Machine Key")?;
+    let rkp_cert = Certificate::from_der(&params.remotely_provisioned_cert)?;
+    let attestation_ext = cert::AttestationExtension::new(&csr_payload.challenge).to_vec()?;
+    let tbs_cert = cert::build_tbs_certificate(
+        &serial_number,
+        rkp_cert.tbs_certificate.subject,
+        Name::from_der(&subject)?,
+        rkp_cert.tbs_certificate.validity,
+        &subject_public_key_info,
+        &attestation_ext,
+    )?;
+
+    // Signs the TBSCertificate and builds the Certificate.
+    // The two private key structs below will be zeroed out on drop.
     let private_key =
         decrypt_private_key(&params.remotely_provisioned_key_blob, dice_artifacts.cdi_seal())
             .map_err(|e| {
                 error!("Failed to decrypt the remotely provisioned key blob: {e}");
                 RequestProcessingError::FailedToDecryptKeyBlob
             })?;
-    let _ec_private_key = EcKey::from_ec_private_key(private_key.as_slice())?;
-
-    // TODO(b/309441500): Build a new certificate signed with the remotely provisioned
-    // `_private_key`.
-    Err(RequestProcessingError::OperationUnimplemented)
+    let ec_private_key = EcKey::from_ec_private_key(private_key.as_slice())?;
+    let signature = ecdsa_sign(&ec_private_key, &tbs_cert.to_vec()?)?;
+    let certificate = cert::build_certificate(tbs_cert, &signature)?;
+    Ok(certificate.to_vec()?)
 }
 
 fn ecdsa_verify(key: &EcKey, signature: &[u8], message: &[u8]) -> bssl_avf::Result<()> {
@@ -72,3 +95,8 @@
     let digest = sha256(message)?;
     key.ecdsa_verify(signature, &digest)
 }
+
+fn ecdsa_sign(key: &EcKey, message: &[u8]) -> bssl_avf::Result<Vec<u8>> {
+    let digest = sha256(message)?;
+    key.ecdsa_sign(&digest)
+}
diff --git a/service_vm/requests/src/lib.rs b/service_vm/requests/src/lib.rs
index b2db298..3f687a4 100644
--- a/service_vm/requests/src/lib.rs
+++ b/service_vm/requests/src/lib.rs
@@ -19,6 +19,7 @@
 extern crate alloc;
 
 mod api;
+mod cert;
 mod client_vm;
 mod keyblob;
 mod pub_key;
diff --git a/service_vm/requests/src/rkp.rs b/service_vm/requests/src/rkp.rs
index 2c01451..9901a92 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/service_vm/requests/src/rkp.rs
@@ -76,10 +76,13 @@
         public_keys.push(public_key.to_cbor_value()?);
     }
     // Builds `CsrPayload`.
+    // TODO(b/299256925): The device information is currently empty as we do not
+    // have sufficient details to include.
+    let device_info = Value::Map(Vec::new());
     let csr_payload = cbor!([
         Value::Integer(CSR_PAYLOAD_SCHEMA_V3.into()),
         Value::Text(String::from(CERTIFICATE_TYPE)),
-        // TODO(b/299256925): Add device info in CBOR format here.
+        device_info,
         Value::Array(public_keys),
     ])?;
     let csr_payload = cbor_util::serialize(&csr_payload)?;
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 001dfeb..886ca81 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -450,7 +450,9 @@
                         key, keyOverrides, /* isProtected= */ false, /* updateBootconfigs= */ true);
         assertThatEventually(
                 100000,
-                () -> getDevice().pullFileContents(LOG_PATH),
+                () ->
+                        getDevice().pullFileContents(CONSOLE_PATH)
+                                + getDevice().pullFileContents(LOG_PATH),
                 containsString("boot completed, time to run payload"));
 
         vmInfo.mProcess.destroy();
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 c6291e4..b06eea6 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1293,7 +1293,7 @@
 
         assertThat(payloadStarted.getNow(false)).isTrue();
         assertThat(exitCodeFuture.getNow(0)).isNotEqualTo(0);
-        assertThat(listener.getLogOutput()).contains(reason);
+        assertThat(listener.getConsoleOutput() + listener.getLogOutput()).contains(reason);
     }
 
     @Test
@@ -1698,7 +1698,7 @@
                         .command(
                                 "logcat",
                                 "-e",
-                                "virtualizationmanager::aidl: Log.*executing main task",
+                                "virtualizationmanager::aidl: (Console|Log).*executing main task",
                                 "-t",
                                 time)
                         .start();
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 6ae3bbd..c63ed4c 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -66,6 +66,7 @@
 use log::{debug, error, info, warn};
 use microdroid_payload_config::{OsConfig, Task, TaskType, VmPayloadConfig};
 use nix::unistd::pipe;
+use regex::Regex;
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
 use semver::VersionReq;
@@ -101,8 +102,6 @@
 
 const MICRODROID_OS_NAME: &str = "microdroid";
 
-const MICRODROID_GKI_OS_NAME: &str = "microdroid_gki";
-
 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
 
 /// Roughly estimated sufficient size for storing vendor public key into DTBO.
@@ -115,6 +114,8 @@
     pub static ref GLOBAL_SERVICE: Strong<dyn IVirtualizationServiceInternal> =
         wait_for_interface(BINDER_SERVICE_IDENTIFIER)
             .expect("Could not connect to VirtualizationServiceInternal");
+    static ref MICRODROID_GKI_OS_NAME_PATTERN: Regex =
+        Regex::new(r"^microdroid_gki-\d+\.\d+$").expect("Failed to construct Regex");
 }
 
 fn create_or_update_idsig_file(
@@ -482,7 +483,7 @@
             }
         };
 
-        let vfio_devices = if !config.devices.is_empty() {
+        let (vfio_devices, dtbo) = if !config.devices.is_empty() {
             let mut set = HashSet::new();
             for device in config.devices.iter() {
                 let path = canonicalize(device)
@@ -493,16 +494,25 @@
                         .or_binder_exception(ExceptionCode::ILLEGAL_ARGUMENT);
                 }
             }
-            GLOBAL_SERVICE
+            let devices = GLOBAL_SERVICE
                 .bindDevicesToVfioDriver(&config.devices)?
                 .into_iter()
                 .map(|x| VfioDevice {
                     sysfs_path: PathBuf::from(&x.sysfsPath),
                     dtbo_label: x.dtboLabel,
                 })
-                .collect::<Vec<_>>()
+                .collect::<Vec<_>>();
+            let dtbo_file = File::from(
+                GLOBAL_SERVICE
+                    .getDtboFile()?
+                    .as_ref()
+                    .try_clone()
+                    .context("Failed to create File from ParcelFileDescriptor")
+                    .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?,
+            );
+            (devices, Some(dtbo_file))
         } else {
-            vec![]
+            (vec![], None)
         };
 
         // Actually start the VM.
@@ -529,6 +539,7 @@
             detect_hangup: is_app_config,
             gdb_port,
             vfio_devices,
+            dtbo,
             dtbo_vendor,
         };
         let instance = Arc::new(
@@ -698,12 +709,12 @@
 
 fn is_valid_os(os_name: &str) -> bool {
     if os_name == MICRODROID_OS_NAME {
-        return true;
+        true
+    } else if cfg!(vendor_modules) && MICRODROID_GKI_OS_NAME_PATTERN.is_match(os_name) {
+        PathBuf::from(format!("/apex/com.android.virt/etc/{}.json", os_name)).exists()
+    } else {
+        false
     }
-    if cfg!(vendor_modules) && os_name == MICRODROID_GKI_OS_NAME {
-        return true;
-    }
-    false
 }
 
 fn load_app_config(
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 9a50776..2ba0e0e 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -116,6 +116,7 @@
     pub detect_hangup: bool,
     pub gdb_port: Option<NonZeroU16>,
     pub vfio_devices: Vec<VfioDevice>,
+    pub dtbo: Option<File>,
     pub dtbo_vendor: Option<File>,
 }
 
@@ -723,11 +724,23 @@
     }
 }
 
-fn append_platform_devices(command: &mut Command, config: &CrosvmConfig) -> Result<(), Error> {
+fn append_platform_devices(
+    command: &mut Command,
+    preserved_fds: &mut Vec<RawFd>,
+    config: &CrosvmConfig,
+) -> Result<(), Error> {
+    if config.vfio_devices.is_empty() {
+        return Ok(());
+    }
+
+    let Some(dtbo) = &config.dtbo else {
+        bail!("VFIO devices assigned but no DTBO available");
+    };
+    command.arg(format!("--device-tree-overlay={},filter", add_preserved_fd(preserved_fds, dtbo)));
+
     for device in &config.vfio_devices {
         command.arg(vfio_argument_for_platform_device(device)?);
     }
-    // TODO(b/291192693): add dtbo to command line when assigned device is not empty.
     Ok(())
 }
 
@@ -889,7 +902,7 @@
 
     // TODO(b/285855436): Pass dtbo_vendor after --device-tree-overlay crosvm option is supported.
 
-    append_platform_devices(&mut command, &config)?;
+    append_platform_devices(&mut command, &mut preserved_fds, &config)?;
 
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index bef7dd0..3f8d193 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_binary {
-    name: "virtualizationservice",
+rust_defaults {
+    name: "virtualizationservice_defaults",
     crate_name: "virtualizationservice",
     defaults: ["avf_build_flags_rust"],
     edition: "2021",
@@ -31,6 +31,7 @@
         "libanyhow",
         "libavflog",
         "libbinder_rs",
+        "libhypervisor_props",
         "liblibc",
         "liblog_rust",
         "libnix",
@@ -44,13 +45,39 @@
         "libserde_xml_rs",
         "libservice_vm_comm",
         "libservice_vm_manager",
+        "libx509_parser",
     ],
     apex_available: ["com.android.virt"],
 }
 
+rust_binary {
+    name: "virtualizationservice",
+    defaults: ["virtualizationservice_defaults"],
+}
+
 xsd_config {
     name: "assignable_devices",
     srcs: ["assignable_devices.xsd"],
     api_dir: "schema",
     package_name: "android.system.virtualizationservice",
 }
+
+rust_test {
+    name: "virtualizationservice_test",
+    defaults: ["virtualizationservice_defaults"],
+    test_suites: ["general-tests"],
+    data: [
+        ":test_rkp_cert_chain",
+    ],
+}
+
+// The chain originates from a CTS test for Keymint, with the Keymint certificate
+// (leaf certificate) truncated.
+//
+// The certificate chain begins with a leaf certificate obtained from RKP and ends
+// with a root certificate. Each certificate in the chain possesses a signature that
+// is signed by the private key of the subsequent certificate in the chain.
+filegroup {
+    name: "test_rkp_cert_chain",
+    srcs: ["testdata/rkp_cert_chain.der"],
+}
diff --git a/virtualizationservice/TEST_MAPPING b/virtualizationservice/TEST_MAPPING
new file mode 100644
index 0000000..4fef83c
--- /dev/null
+++ b/virtualizationservice/TEST_MAPPING
@@ -0,0 +1,9 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+  "avf-presubmit" : [
+    {
+      "name" : "virtualizationservice_test"
+    }
+  ]
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index c384a6f..a2cb693 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -81,4 +81,7 @@
      * @return a list of pairs (sysfs path, DTBO node label) for devices.
      */
     BoundDevice[] bindDevicesToVfioDriver(in String[] devices);
+
+    /** Returns a read-only file descriptor of the VM DTBO file. */
+    ParcelFileDescriptor getDtboFile();
 }
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 938225e..3ac1e60 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -43,7 +43,7 @@
 use rustutils::system_properties;
 use serde::Deserialize;
 use std::collections::{HashMap, HashSet};
-use std::fs::{self, create_dir, remove_dir_all, set_permissions, File, Permissions};
+use std::fs::{self, create_dir, remove_dir_all, remove_file, set_permissions, File, Permissions};
 use std::io::{Read, Write};
 use std::os::unix::fs::PermissionsExt;
 use std::os::unix::raw::{pid_t, uid_t};
@@ -52,6 +52,7 @@
 use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
 use vsock::{VsockListener, VsockStream};
 use nix::unistd::{chown, Uid};
+use x509_parser::{traits::FromDer, certificate::X509Certificate};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
 pub type Cid = u32;
@@ -166,35 +167,46 @@
         requester_uid: i32,
     ) -> binder::Result<Vec<Certificate>> {
         check_manage_access()?;
-        info!("Received csr. Requestting attestation...");
-        if cfg!(remote_attestation) {
-            let attestation_key = get_rkpd_attestation_key(
-                REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME,
-                requester_uid as u32,
-            )
-            .context("Failed to retrieve the remotely provisioned keys")
-            .with_log()
-            .or_service_specific_exception(-1)?;
-            let certificate = request_attestation(csr, &attestation_key.keyBlob)
-                .context("Failed to request attestation")
-                .with_log()
-                .or_service_specific_exception(-1)?;
-            // TODO(b/309780089): Parse the remotely provisioned certificate chain into
-            // individual certificates.
-            let mut certificate_chain =
-                vec![Certificate { encodedCertificate: attestation_key.encodedCertChain }];
-            certificate_chain.push(Certificate { encodedCertificate: certificate });
-            Ok(certificate_chain)
-        } else {
-            Err(Status::new_exception_str(
+        if !cfg!(remote_attestation) {
+            return Err(Status::new_exception_str(
                 ExceptionCode::UNSUPPORTED_OPERATION,
                 Some(
                     "requestAttestation is not supported with the remote_attestation feature \
                      disabled",
                 ),
             ))
-            .with_log()
+            .with_log();
         }
+        info!("Received csr. Requestting attestation...");
+        let attestation_key = get_rkpd_attestation_key(
+            REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME,
+            requester_uid as u32,
+        )
+        .context("Failed to retrieve the remotely provisioned keys")
+        .with_log()
+        .or_service_specific_exception(-1)?;
+        let mut certificate_chain = split_x509_certificate_chain(&attestation_key.encodedCertChain)
+            .context("Failed to split the remotely provisioned certificate chain")
+            .with_log()
+            .or_service_specific_exception(-1)?;
+        if certificate_chain.is_empty() {
+            return Err(Status::new_service_specific_error_str(
+                -1,
+                Some("The certificate chain should contain at least 1 certificate"),
+            ))
+            .with_log();
+        }
+        let certificate = request_attestation(
+            csr.to_vec(),
+            attestation_key.keyBlob,
+            certificate_chain[0].encodedCertificate.clone(),
+        )
+        .context("Failed to request attestation")
+        .with_log()
+        .or_service_specific_exception(-1)?;
+        certificate_chain.insert(0, Certificate { encodedCertificate: certificate });
+
+        Ok(certificate_chain)
     }
 
     fn getAssignableDevices(&self) -> binder::Result<Vec<AssignableDevice>> {
@@ -212,18 +224,8 @@
 
         let vfio_service: Strong<dyn IVfioHandler> =
             wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())?;
-
         vfio_service.bindDevicesToVfioDriver(devices)?;
 
-        let dtbo_path = Path::new(TEMPORARY_DIRECTORY).join("common").join("dtbo");
-        if !dtbo_path.exists() {
-            // open a writable file descriptor for vfio_handler
-            let dtbo = File::create(&dtbo_path)
-                .context("Failed to create VM DTBO file")
-                .or_service_specific_exception(-1)?;
-            vfio_service.writeVmDtbo(&ParcelFileDescriptor::new(dtbo))?;
-        }
-
         Ok(get_assignable_devices()?
             .device
             .into_iter()
@@ -236,6 +238,14 @@
             })
             .collect::<Vec<_>>())
     }
+
+    fn getDtboFile(&self) -> binder::Result<ParcelFileDescriptor> {
+        check_use_custom_virtual_machine()?;
+
+        let state = &mut *self.state.lock().unwrap();
+        let file = state.get_dtbo_file().or_service_specific_exception(-1)?;
+        Ok(ParcelFileDescriptor::new(file))
+    }
 }
 
 // KEEP IN SYNC WITH assignable_devices.xsd
@@ -290,6 +300,17 @@
     Ok(devices)
 }
 
+fn split_x509_certificate_chain(mut cert_chain: &[u8]) -> Result<Vec<Certificate>> {
+    let mut out = Vec::new();
+    while !cert_chain.is_empty() {
+        let (remaining, _) = X509Certificate::from_der(cert_chain)?;
+        let end = cert_chain.len() - remaining.len();
+        out.push(Certificate { encodedCertificate: cert_chain[..end].to_vec() });
+        cert_chain = remaining;
+    }
+    Ok(out)
+}
+
 #[derive(Debug, Default)]
 struct GlobalVmInstance {
     /// The unique CID assigned to the VM for vsock communication.
@@ -314,6 +335,9 @@
     /// VM contexts currently allocated to running VMs. A CID is never recycled as long
     /// as there is a strong reference held by a GlobalVmContext.
     held_contexts: HashMap<Cid, Weak<GlobalVmInstance>>,
+
+    /// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
+    dtbo_file: Mutex<Option<File>>,
 }
 
 impl GlobalState {
@@ -377,26 +401,64 @@
 
         let cid = self.get_next_available_cid()?;
         let instance = Arc::new(GlobalVmInstance { cid, requester_uid, requester_debug_pid });
-        create_temporary_directory(&instance.get_temp_dir(), requester_uid)?;
+        create_temporary_directory(&instance.get_temp_dir(), Some(requester_uid))?;
 
         self.held_contexts.insert(cid, Arc::downgrade(&instance));
         let binder = GlobalVmContext { instance, ..Default::default() };
         Ok(BnGlobalVmContext::new_binder(binder, BinderFeatures::default()))
     }
+
+    fn get_dtbo_file(&mut self) -> Result<File> {
+        let mut file = self.dtbo_file.lock().unwrap();
+
+        let fd = if let Some(ref_fd) = &*file {
+            ref_fd.try_clone()?
+        } else {
+            let path = get_or_create_common_dir()?.join("vm.dtbo");
+            if path.exists() {
+                // All temporary files are deleted when the service is started.
+                // If the file exists but the FD is not cached, the file is
+                // likely corrupted.
+                remove_file(&path).context("Failed to clone cached VM DTBO file descriptor")?;
+            }
+
+            // Open a write-only file descriptor for vfio_handler.
+            let write_fd = File::create(&path).context("Failed to create VM DTBO file")?;
+
+            let vfio_service: Strong<dyn IVfioHandler> =
+                wait_for_interface(<BpVfioHandler as IVfioHandler>::get_descriptor())?;
+            vfio_service.writeVmDtbo(&ParcelFileDescriptor::new(write_fd))?;
+
+            // Open read-only. This FD will be cached and returned to clients.
+            let read_fd = File::open(&path).context("Failed to open VM DTBO file")?;
+            let read_fd_clone =
+                read_fd.try_clone().context("Failed to clone VM DTBO file descriptor")?;
+            *file = Some(read_fd);
+            read_fd_clone
+        };
+
+        Ok(fd)
+    }
 }
 
-fn create_temporary_directory(path: &PathBuf, requester_uid: uid_t) -> Result<()> {
+fn create_temporary_directory(path: &PathBuf, requester_uid: Option<uid_t>) -> Result<()> {
+    // Directory may exist if previous attempt to create it had failed.
+    // Delete it before trying again.
     if path.as_path().exists() {
         remove_temporary_dir(path).unwrap_or_else(|e| {
             warn!("Could not delete temporary directory {:?}: {}", path, e);
         });
     }
-    // Create a directory that is owned by client's UID but system's GID, and permissions 0700.
+    // Create directory.
+    create_dir(path).with_context(|| format!("Could not create temporary directory {:?}", path))?;
+    // If provided, change ownership to client's UID but system's GID, and permissions 0700.
     // If the chown() fails, this will leave behind an empty directory that will get removed
     // at the next attempt, or if virtualizationservice is restarted.
-    create_dir(path).with_context(|| format!("Could not create temporary directory {:?}", path))?;
-    chown(path, Some(Uid::from_raw(requester_uid)), None)
-        .with_context(|| format!("Could not set ownership of temporary directory {:?}", path))?;
+    if let Some(uid) = requester_uid {
+        chown(path, Some(Uid::from_raw(uid)), None).with_context(|| {
+            format!("Could not set ownership of temporary directory {:?}", path)
+        })?;
+    }
     Ok(())
 }
 
@@ -410,6 +472,14 @@
     Ok(())
 }
 
+fn get_or_create_common_dir() -> Result<PathBuf> {
+    let path = Path::new(TEMPORARY_DIRECTORY).join("common");
+    if !path.exists() {
+        create_temporary_directory(&path, None)?;
+    }
+    Ok(path)
+}
+
 /// Implementation of the AIDL `IGlobalVmContext` interface.
 #[derive(Debug, Default)]
 struct GlobalVmContext {
@@ -514,3 +584,24 @@
 fn check_use_custom_virtual_machine() -> binder::Result<()> {
     check_permission("android.permission.USE_CUSTOM_VIRTUAL_MACHINE")
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+
+    const TEST_RKP_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
+
+    #[test]
+    fn splitting_x509_certificate_chain_succeeds() -> Result<()> {
+        let bytes = fs::read(TEST_RKP_CERT_CHAIN_PATH)?;
+        let cert_chain = split_x509_certificate_chain(&bytes)?;
+
+        assert_eq!(4, cert_chain.len());
+        for cert in cert_chain {
+            let (remaining, _) = X509Certificate::from_der(&cert.encodedCertificate)?;
+            assert!(remaining.is_empty());
+        }
+        Ok(())
+    }
+}
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index d80ddd4..ea073bf 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -34,7 +34,7 @@
 
 const LOG_TAG: &str = "VirtualizationService";
 pub(crate) const REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME: &str =
-    "android.system.virtualization.IRemotelyProvisionedComponent/avf";
+    "android.hardware.security.keymint.IRemotelyProvisionedComponent/avf";
 
 fn get_calling_pid() -> pid_t {
     ThreadState::get_calling_pid()
@@ -69,10 +69,17 @@
     register_lazy_service(BINDER_SERVICE_IDENTIFIER, service.as_binder()).unwrap();
     info!("Registered Binder service {}.", BINDER_SERVICE_IDENTIFIER);
 
-    // The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
-    // RKP VM attestation.
-    let _remote_provisioning_service = remote_provisioning::new_binder();
-    // TODO(b/274881098): Register the RKP service when the implementation is ready.
+    if cfg!(remote_attestation) {
+        // The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
+        // RKP VM attestation.
+        let remote_provisioning_service = remote_provisioning::new_binder();
+        register_lazy_service(
+            REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME,
+            remote_provisioning_service.as_binder(),
+        )
+        .unwrap();
+        info!("Registered Binder service {}.", REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME);
+    }
 
     ProcessState::join_thread_pool();
 }
diff --git a/virtualizationservice/src/remote_provisioning.rs b/virtualizationservice/src/remote_provisioning.rs
index a9a07a5..40f54db 100644
--- a/virtualizationservice/src/remote_provisioning.rs
+++ b/virtualizationservice/src/remote_provisioning.rs
@@ -27,7 +27,11 @@
 };
 use anyhow::Context;
 use avflog::LogResult;
-use binder::{BinderFeatures, Interface, IntoBinderResult, Result as BinderResult, Status, Strong};
+use binder::{
+    BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Status,
+    Strong,
+};
+use hypervisor_props::is_protected_vm_supported;
 use service_vm_comm::{RequestProcessingError, Response};
 
 /// Constructs a binder object that implements `IRemotelyProvisionedComponent`.
@@ -45,11 +49,13 @@
 #[allow(non_snake_case)]
 impl IRemotelyProvisionedComponent for AvfRemotelyProvisionedComponent {
     fn getHardwareInfo(&self) -> BinderResult<RpcHardwareInfo> {
+        check_protected_vm_is_supported()?;
+
         Ok(RpcHardwareInfo {
             versionNumber: 3,
             rpcAuthorName: String::from("Android Virtualization Framework"),
             supportedEekCurve: CURVE_NONE,
-            uniqueId: Some(String::from("Android Virtualization Framework 1")),
+            uniqueId: Some(String::from("AVF Remote Provisioning 1")),
             supportedNumKeysInCsr: MIN_SUPPORTED_NUM_KEYS_IN_CSR,
         })
     }
@@ -59,6 +65,8 @@
         testMode: bool,
         macedPublicKey: &mut MacedPublicKey,
     ) -> BinderResult<Vec<u8>> {
+        check_protected_vm_is_supported()?;
+
         if testMode {
             return Err(Status::new_service_specific_error_str(
                 STATUS_REMOVED,
@@ -101,6 +109,8 @@
         keysToSign: &[MacedPublicKey],
         challenge: &[u8],
     ) -> BinderResult<Vec<u8>> {
+        check_protected_vm_is_supported()?;
+
         const MAX_CHALLENGE_SIZE: usize = 64;
         if challenge.len() > MAX_CHALLENGE_SIZE {
             let message = format!(
@@ -123,6 +133,18 @@
     }
 }
 
+fn check_protected_vm_is_supported() -> BinderResult<()> {
+    if is_protected_vm_supported().unwrap_or(false) {
+        Ok(())
+    } else {
+        Err(Status::new_exception_str(
+            ExceptionCode::UNSUPPORTED_OPERATION,
+            Some("Protected VM support is missing for this operation"),
+        ))
+        .with_log()
+    }
+}
+
 fn to_service_specific_error(response: Response) -> Status {
     match response {
         Response::Err(e) => match e {
diff --git a/virtualizationservice/src/rkpvm.rs b/virtualizationservice/src/rkpvm.rs
index 5087120..79e09b0 100644
--- a/virtualizationservice/src/rkpvm.rs
+++ b/virtualizationservice/src/rkpvm.rs
@@ -24,15 +24,14 @@
 use service_vm_manager::ServiceVm;
 
 pub(crate) fn request_attestation(
-    csr: &[u8],
-    remotely_provisioned_keyblob: &[u8],
+    csr: Vec<u8>,
+    remotely_provisioned_key_blob: Vec<u8>,
+    remotely_provisioned_cert: Vec<u8>,
 ) -> Result<Vec<u8>> {
     let mut vm = ServiceVm::start()?;
 
-    let params = ClientVmAttestationParams {
-        csr: csr.to_vec(),
-        remotely_provisioned_key_blob: remotely_provisioned_keyblob.to_vec(),
-    };
+    let params =
+        ClientVmAttestationParams { csr, remotely_provisioned_key_blob, remotely_provisioned_cert };
     let request = Request::RequestClientVmAttestation(params);
     match vm.process_request(request).context("Failed to process request")? {
         Response::RequestClientVmAttestation(cert) => Ok(cert),
diff --git a/virtualizationservice/testdata/rkp_cert_chain.der b/virtualizationservice/testdata/rkp_cert_chain.der
new file mode 100644
index 0000000..f32065d
--- /dev/null
+++ b/virtualizationservice/testdata/rkp_cert_chain.der
Binary files differ
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 87278bc..9a92f13 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -27,6 +27,7 @@
 use clap::{Args, Parser};
 use create_idsig::command_create_idsig;
 use create_partition::command_create_partition;
+use glob::glob;
 use run::{command_run, command_run_app, command_run_microdroid};
 use std::num::NonZeroU16;
 use std::path::{Path, PathBuf};
@@ -107,10 +108,10 @@
     #[arg(long)]
     devices: Vec<PathBuf>,
 
-    /// If set, use GKI instead of microdroid kernel
+    /// Version of GKI to use. If set, use instead of microdroid kernel
     #[cfg(vendor_modules)]
     #[arg(long)]
-    gki: bool,
+    gki: Option<String>,
 }
 
 impl MicrodroidConfig {
@@ -125,13 +126,13 @@
     }
 
     #[cfg(vendor_modules)]
-    fn gki(&self) -> bool {
-        self.gki
+    fn gki(&self) -> Option<&str> {
+        self.gki.as_deref()
     }
 
     #[cfg(not(vendor_modules))]
-    fn gki(&self) -> bool {
-        false
+    fn gki(&self) -> Option<&str> {
+        None
     }
 
     #[cfg(device_assignment)]
@@ -315,6 +316,12 @@
     Ok(())
 }
 
+fn extract_gki_version(gki_config: &Path) -> Option<&str> {
+    let name = gki_config.file_name()?;
+    let name_str = name.to_str()?;
+    name_str.strip_prefix("microdroid_gki-")?.strip_suffix(".json")
+}
+
 /// Print information about supported VM types.
 fn command_info() -> Result<(), Error> {
     let non_protected_vm_supported = hypervisor_props::is_vm_supported()?;
@@ -354,6 +361,12 @@
     let devices = devices.into_iter().map(|x| x.node).collect::<Vec<_>>();
     println!("Assignable devices: {}", serde_json::to_string(&devices)?);
 
+    let gki_configs =
+        glob("/apex/com.android.virt/etc/microdroid_gki-*.json")?.collect::<Result<Vec<_>, _>>()?;
+    let gki_versions =
+        gki_configs.iter().filter_map(|x| extract_gki_version(x)).collect::<Vec<_>>();
+    println!("Available gki versions: {}", serde_json::to_string(&gki_versions)?);
+
     Ok(())
 }
 
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 44ba9af..8721e71 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -111,8 +111,11 @@
         }
         Payload::ConfigPath(config_path)
     } else if let Some(payload_binary_name) = config.payload_binary_name {
-        let os_name =
-            if config.microdroid.gki() { "microdroid_gki" } else { "microdroid" }.to_owned();
+        let os_name = if let Some(ver) = config.microdroid.gki() {
+            format!("microdroid_gki-{ver}")
+        } else {
+            "microdroid".to_owned()
+        };
         Payload::PayloadConfig(VirtualMachinePayloadConfig {
             payloadBinaryName: payload_binary_name,
             osName: os_name,
diff --git a/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
index 78cd80d..3483e1d 100644
--- a/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -224,8 +224,8 @@
  * Gets the number of certificates in the certificate chain.
  *
  * The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
- * the attestation key's certificate chain. It starts with a root certificate and ends with a
- * leaf certificate covering the attested public key.
+ * the attestation key's certificate chain. It starts with a leaf certificate covering the attested
+ * public key and ends with a root certificate.
  *
  * \param result A pointer to the attestation result obtained from `AVmPayload_requestAttestation`
  *               when the attestation succeeds.
@@ -240,8 +240,8 @@
  * attestation result.
  *
  * The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
- * the attestation key's certificate chain. It starts with a root certificate and ends with a
- * leaf certificate covering the attested public key.
+ * the attestation key's certificate chain. It starts with a leaf certificate covering the attested
+ * public key and ends with a root certificate.
  *
  * \param result A pointer to the attestation result obtained from `AVmPayload_requestAttestation`
  *               when the attestation succeeds.