Repository: chipsalliance/caliptra-sw (rust)

Issue:
Add an RT mailbox command to populate the IDevID Certificate
The Caliptra IDevID cert is generated by an external CA and not Caliptra itself. On some systems, the IDevID may be persisted somewhere outside Caliptra, like in flash alongside SoC firmware.

We should add a mailbox command `POPULATE_IDEV_CERT` which allows the SoC to provide a DER-encoded IDevID certificate on ever boot. Once the certificate has been populated, the DPE `GetCertificateChain` command will also include the IDevID cert. If this command is not called, the chain returned by `GetCertificateChain` will start at LDevID (as it does today).

Pull request:
Add POPULATE_IDEV_CERT mailbox command to RT
This command will allow the SoC to add the idev id cert to the start of the certificate chain.

Fixes #1048 

Patch:
diff --git a/api/src/mailbox.rs b/api/src/mailbox.rs
index eb745238..c88e0f10 100644
--- a/api/src/mailbox.rs
+++ b/api/src/mailbox.rs
@@ -11,6 +11,7 @@ impl CommandId {
     pub const GET_IDEV_CSR: Self = Self(0x49444556); // "IDEV"
     pub const GET_IDEV_CERT: Self = Self(0x49444543); // IDEC
     pub const GET_IDEV_INFO: Self = Self(0x49444549); // IDEI
+    pub const POPULATE_IDEV_CERT: Self = Self(0x49444550); // IDEP
     pub const GET_LDEV_CERT: Self = Self(0x4C444556); // "LDEV"
     pub const ECDSA384_VERIFY: Self = Self(0x53494756); // "SIGV"
     pub const STASH_MEASUREMENT: Self = Self(0x4D454153); // "MEAS"
@@ -376,3 +377,16 @@ pub struct CapabilitiesResp {
     pub hdr: MailboxRespHeader,
     pub capabilities: [u8; crate::capabilities::Capabilities::SIZE_IN_BYTES],
 }
+
+// POPULATE_IDEV_CERT
+// No command-specific output args
+#[repr(C)]
+#[derive(Debug, AsBytes, FromBytes, PartialEq, Eq)]
+pub struct PopulateIdevCertReq {
+    pub hdr: MailboxReqHeader,
+    pub cert_size: u32,
+    pub cert: [u8; PopulateIdevCertReq::MAX_CERT_SIZE], // variable length
+}
+impl PopulateIdevCertReq {
+    pub const MAX_CERT_SIZE: usize = 1024;
+}
diff --git a/error/src/lib.rs b/error/src/lib.rs
index bff36585..c694ecd4 100644
--- a/error/src/lib.rs
+++ b/error/src/lib.rs
@@ -352,6 +352,8 @@ impl CaliptraError {
     pub const RUNTIME_PL1_USED_DPE_CONTEXT_THRESHOLD_EXCEEDED: CaliptraError =
         CaliptraError::new_const(0x000E001E);
     pub const RUNTIME_GLOBAL_WDT_EXPIRED: CaliptraError = CaliptraError::new_const(0x000E001F);
+    pub const RUNTIME_IDEV_CERT_POPULATION_FAILED: CaliptraError =
+        CaliptraError::new_const(0x000E0021);
 
     /// FMC Errors
     pub const FMC_GLOBAL_NMI: CaliptraError = CaliptraError::new_const(0x000F0001);
diff --git a/runtime/README.md b/runtime/README.md
index 84bfaa08..fc57621c 100644
--- a/runtime/README.md
+++ b/runtime/README.md
@@ -143,6 +143,29 @@ Table: `GET_IDEV_CERT` output arguments
 | cert_size   | u32        | Length in bytes of the cert field in use for the IDevId certificate
 | cert        | u8[1024]   | DER-encoded IDevID CERT
 
+### POPULATE\_IDEV\_CERT
+
+Exposes a command that allows the SoC to provide a DER-encoded
+IDevId certificate on every boot. The IDevId certificate is added 
+to the start of the certificate chain.
+
+Command Code: `0x4944_4550` ("IDEP")
+
+Table: `POPULATE_IDEV_CERT` input arguments
+
+| **Name**    | **Type**      | **Description**
+| --------    | --------      | ---------------
+| chksum      | u32           | Checksum over other input arguments, computed by the caller. Little endian.
+| cert_size   | u32           | Size of the DER-encoded IDevId certificate
+| cert        | u8[1024]      | DER-encoded IDevID CERT
+
+Table: `POPULATE_IDEV_CERT` output arguments
+
+| **Name**    | **Type** | **Description**
+| --------    | -------- | ---------------
+| chksum      | u32      | Checksum over other output arguments, computed by Caliptra. Little endian.
+| fips_status | u32      | Indicates if the command is FIPS approved or an error
+
 ### GET\_IDEV\_CSR
 
 ROM exposes a command to get a self-signed IDEVID CSR.
@@ -459,7 +482,8 @@ Caliptra models PAUSER callers to its mailbox as having 1 of 2 privilege levels:
 * PL0 - High Privilege. Only 1 PAUSER in the SoC may be at PL0. The PL0 PAUSER
   is denoted in the signed Caliptra firmware image. The PL0 PAUSER may call any
   supported DPE commands. Only PL0 can use the CertifyKey command. Success of the
-  CertifyKey command signifies to the caller that it is at PL0.
+  CertifyKey command signifies to the caller that it is at PL0. Only PL0 can use 
+  the POPULATE_IDEV_CERT mailbox command. 
 * PL1 - Restricted Privilege. All other PAUSERs in the SoC are PL1. Caliptra
   SHALL fail any calls to the DPE CertifyKey command by PL1 callers.
   PL1 callers should use the CertifyCsr command instead.
diff --git a/runtime/src/info.rs b/runtime/src/info.rs
index 4bd398fb..d5e02e67 100644
--- a/runtime/src/info.rs
+++ b/runtime/src/info.rs
@@ -1,8 +1,10 @@
 // Licensed under the Apache-2.0 license
 
-use crate::{handoff::RtHandoff, Drivers};
+use crate::{handoff::RtHandoff, Drivers, MAX_CERT_CHAIN_SIZE, PL0_PAUSER_FLAG};
+use arrayvec::ArrayVec;
 use caliptra_common::mailbox_api::{
     FwInfoResp, GetIdevCertReq, GetIdevCertResp, GetIdevInfoResp, MailboxResp, MailboxRespHeader,
+    PopulateIdevCertReq,
 };
 use caliptra_drivers::{CaliptraError, CaliptraResult};
 use caliptra_x509::{Ecdsa384CertBuilder, Ecdsa384Signature};
@@ -80,3 +82,34 @@ impl IDevIdCertCmd {
         }
     }
 }
