[attestation] Verify client VM's DICE chain up to pvmfw payload

This cl validates Client VM's DICE chain up to the pvmfw payload
and parse the DICE chain payload to extract necessary fields
required for attestation.

Test: atest rialto_test
Bug: 278717513
Change-Id: Ia60ed9a65fc5ef4ed5fdb6804403035fa9d7c00e
diff --git a/rialto/Android.bp b/rialto/Android.bp
index 1ab02e9..90008a9 100644
--- a/rialto/Android.bp
+++ b/rialto/Android.bp
@@ -113,6 +113,8 @@
         "libciborium",
         "libclient_vm_csr",
         "libcoset",
+        "libcstr",
+        "libdiced_open_dice",
         "libdiced_sample_inputs",
         "liblibc",
         "liblog_rust",
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index d9cffe0..0bdc927 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -37,7 +37,7 @@
 use hyp::{get_mem_sharer, get_mmio_guard};
 use libfdt::FdtError;
 use log::{debug, error, info};
-use service_vm_comm::{ServiceVmRequest, VmType};
+use service_vm_comm::{RequestProcessingError, Response, ServiceVmRequest, VmType};
 use service_vm_requests::process_request;
 use virtio_drivers::{
     device::socket::{VsockAddr, VMADDR_CID_HOST},
@@ -178,7 +178,15 @@
 
     let mut vsock_stream = VsockStream::new(socket_device, host_addr())?;
     while let ServiceVmRequest::Process(req) = vsock_stream.read_request()? {
-        let response = process_request(req, bcc_handover.as_ref());
+        let mut response = process_request(req, bcc_handover.as_ref());
+        // TODO(b/185878400): We don't want to issue a certificate to pVM when the client VM
+        // attestation is unfinished. The following code should be removed once the
+        // verification is completed.
+        if vm_type() == VmType::ProtectedVm
+            && matches!(response, Response::RequestClientVmAttestation(_))
+        {
+            response = Response::Err(RequestProcessingError::OperationUnimplemented);
+        }
         vsock_stream.write_response(&response)?;
         vsock_stream.flush()?;
     }
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index 85c3efe..0260773 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -26,10 +26,15 @@
 use ciborium::value::Value;
 use client_vm_csr::generate_attestation_key_and_csr;
 use coset::{CborSerializable, CoseMac0, CoseSign};
+use cstr::cstr;
+use diced_open_dice::{
+    retry_bcc_format_config_descriptor, retry_bcc_main_flow, Config, DiceArtifacts,
+    DiceConfigValues, DiceMode, InputValues, OwnedDiceArtifacts, HASH_SIZE, HIDDEN_SIZE,
+};
 use log::info;
 use service_vm_comm::{
     ClientVmAttestationParams, Csr, CsrPayload, EcdsaP256KeyPair, GenerateCertificateRequestParams,
-    Request, Response, VmType,
+    Request, RequestProcessingError, Response, VmType,
 };
 use service_vm_manager::ServiceVm;
 use std::fs;
@@ -48,6 +53,19 @@
 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";
+/// The following data are generated randomly with urandom.
+const CODE_HASH_MICRODROID: [u8; HASH_SIZE] = [
+    0x08, 0x78, 0xc2, 0x5b, 0xe7, 0xea, 0x3d, 0x62, 0x70, 0x22, 0xd9, 0x1c, 0x4f, 0x3c, 0x2e, 0x2f,
+    0x0f, 0x97, 0xa4, 0x6f, 0x6d, 0xd5, 0xe6, 0x4a, 0x6d, 0xbe, 0x34, 0x2e, 0x56, 0x04, 0xaf, 0xef,
+    0x74, 0x3f, 0xec, 0xb8, 0x44, 0x11, 0xf4, 0x2f, 0x05, 0xb2, 0x06, 0xa3, 0x0e, 0x75, 0xb7, 0x40,
+    0x9a, 0x4c, 0x58, 0xab, 0x96, 0xe7, 0x07, 0x97, 0x07, 0x86, 0x5c, 0xa1, 0x42, 0x12, 0xf0, 0x34,
+];
+const AUTHORITY_HASH_MICRODROID: [u8; HASH_SIZE] = [
+    0xc7, 0x97, 0x5b, 0xa9, 0x9e, 0xbf, 0x0b, 0xeb, 0xe7, 0x7f, 0x69, 0x8f, 0x8e, 0xcf, 0x04, 0x7d,
+    0x2c, 0x0f, 0x4d, 0xbe, 0xcb, 0xf5, 0xf1, 0x4c, 0x1d, 0x1c, 0xb7, 0x44, 0xdf, 0xf8, 0x40, 0x90,
+    0x09, 0x65, 0xab, 0x01, 0x34, 0x3e, 0xc2, 0xc4, 0xf7, 0xa2, 0x3a, 0x5c, 0x4e, 0x76, 0x4f, 0x42,
+    0xa8, 0x6c, 0xc9, 0xf1, 0x7b, 0x12, 0x80, 0xa4, 0xef, 0xa2, 0x4d, 0x72, 0xa1, 0x21, 0xe2, 0x47,
+];
 
 #[test]
 fn process_requests_in_protected_vm() -> Result<()> {
@@ -65,7 +83,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)?;
+    check_attestation_request(&mut vm, &key_pair, vm_type)?;
     Ok(())
 }
 
@@ -123,6 +141,7 @@
 fn check_attestation_request(
     vm: &mut ServiceVm,
     remotely_provisioned_key_pair: &EcdsaP256KeyPair,
+    vm_type: VmType,
 ) -> Result<()> {
     /// The following data was generated randomly with urandom.
     const CHALLENGE: [u8; 16] = [
@@ -130,6 +149,7 @@
         0x5c,
     ];
     let dice_artifacts = diced_sample_inputs::make_sample_bcc_and_cdis()?;
+    let dice_artifacts = extend_dice_artifacts_with_microdroid_payload(&dice_artifacts)?;
     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)?;
@@ -151,6 +171,9 @@
 
     match response {
         Response::RequestClientVmAttestation(certificate) => {
+            // The end-to-end test for non-protected VM attestation works because both the service
+            // VM and the client VM use the same fake DICE chain.
+            assert_eq!(vm_type, VmType::NonProtectedVm);
             check_certificate_for_client_vm(
                 &certificate,
                 &remotely_provisioned_key_pair.maced_public_key,
@@ -159,10 +182,43 @@
             )?;
             Ok(())
         }
+        Response::Err(RequestProcessingError::InvalidDiceChain) => {
+            // The end-to-end test for protected VM attestation doesn't work because the service VM
+            // compares the fake DICE chain in the CSR with the real DICE chain.
+            // We cannot generate a valid DICE chain with the same payloads up to pvmfw.
+            assert_eq!(vm_type, VmType::ProtectedVm);
+            Ok(())
+        }
         _ => bail!("Incorrect response type: {response:?}"),
     }
 }
 
+fn extend_dice_artifacts_with_microdroid_payload(
+    dice_artifacts: &dyn DiceArtifacts,
+) -> Result<OwnedDiceArtifacts> {
+    let config_values = DiceConfigValues {
+        component_name: Some(cstr!("Microdroid payload")),
+        component_version: Some(1),
+        resettable: true,
+        ..Default::default()
+    };
+    let config_descriptor = retry_bcc_format_config_descriptor(&config_values)?;
+    let input_values = InputValues::new(
+        CODE_HASH_MICRODROID,
+        Config::Descriptor(config_descriptor.as_slice()),
+        AUTHORITY_HASH_MICRODROID,
+        DiceMode::kDiceModeDebug,
+        [0u8; HIDDEN_SIZE], // hidden
+    );
+    retry_bcc_main_flow(
+        dice_artifacts.cdi_attest(),
+        dice_artifacts.cdi_seal(),
+        dice_artifacts.bcc().unwrap(),
+        &input_values,
+    )
+    .context("Failed to run BCC main flow for Microdroid")
+}
+
 fn check_certificate_for_client_vm(
     certificate: &[u8],
     maced_public_key: &[u8],
@@ -205,8 +261,13 @@
     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!(2, attestation_ext.len());
     assert_eq!(csr_payload.challenge, attestation_ext[0].as_slice()?);
+    let is_vm_secure = attestation_ext[1].as_bool()?;
+    assert!(
+        !is_vm_secure,
+        "The VM shouldn't be secure as the last payload added in the test is in Debug mode"
+    );
 
     // Checks other fields on the certificate
     assert_eq!(X509Version::V3, cert.version());
diff --git a/service_vm/comm/src/csr.rs b/service_vm/comm/src/csr.rs
index 757d080..2a27f90 100644
--- a/service_vm/comm/src/csr.rs
+++ b/service_vm/comm/src/csr.rs
@@ -100,7 +100,8 @@
     }
 }
 