+
+pub struct PopulateIDevIdCertCmd;
+impl PopulateIDevIdCertCmd {
+    pub(crate) fn execute(drivers: &mut Drivers, cmd_args: &[u8]) -> CaliptraResult<MailboxResp> {
+        if let Some(cmd) = PopulateIdevCertReq::read_from(cmd_args) {
+            let cert_size = cmd.cert_size as usize;
+            if cert_size > cmd.cert.len() {
+                return Err(CaliptraError::RUNTIME_MAILBOX_INVALID_PARAMS);
+            }
+
+            let flags = drivers.persistent_data.get().manifest1.header.flags;
+            // PL1 cannot call this mailbox command
+            if flags & PL0_PAUSER_FLAG == 0 {
+                return Err(CaliptraError::RUNTIME_INCORRECT_PAUSER_PRIVILEGE_LEVEL);
+            }
+
+            let mut tmp_chain = ArrayVec::<u8, MAX_CERT_CHAIN_SIZE>::new();
+            tmp_chain
+                .try_extend_from_slice(&cmd.cert[..cert_size])
+                .map_err(|_| CaliptraError::RUNTIME_IDEV_CERT_POPULATION_FAILED)?;
+            tmp_chain
+                .try_extend_from_slice(drivers.cert_chain.as_slice())
+                .map_err(|_| CaliptraError::RUNTIME_IDEV_CERT_POPULATION_FAILED)?;
+            drivers.cert_chain = tmp_chain;
+
+            Ok(MailboxResp::default())
+        } else {
+            Err(CaliptraError::RUNTIME_INSUFFICIENT_MEMORY)
+        }
+    }
+}
diff --git a/runtime/src/invoke_dpe.rs b/runtime/src/invoke_dpe.rs
index 46195128..54e4e425 100644
--- a/runtime/src/invoke_dpe.rs
+++ b/runtime/src/invoke_dpe.rs
@@ -1,6 +1,6 @@
 // Licensed under the Apache-2.0 license
 