-fn try_as_bytes(v: Value, context: &str) -> coset::Result<Vec<u8>> {
+/// Reads the provided value `v` as bytes array.
+pub fn try_as_bytes(v: Value, context: &str) -> coset::Result<Vec<u8>> {
     if let Value::Bytes(data) = v {
         Ok(data)
     } else {
@@ -110,7 +111,8 @@
     }
 }
 
-fn cbor_value_type(v: &Value) -> &'static str {
+/// Reads the type of the provided value `v`.
+pub fn cbor_value_type(v: &Value) -> &'static str {
     match v {
         Value::Integer(_) => "int",
         Value::Bytes(_) => "bstr",
diff --git a/service_vm/comm/src/lib.rs b/service_vm/comm/src/lib.rs
index bb85a26..c9de540 100644
--- a/service_vm/comm/src/lib.rs
+++ b/service_vm/comm/src/lib.rs
@@ -23,7 +23,7 @@
 mod message;
 mod vsock;
 
-pub use csr::{Csr, CsrPayload};
+pub use csr::{cbor_value_type, try_as_bytes, Csr, CsrPayload};
 pub use message::{
     ClientVmAttestationParams, EcdsaP256KeyPair, GenerateCertificateRequestParams, Request,
     RequestProcessingError, Response, ServiceVmRequest,
diff --git a/service_vm/comm/src/message.rs b/service_vm/comm/src/message.rs
index 87c8378..80a9608 100644
--- a/service_vm/comm/src/message.rs
+++ b/service_vm/comm/src/message.rs
@@ -130,6 +130,9 @@
 
     /// An error happened during the DER encoding/decoding.
     DerError,
+
+    /// The DICE chain from the client VM is invalid.
+    InvalidDiceChain,
 }
 
 impl fmt::Display for RequestProcessingError {
@@ -155,6 +158,9 @@
             Self::DerError => {
                 write!(f, "An error happened during the DER encoding/decoding")
             }
+            Self::InvalidDiceChain => {
+                write!(f, "The DICE chain from the client VM is invalid")
+            }
         }
     }
 }
diff --git a/service_vm/comm/src/vsock.rs b/service_vm/comm/src/vsock.rs
index aa7166d..7f7cf25 100644
--- a/service_vm/comm/src/vsock.rs
+++ b/service_vm/comm/src/vsock.rs
@@ -18,7 +18,7 @@
 const NON_PROTECTED_VM_PORT: u32 = 5680;
 
 /// VM Type.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum VmType {
     /// Protected VM.
     ProtectedVm,
diff --git a/service_vm/requests/src/cert.rs b/service_vm/requests/src/cert.rs
index 68ca382..2baca2a 100644
--- a/service_vm/requests/src/cert.rs
+++ b/service_vm/requests/src/cert.rs
@@ -49,6 +49,8 @@
 pub(crate) struct AttestationExtension<'a> {
     #[asn1(type = "OCTET STRING")]
     attestation_challenge: &'a [u8],
+    /// Indicates whether the VM is operating under a secure configuration.
+    is_vm_secure: bool,
 }
 
 impl<'a> AssociatedOid for AttestationExtension<'a> {
@@ -56,8 +58,8 @@
 }
 
 impl<'a> AttestationExtension<'a> {
-    pub(crate) fn new(challenge: &'a [u8]) -> Self {
-        Self { attestation_challenge: challenge }
+    pub(crate) fn new(attestation_challenge: &'a [u8], is_vm_secure: bool) -> Self {
+        Self { attestation_challenge, is_vm_secure }
     }
 }
 
diff --git a/service_vm/requests/src/client_vm.rs b/service_vm/requests/src/client_vm.rs
index e1f345c..bb92f4a 100644
--- a/service_vm/requests/src/client_vm.rs
+++ b/service_vm/requests/src/client_vm.rs
@@ -16,6 +16,7 @@
 //! client VM.
 
 use crate::cert;
+use crate::dice::{validate_client_vm_dice_chain_prefix_match, ClientVmDiceChain};
 use crate::keyblob::decrypt_private_key;
 use alloc::vec::Vec;
 use bssl_avf::{rand_bytes, sha256, EcKey, EvpPKey};
@@ -43,21 +44,30 @@
     })?;
     let csr_payload = CsrPayload::from_cbor_slice(csr_payload)?;
 