-use crate::{CptraDpeTypes, DpeCrypto, DpeEnv, DpePlatform, Drivers};
+use crate::{CptraDpeTypes, DpeCrypto, DpeEnv, DpePlatform, Drivers, PL0_PAUSER_FLAG};
 use caliptra_common::mailbox_api::{InvokeDpeReq, InvokeDpeResp, MailboxResp, MailboxRespHeader};
 use caliptra_drivers::{CaliptraError, CaliptraResult};
 use crypto::{AlgLen, Crypto};
@@ -16,7 +16,6 @@ use zerocopy::FromBytes;
 
 pub struct InvokeDpeCmd;
 impl InvokeDpeCmd {
-    const PL0_PAUSER_FLAG: u32 = 1;
     pub const PL0_DPE_ACTIVE_CONTEXT_THRESHOLD: usize = 8;
     pub const PL1_DPE_ACTIVE_CONTEXT_THRESHOLD: usize = 16;
 
@@ -147,6 +146,6 @@ impl InvokeDpeCmd {
     }
 
     fn is_caller_pl1(pl0_pauser: u32, flags: u32, locality: u32) -> bool {
-        flags & Self::PL0_PAUSER_FLAG == 0 && locality != pl0_pauser
+        flags & PL0_PAUSER_FLAG == 0 && locality != pl0_pauser
     }
 }
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index b9beb5e4..a6b4980f 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -29,7 +29,7 @@ pub use fips::FipsShutdownCmd;
 #[cfg(feature = "fips_self_test")]
 pub use fips::{fips_self_test_cmd, fips_self_test_cmd::SelfTestStatus};
 
-pub use info::{FwInfoCmd, IDevIdCertCmd, IDevIdInfoCmd};
+pub use info::{FwInfoCmd, IDevIdCertCmd, IDevIdInfoCmd, PopulateIDevIdCertCmd};
 pub use invoke_dpe::InvokeDpeCmd;
 pub use stash_measurement::StashMeasurementCmd;
 pub use verify::EcdsaVerifyCmd;