+    // Validates the prefix of the Client VM DICE chain in the CSR.
+    let service_vm_dice_chain =
+        dice_artifacts.bcc().ok_or(RequestProcessingError::MissingDiceChain)?;
+    let client_vm_dice_chain =
+        validate_client_vm_dice_chain_prefix_match(&csr.dice_cert_chain, service_vm_dice_chain)?;
+    // Validates the signatures in the Client VM DICE chain and extracts the partially decoded
+    // DiceChainEntryPayloads.
+    let client_vm_dice_chain =
+        ClientVmDiceChain::validate_signatures_and_parse_dice_chain(client_vm_dice_chain)?;
+
     // AAD is empty as defined in service_vm/comm/client_vm_csr.cddl.
     let aad = &[];
 
+    // Verifies the first signature with the leaf private key in the DICE chain.
     // TODO(b/310931749): Verify the first signature with CDI_Leaf_Pub of
     // the DICE chain in `cose_sign`.
 
+    // Verifies the second signature with the public key in the CSR payload.
     let ec_public_key = EcKey::from_cose_public_key(&csr_payload.public_key)?;
     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
@@ -66,7 +76,11 @@
     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 attestation_ext = cert::AttestationExtension::new(
+        &csr_payload.challenge,
+        client_vm_dice_chain.all_entries_are_secure(),
+    )
+    .to_vec()?;
     let tbs_cert = cert::build_tbs_certificate(
         &serial_number,
         rkp_cert.tbs_certificate.subject,
diff --git a/service_vm/requests/src/dice.rs b/service_vm/requests/src/dice.rs
new file mode 100644
index 0000000..c220af6
--- /dev/null
+++ b/service_vm/requests/src/dice.rs
@@ -0,0 +1,321 @@
+// 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.
+
+//! This module contains functions related to DICE.
+
+use alloc::vec::Vec;
+use ciborium::value::Value;
+use core::cell::OnceCell;
+use core::result;
+use coset::{
+    self, iana, AsCborValue, CborSerializable, CoseError, CoseKey, CoseSign1, KeyOperation,
+};
+use diced_open_dice::{DiceMode, HASH_SIZE};
+use log::error;
+use service_vm_comm::{cbor_value_type, try_as_bytes, RequestProcessingError};
+
+type Result<T> = result::Result<T, RequestProcessingError>;
+
+const CODE_HASH: i64 = -4670545;
+const CONFIG_DESC: i64 = -4670548;
+const AUTHORITY_HASH: i64 = -4670549;
+const MODE: i64 = -4670551;
+const SUBJECT_PUBLIC_KEY: i64 = -4670552;
+
+/// Represents a partially decoded `DiceCertChain` from the client VM.
+/// The whole chain is defined as following:
+///
+/// DiceCertChain = [
+///     PubKeyEd25519 / PubKeyECDSA256 / PubKeyECDSA384,  ; UDS_Pub
+///     + DiceChainEntry,               ; First CDI_Certificate -> Last CDI_Certificate
+/// ]
+#[derive(Debug, Clone)]
+pub(crate) struct ClientVmDiceChain {
+    pub(crate) payloads: Vec<DiceChainEntryPayload>,
+}
+
+impl ClientVmDiceChain {
+    /// Validates the signatures of the entries in the `client_vm_dice_chain` as following:
+    ///
+    /// - The first entry of the `client_vm_dice_chain` must be signed with the root public key.
+    /// - After the first entry, each entry of the `client_vm_dice_chain` must be signed with the
+    ///  subject public key of the previous entry.
+    ///
+    /// Returns a partially decoded client VM's DICE chain if the verification succeeds.
+    pub(crate) fn validate_signatures_and_parse_dice_chain(
+        mut client_vm_dice_chain: Vec<Value>,
+    ) -> Result<Self> {
+        let root_public_key =
+            CoseKey::from_cbor_value(client_vm_dice_chain.remove(0))?.try_into()?;
+
+        let mut payloads = Vec::with_capacity(client_vm_dice_chain.len());
+        let mut previous_public_key = &root_public_key;
+        for (i, value) in client_vm_dice_chain.into_iter().enumerate() {
+            let payload = DiceChainEntryPayload::validate_cose_signature_and_extract_payload(
+                value,
+                previous_public_key,
+            )
+            .map_err(|e| {
+                error!("Failed to verify the DICE chain entry {}: {:?}", i, e);
+                e
+            })?;
+            payloads.push(payload);
+            previous_public_key = &payloads.last().unwrap().subject_public_key;
+        }
+        // After successfully calling `validate_client_vm_dice_chain_prefix_match`, we can be
+        // certain that the client VM's DICE chain must contain at least three entries that
+        // describe:
+        // - pvmfw
+        // - Microdroid kernel
+        // - Apk/Apexes
+        assert!(
+            payloads.len() >= 3,
+            "The client VM DICE chain must contain at least three DiceChainEntryPayloads"
+        );
+        Ok(Self { payloads })
+    }
+
+    /// Returns true if all payloads in the DICE chain are in normal mode.
+    pub(crate) fn all_entries_are_secure(&self) -> bool {
+        self.payloads.iter().all(|p| p.mode == DiceMode::kDiceModeNormal)
+    }
+}
+
+/// Validates that the `client_vm_dice_chain` matches the `service_vm_dice_chain` up to the pvmfw
+/// entry.
+///
+/// Returns a CBOR value array of the client VM's DICE chain if the verification succeeds.
+pub(crate) fn validate_client_vm_dice_chain_prefix_match(
+    client_vm_dice_chain: &[u8],
+    service_vm_dice_chain: &[u8],
+) -> Result<Vec<Value>> {
+    let client_vm_dice_chain =
+        try_as_value_array(Value::from_slice(client_vm_dice_chain)?, "client_vm_dice_chain")?;
+    let service_vm_dice_chain =
+        try_as_value_array(Value::from_slice(service_vm_dice_chain)?, "service_vm_dice_chain")?;
+    if service_vm_dice_chain.len() < 3 {
+        // The service VM's DICE chain must contain the root key and at least two other entries
+        // that describe:
+        //   - pvmfw
+        //   - Service VM kernel
+        error!("The service VM DICE chain must contain at least three entries");
+        return Err(RequestProcessingError::InternalError);
+    }
+    // Ignores the last entry that describes service VM
+    let entries_up_to_pvmfw = &service_vm_dice_chain[0..(service_vm_dice_chain.len() - 1)];
+    if entries_up_to_pvmfw.len() + 2 != client_vm_dice_chain.len() {
+        // Client VM DICE chain = entries_up_to_pvmfw
+        //    + Microdroid kernel entry (added in pvmfw)
+        //    + Apk/Apexes entry (added in microdroid)
+        error!("The client VM's DICE chain must contain exactly two extra entries");
+        return Err(RequestProcessingError::InvalidDiceChain);
+    }
+    if entries_up_to_pvmfw != &client_vm_dice_chain[0..entries_up_to_pvmfw.len()] {
+        error!(
+            "The client VM's DICE chain does not match service VM's DICE chain up to \
+             the pvmfw entry"
+        );
+        return Err(RequestProcessingError::InvalidDiceChain);
+    }
+    Ok(client_vm_dice_chain)
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct PublicKey(CoseKey);
+
+impl TryFrom<CoseKey> for PublicKey {
+    type Error = RequestProcessingError;
+
+    fn try_from(key: CoseKey) -> Result<Self> {
+        if !key.key_ops.contains(&KeyOperation::Assigned(iana::KeyOperation::Verify)) {
+            error!("Public key does not support verification");
+            return Err(RequestProcessingError::InvalidDiceChain);
+        }
+        Ok(Self(key))
+    }
+}
+
+/// Represents a partially decoded `DiceChainEntryPayload`. The whole payload is defined in:
+///
+/// hardware/interfaces/security/rkp/aidl/android/hardware/security/keymint/
+/// generateCertificateRequestV2.cddl
+#[derive(Debug, Clone)]
+pub(crate) struct DiceChainEntryPayload {
+    /// TODO(b/310931749): Verify the DICE chain entry using the subject public key.
+    #[allow(dead_code)]
+    subject_public_key: PublicKey,
+    mode: DiceMode,
+    /// TODO(b/271275206): Verify Microdroid kernel authority and code hashes.
+    #[allow(dead_code)]
+    code_hash: [u8; HASH_SIZE],
+    #[allow(dead_code)]
+    authority_hash: [u8; HASH_SIZE],
+    /// TODO(b/313815907): Parse the config descriptor and read Apk/Apexes info in it.
+    #[allow(dead_code)]
+    config_descriptor: Vec<u8>,
+}
+
+impl DiceChainEntryPayload {
+    /// Validates the signature of the provided CBOR value with the provided public key and
+    /// extracts payload from the value.
+    fn validate_cose_signature_and_extract_payload(
+        value: Value,
+        _authority_public_key: &PublicKey,
+    ) -> Result<Self> {
+        let cose_sign1 = CoseSign1::from_cbor_value(value)?;
+        // TODO(b/310931749): Verify the DICE chain entry using `authority_public_key`.
+
+        let payload = cose_sign1.payload.ok_or_else(|| {
+            error!("No payload found in the DICE chain entry");
+            RequestProcessingError::InvalidDiceChain
+        })?;
+        let payload = Value::from_slice(&payload)?;
+        let Value::Map(entries) = payload else {
+            return Err(CoseError::UnexpectedItem(cbor_value_type(&payload), "map").into());
+        };
+        build_payload(entries)
+    }
+}
+
+fn build_payload(entries: Vec<(Value, Value)>) -> Result<DiceChainEntryPayload> {
+    let mut builder = PayloadBuilder::default();
+    for (key, value) in entries.into_iter() {
+        let Some(Ok(key)) = key.as_integer().map(i64::try_from) else {
+            error!("Invalid key found in the DICE chain entry: {:?}", key);
+            return Err(RequestProcessingError::InvalidDiceChain);
+        };
+        match key {
+            SUBJECT_PUBLIC_KEY => {
+                let subject_public_key = try_as_bytes(value, "subject_public_key")?;
+                let subject_public_key = CoseKey::from_slice(&subject_public_key)?.try_into()?;
+                builder.subject_public_key(subject_public_key)?;
+            }
+            MODE => builder.mode(to_mode(value)?)?,
+            CODE_HASH => builder.code_hash(try_as_byte_array(value, "code_hash")?)?,
+            AUTHORITY_HASH => {
+                builder.authority_hash(try_as_byte_array(value, "authority_hash")?)?
+            }
+            CONFIG_DESC => builder.config_descriptor(try_as_bytes(value, "config_descriptor")?)?,
+            _ => {}
+        }
+    }
+    builder.build()
+}
+
+fn try_as_value_array(v: Value, context: &str) -> coset::Result<Vec<Value>> {
+    if let Value::Array(data) = v {
+        Ok(data)
+    } else {
+        let v_type = cbor_value_type(&v);
+        error!("The provided value type '{v_type}' is not of type 'bytes': {context}");
+        Err(CoseError::UnexpectedItem(v_type, "array"))
+    }
+}
+
+fn try_as_byte_array<const N: usize>(v: Value, context: &str) -> Result<[u8; N]> {
+    let data = try_as_bytes(v, context)?;
+    data.try_into().map_err(|e| {
+        error!("The provided value '{context}' is not an array of length {N}: {e:?}");
+        RequestProcessingError::InternalError
+    })
+}
+
+fn to_mode(value: Value) -> Result<DiceMode> {
+    let mode = match value {
+        // Mode is supposed to be encoded as a 1-byte bstr, but some implementations instead
+        // encode it as an integer. Accept either. See b/273552826.
+        // If Mode is omitted, it should be treated as if it was NotConfigured, according to
+        // the Open Profile for DICE spec.
+        Value::Bytes(bytes) => {
+            if bytes.len() != 1 {
+                error!("Bytes array with invalid length for mode: {:?}", bytes.len());
+                return Err(RequestProcessingError::InvalidDiceChain);
+            }
+            bytes[0].into()
+        }
+        Value::Integer(i) => i,
+        v => return Err(CoseError::UnexpectedItem(cbor_value_type(&v), "bstr or int").into()),
+    };
+    let mode = match mode {
+        x if x == (DiceMode::kDiceModeNormal as i64).into() => DiceMode::kDiceModeNormal,
+        x if x == (DiceMode::kDiceModeDebug as i64).into() => DiceMode::kDiceModeDebug,
+        x if x == (DiceMode::kDiceModeMaintenance as i64).into() => DiceMode::kDiceModeMaintenance,
+        // If Mode is invalid, it should be treated as if it was NotConfigured, according to
+        // the Open Profile for DICE spec.
+        _ => DiceMode::kDiceModeNotInitialized,
+    };
+    Ok(mode)
+}
+
+#[derive(Default, Debug, Clone)]
+struct PayloadBuilder {
+    subject_public_key: OnceCell<PublicKey>,
+    mode: OnceCell<DiceMode>,
+    code_hash: OnceCell<[u8; HASH_SIZE]>,
+    authority_hash: OnceCell<[u8; HASH_SIZE]>,
+    config_descriptor: OnceCell<Vec<u8>>,
+}
+
+fn set_once<T>(field: &OnceCell<T>, value: T, field_name: &str) -> Result<()> {
+    field.set(value).map_err(|_| {
+        error!("Field '{field_name}' is duplicated in the Payload");
+        RequestProcessingError::InvalidDiceChain
+    })
+}
+
+fn take_value<T>(field: &mut OnceCell<T>, field_name: &str) -> Result<T> {
+    field.take().ok_or_else(|| {
+        error!("Field '{field_name}' is missing in the Payload");
+        RequestProcessingError::InvalidDiceChain
+    })
+}
+
+impl PayloadBuilder {
+    fn subject_public_key(&mut self, key: PublicKey) -> Result<()> {
+        set_once(&self.subject_public_key, key, "subject_public_key")
+    }
+
+    fn mode(&mut self, mode: DiceMode) -> Result<()> {
+        set_once(&self.mode, mode, "mode")
+    }
+
+    fn code_hash(&mut self, code_hash: [u8; HASH_SIZE]) -> Result<()> {
+        set_once(&self.code_hash, code_hash, "code_hash")
+    }
+
+    fn authority_hash(&mut self, authority_hash: [u8; HASH_SIZE]) -> Result<()> {
+        set_once(&self.authority_hash, authority_hash, "authority_hash")
+    }
+
+    fn config_descriptor(&mut self, config_descriptor: Vec<u8>) -> Result<()> {
+        set_once(&self.config_descriptor, config_descriptor, "config_descriptor")
+    }
+
+    fn build(mut self) -> Result<DiceChainEntryPayload> {
+        let subject_public_key = take_value(&mut self.subject_public_key, "subject_public_key")?;
+        // If Mode is omitted, it should be treated as if it was NotConfigured, according to
+        // the Open Profile for DICE spec.
+        let mode = self.mode.take().unwrap_or(DiceMode::kDiceModeNotInitialized);
+        let code_hash = take_value(&mut self.code_hash, "code_hash")?;
+        let authority_hash = take_value(&mut self.authority_hash, "authority_hash")?;
+        let config_descriptor = take_value(&mut self.config_descriptor, "config_descriptor")?;
+        Ok(DiceChainEntryPayload {
+            subject_public_key,
+            mode,
+            code_hash,
+            authority_hash,
+            config_descriptor,
+        })
+    }
+}
diff --git a/service_vm/requests/src/lib.rs b/service_vm/requests/src/lib.rs
index 3f687a4..0dfac09 100644
--- a/service_vm/requests/src/lib.rs
+++ b/service_vm/requests/src/lib.rs
@@ -21,6 +21,7 @@
 mod api;
 mod cert;
 mod client_vm;
+mod dice;
 mod keyblob;
 mod pub_key;
 mod rkp;