@@ -75,6 +75,8 @@ impl From<RtBootStatus> for u32 {
 pub const DPE_SUPPORT: Support = Support::all();
 pub const MAX_CERT_CHAIN_SIZE: usize = 4096;
 
+pub const PL0_PAUSER_FLAG: u32 = 1;
+
 pub struct CptraDpeTypes;
 
 impl DpeTypes for CptraDpeTypes {
@@ -137,6 +139,7 @@ fn handle_command(drivers: &mut Drivers) -> CaliptraResult<MboxStatusE> {
         CommandId::STASH_MEASUREMENT => StashMeasurementCmd::execute(drivers, cmd_bytes),
         CommandId::DISABLE_ATTESTATION => DisableAttestationCmd::execute(drivers),
         CommandId::FW_INFO => FwInfoCmd::execute(drivers),
+        CommandId::POPULATE_IDEV_CERT => PopulateIDevIdCertCmd::execute(drivers, cmd_bytes),
         #[cfg(feature = "test_only_commands")]
         CommandId::TEST_ONLY_GET_LDEV_CERT => GetLdevCertCmd::execute(drivers),
         #[cfg(feature = "test_only_commands")]


Test patch:
diff --git a/runtime/doc/test-coverage.md b/runtime/doc/test-coverage.md
index 5b949a09..703b40c4 100644
--- a/runtime/doc/test-coverage.md
+++ b/runtime/doc/test-coverage.md
@@ -41,6 +41,7 @@ Checks that the get_idev_info mailbox command succeeds | **test_idev_id_info** |
 Checks that the get_idev_cert mailbox command succeeds and verifies the size of the resulting certificate | **test_idev_id_cert** | N/A
 Checks that the version mailbox command succeeds and validates the FIPS version response | **test_fips_cmd_api** | RUNTIME_SHUTDOWN
 Check that the error register is cleared when a successful mailbox command runs after a failed mailbox command | **test_error_cleared** | RUNTIME_MAILBOX_INVALID_PARAMS
+Calls the POPULATE_IDEV_CERT mailbox command and checks that the IDevId certificate is able to be parsed from the certificate chain | **test_populate_idev_cert_cmd** | N/A
 
 <br><br>
 # **Wycheproof Tests**
diff --git a/runtime/tests/runtime_integration_tests/integration_tests.rs b/runtime/tests/runtime_integration_tests/integration_tests.rs
index 9ec4440d..6a30a103 100644
--- a/runtime/tests/runtime_integration_tests/integration_tests.rs
+++ b/runtime/tests/runtime_integration_tests/integration_tests.rs
@@ -8,7 +8,7 @@ use caliptra_builder::{
 use caliptra_common::mailbox_api::{
     CommandId, EcdsaVerifyReq, FipsVersionResp, FwInfoResp, GetIdevCertReq, GetIdevCertResp,
     GetIdevInfoResp, InvokeDpeReq, InvokeDpeResp, MailboxReqHeader, MailboxRespHeader,
-    StashMeasurementReq, StashMeasurementResp,
+    PopulateIdevCertReq, StashMeasurementReq, StashMeasurementResp,
 };
 use caliptra_drivers::{CaliptraError, Ecc384PubKey};
 use caliptra_hw_model::{DefaultHwModel, HwModel, ModelError, ShaAccMode};
@@ -37,7 +37,7 @@ use openssl::{
     ec::{EcGroup, EcKey},
     ecdsa::EcdsaSig,
     nid::Nid,
-    pkey::PKey,
+    pkey::{PKey, Private},
     stack::Stack,
     x509::{
         store::X509StoreBuilder, verify::X509VerifyFlags, X509Builder, X509StoreContext,
@@ -486,6 +486,171 @@ fn test_invoke_dpe_get_certificate_chain_cmd() {
     assert_ne!([0u8; 2048], cert_chain.certificate_chain);
 }
 
+fn get_full_cert_chain(model: &mut DefaultHwModel, out: &mut [u8; 4096]) -> usize {
+    // first half
+    let mut data = [0u8; InvokeDpeReq::DATA_MAX_SIZE];
+    let get_cert_chain_cmd = GetCertificateChainCmd {
+        offset: 0,
+        size: 2048,
+    };
+    let cmd_hdr = CommandHdr::new_for_test(Command::GET_CERTIFICATE_CHAIN);
+    let cmd_hdr_buf = cmd_hdr.as_bytes();
+    data[..cmd_hdr_buf.len()].copy_from_slice(cmd_hdr_buf);
+    let dpe_cmd_buf = get_cert_chain_cmd.as_bytes();
+    data[cmd_hdr_buf.len()..cmd_hdr_buf.len() + dpe_cmd_buf.len()].copy_from_slice(dpe_cmd_buf);
+    let cmd = InvokeDpeReq {
+        hdr: MailboxReqHeader { chksum: 0 },
+        data,
+        data_size: (cmd_hdr_buf.len() + dpe_cmd_buf.len()) as u32,
+    };
+
+    let checksum = caliptra_common::checksum::calc_checksum(
+        u32::from(CommandId::INVOKE_DPE),
+        &cmd.as_bytes()[4..],
+    );
+
+    let cmd = InvokeDpeReq {
+        hdr: MailboxReqHeader { chksum: checksum },
+        ..cmd
+    };
+
+    let resp = model
+        .mailbox_execute(u32::from(CommandId::INVOKE_DPE), cmd.as_bytes())
+        .unwrap()
+        .expect("We should have received a response");
+
+    let mut resp_hdr = InvokeDpeResp::default();
+    resp_hdr.as_bytes_mut()[..resp.len()].copy_from_slice(&resp);
+
+    let cert_chunk_1 =
+        GetCertificateChainResp::read_from(&resp_hdr.data[..resp_hdr.data_size as usize]).unwrap();
+    out[..cert_chunk_1.certificate_size as usize]
+        .copy_from_slice(&cert_chunk_1.certificate_chain[..cert_chunk_1.certificate_size as usize]);
+
+    // second half
+    let mut data = [0u8; InvokeDpeReq::DATA_MAX_SIZE];
+    let get_cert_chain_cmd = GetCertificateChainCmd {
+        offset: cert_chunk_1.certificate_size,
+        size: 2048,
+    };
+    let cmd_hdr = CommandHdr::new_for_test(Command::GET_CERTIFICATE_CHAIN);
+    let cmd_hdr_buf = cmd_hdr.as_bytes();
+    data[..cmd_hdr_buf.len()].copy_from_slice(cmd_hdr_buf);
+    let dpe_cmd_buf = get_cert_chain_cmd.as_bytes();
+    data[cmd_hdr_buf.len()..cmd_hdr_buf.len() + dpe_cmd_buf.len()].copy_from_slice(dpe_cmd_buf);
+    let cmd = InvokeDpeReq {
+        hdr: MailboxReqHeader { chksum: 0 },
+        data,
+        data_size: (cmd_hdr_buf.len() + dpe_cmd_buf.len()) as u32,
+    };
+
+    let checksum = caliptra_common::checksum::calc_checksum(
+        u32::from(CommandId::INVOKE_DPE),
+        &cmd.as_bytes()[4..],
+    );
+
+    let cmd = InvokeDpeReq {
+        hdr: MailboxReqHeader { chksum: checksum },
+        ..cmd
+    };
+
+    let resp = model
+        .mailbox_execute(u32::from(CommandId::INVOKE_DPE), cmd.as_bytes())
+        .unwrap()
+        .expect("We should have received a response");
+
+    let mut resp_hdr = InvokeDpeResp::default();
+    resp_hdr.as_bytes_mut()[..resp.len()].copy_from_slice(&resp);
+
+    let cert_chunk_2 =
+        GetCertificateChainResp::read_from(&resp_hdr.data[..resp_hdr.data_size as usize]).unwrap();
+    out[cert_chunk_1.certificate_size as usize
+        ..cert_chunk_1.certificate_size as usize + cert_chunk_2.certificate_size as usize]
+        .copy_from_slice(&cert_chunk_2.certificate_chain[..cert_chunk_2.certificate_size as usize]);
+
+    cert_chunk_1.certificate_size as usize + cert_chunk_2.certificate_size as usize
+}
+
+// Will panic if any of the cert chain chunks is not a valid X.509 cert
+fn parse_cert_chain(cert_chain: &[u8], cert_chain_size: usize, expected_num_certs: u32) {
+    let mut i = 0;
+    let mut cert_count = 0;
+    while i < cert_chain_size {
+        let curr_cert = X509::from_der(&cert_chain[i..]).unwrap();
+        i += curr_cert.to_der().unwrap().len();
+        cert_count += 1;
+    }
+    assert_eq!(expected_num_certs, cert_count);
+}
+
+#[test]
+fn test_populate_idev_cert_cmd() {
+    let mut model = run_rt_test(None, None, None);
+
+    model.step_until(|m| {
+        m.soc_ifc().cptra_boot_status().read() == u32::from(RtBootStatus::RtReadyForCommands)
+    });
+
+    let mut cert_chain_without_idev_cert = [0u8; 4096];
+    let cert_chain_len_without_idev_cert =
+        get_full_cert_chain(&mut model, &mut cert_chain_without_idev_cert);
+
+    // generate test idev cert
+    let ec_group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
+    let ec_key = PKey::from_ec_key(EcKey::generate(&ec_group).unwrap()).unwrap();
+
+    let cert = generate_test_x509_cert(ec_key);
+
+    // copy der encoded idev cert
+    let cert_bytes = cert.to_der().unwrap();
+    let mut cert_slice = [0u8; PopulateIdevCertReq::MAX_CERT_SIZE];
+    cert_slice[..cert_bytes.len()].copy_from_slice(&cert_bytes);
+
+    let pop_idev_cmd = PopulateIdevCertReq {
+        hdr: MailboxReqHeader { chksum: 0 },
+        cert_size: cert_bytes.len() as u32,
+        cert: cert_slice,
+    };
+    let checksum = caliptra_common::checksum::calc_checksum(
+        u32::from(CommandId::POPULATE_IDEV_CERT),
+        &pop_idev_cmd.as_bytes()[4..],
+    );
+
+    let pop_idev_cmd = PopulateIdevCertReq {
+        hdr: MailboxReqHeader { chksum: checksum },
+        ..pop_idev_cmd
+    };
+
+    // call populate idev cert so that the idev cert is added to the certificate chain
+    model
+        .mailbox_execute(
+            u32::from(CommandId::POPULATE_IDEV_CERT),
+            pop_idev_cmd.as_bytes(),
+        )
+        .unwrap()
+        .expect("We should have received a response");
+
+    let mut cert_chain_with_idev_cert = [0u8; 4096];
+    let cert_chain_len_with_idev_cert =
+        get_full_cert_chain(&mut model, &mut cert_chain_with_idev_cert);
+
+    // read idev cert from prefix of cert chain and parse it as X509
+    let idev_len = cert_chain_len_with_idev_cert - cert_chain_len_without_idev_cert;
+    let idev_cert = X509::from_der(&cert_chain_with_idev_cert[..idev_len]).unwrap();
+    assert_eq!(idev_cert, cert);
+
+    // ensure rest of cert chain is not corrupted
+    assert_eq!(
+        cert_chain_without_idev_cert[..cert_chain_len_without_idev_cert],
+        cert_chain_with_idev_cert[idev_len..cert_chain_len_with_idev_cert]
+    );
+    parse_cert_chain(
+        &cert_chain_with_idev_cert[idev_len..],
+        cert_chain_len_with_idev_cert - idev_len,
+        3,
+    );
+}
+
 #[test]
 fn test_pauser_privilege_level_dpe_context_thresholds() {
     let mut model = run_rt_test(None, None, None);
@@ -1038,14 +1203,7 @@ fn test_idev_id_info() {
     GetIdevInfoResp::read_from(resp.as_slice()).unwrap();
 }
 
-#[test]
-fn test_idev_id_cert() {
-    let mut model = run_rt_test(None, None, None);
-
-    // generate 48 byte ECDSA key pair
-    let ec_group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
-    let ec_key = PKey::from_ec_key(EcKey::generate(&ec_group).unwrap()).unwrap();
-
+fn generate_test_x509_cert(ec_key: PKey<Private>) -> X509 {
     let mut cert_builder = X509Builder::new().unwrap();
     cert_builder.set_version(2).unwrap();
     cert_builder
@@ -1065,9 +1223,19 @@ fn test_idev_id_cert() {
     cert_builder
         .set_not_after(&Asn1Time::days_from_now(365).unwrap())
         .unwrap();
-
     cert_builder.sign(&ec_key, MessageDigest::sha384()).unwrap();
-    let cert = cert_builder.build();
+    cert_builder.build()
+}
+
+#[test]
+fn test_idev_id_cert() {
+    let mut model = run_rt_test(None, None, None);
+
+    // generate 48 byte ECDSA key pair
+    let ec_group = EcGroup::from_curve_name(Nid::SECP384R1).unwrap();
+    let ec_key = PKey::from_ec_key(EcKey::generate(&ec_group).unwrap()).unwrap();
+
+    let cert = generate_test_x509_cert(ec_key.clone());
     assert!(cert.verify(&ec_key).unwrap());
 
     // Extract the r and s values of the signature