diff --git a/fw/core/crypto/x509-builder/gen/src/main.rs b/fw/core/crypto/x509-builder/gen/src/main.rs index f4000ac11..d925dfb37 100644 --- a/fw/core/crypto/x509-builder/gen/src/main.rs +++ b/fw/core/crypto/x509-builder/gen/src/main.rs @@ -68,6 +68,13 @@ fn main() { ); // Leaf + // + // NOTE: the firmware does NOT use this generic 2-RDN (CN + serialNumber) + // leaf template. The firmware's PID leaf uses a hand-ported, single- + // `commonName` template (issuer CN(64), subject CN(32)) in `leaf_cert.rs` + // that chains to the HSP-provisioned alias certificate. To avoid clobbering + // that hand-maintained file, this generator writes the generic template to + // `leaf_cert.generated.rs` (reference only; not compiled by the firmware). println!(" Generating Leaf certificate template..."); let leaf = cert::build_leaf_cert(); let leaf_src = code_gen::emit_template_module( @@ -75,7 +82,8 @@ fn main() { &leaf.tbs, &leaf.fields, ); - fs::write(out_dir.join("leaf_cert.rs"), leaf_src).expect("write leaf_cert.rs"); + fs::write(out_dir.join("leaf_cert.generated.rs"), leaf_src) + .expect("write leaf_cert.generated.rs"); println!( " TBS size: {} bytes, {} variable fields", leaf.tbs.len(), diff --git a/fw/core/crypto/x509-builder/src/cert_builder.rs b/fw/core/crypto/x509-builder/src/cert_builder.rs index e472d33b0..2e6ab70ae 100644 --- a/fw/core/crypto/x509-builder/src/cert_builder.rs +++ b/fw/core/crypto/x509-builder/src/cert_builder.rs @@ -157,9 +157,13 @@ pub struct RootCertParams<'a> { pub subject_key_id: &'a [u8; 20], } -/// Parameters for building a Leaf (end-entity) certificate. +/// Parameters for building the partition-id (PID) leaf certificate. /// -/// All CN/SN strings are validated and padded internally by the builder. +/// The leaf uses the single-`commonName` profile (issuer `CN(64)`, subject +/// `CN(32)`) so its issuer DN byte-matches the CP alias certificate's subject +/// DN and the leaf chains to it. Every field is a fixed-size raw byte array +/// patched verbatim (no CN/SN string validation or padding): in particular +/// `issuer_cn` is copied byte-for-byte from the alias cert's subject CN. pub struct LeafCertParams<'a> { /// Uncompressed P-384 public key (97 bytes: `0x04 || x || y`). pub public_key: &'a [u8; 97], @@ -169,20 +173,14 @@ pub struct LeafCertParams<'a> { pub not_before: &'a [u8; 15], /// NOT_AFTER as GeneralizedTime ASCII (15 bytes). pub not_after: &'a [u8; 15], - /// Subject Common Name (ASCII, max [`CN_LEN`] bytes; space-padded internally). - pub subject_cn: &'a str, - /// Subject serialNumber (max [`SN_LEN`] bytes; zero-padded internally). - pub subject_sn: &'a str, - /// Issuer Common Name (ASCII, max [`CN_LEN`] bytes; space-padded internally). - pub issuer_cn: &'a str, - /// Issuer serialNumber (max [`SN_LEN`] bytes; zero-padded internally). - pub issuer_sn: &'a str, + /// Issuer commonName value (64 bytes) — the alias certificate's subject CN. + pub issuer_cn: &'a [u8; 64], + /// Subject commonName value (32 bytes). + pub subject_cn: &'a [u8; 32], /// Subject Key Identifier (SHA-1 of the subject's public key, 20 bytes). pub subject_key_id: &'a [u8; 20], - /// Authority Key Identifier (SHA-1 of the issuer's public key, 20 bytes). + /// Authority Key Identifier (the alias cert's Subject Key Identifier, 20 bytes). pub authority_key_id: &'a [u8; 20], - /// Key Usage extension flags (see [`KeyUsage`] named constants). - pub key_usage: KeyUsage, } /// Pad an ASCII CN string to exactly [`CN_LEN`] bytes with trailing spaces. @@ -243,8 +241,10 @@ pub async fn build_root_cert<'a>( /// Build a Leaf (end-entity) certificate from the /// [`leaf_cert`](crate::leaf_cert) template. /// -/// See the crate-level docs for the shared `(pal, io, alloc, params, -/// priv_key, out)` contract and query/copy semantics. +/// Thin wrapper over [`build_leaf_cert_with_signer`]: constructs the default +/// P-384 [`TbsSigner`] (`pal.ecc_sign` + `priv_key`) and delegates. The +/// public contract — `(pal, io, alloc, params, priv_key, out)` with the +/// query/copy semantics from the crate-level docs — is unchanged. pub async fn build_leaf_cert<'a>( pal: &(impl HsmCrypto + HsmAlloc + 'a), io: &impl HsmIo, @@ -253,24 +253,141 @@ pub async fn build_leaf_cert<'a>( priv_key: &DmaBuf, out: Option<&mut [u8]>, ) -> HsmResult { - use crate::leaf_cert::TBS_TEMPLATE; - let tbs_len = TBS_TEMPLATE.len(); - preflight(priv_key, tbs_len)?; - let max_size = max_signed_size(tbs_len); + // Validate `priv_key` up front so even the query path (`out = None`) + // rejects a bad key, matching pre-refactor behaviour. The `_with_signer` + // variant has no priv_key to check, so this preflight is exclusive to + // this default-signer entry point. + preflight(priv_key, crate::leaf_cert::TBS_TEMPLATE.len())?; + let signer = EccP384Signer { + pal, + alloc, + priv_key, + }; + build_leaf_cert_with_signer(pal, io, alloc, params, &signer, out).await +} + +/// Signer hook for [`build_leaf_cert_with_signer`]. +/// +/// Produces a raw ECDSA-P384 signature — `r || s`, little-endian by half, +/// `SIGNATURE_LEN` (96) bytes — over a caller-supplied SHA-384 `digest`, +/// writing it into `sig`. Implement this to inject a signer other than the +/// PAL's default [`HsmEcc::ecc_sign`]; in particular a *deterministic* +/// (RFC 6979) signer, so the resulting certificate is byte-stable across +/// regenerations (required for cacheable, lazily-regenerated leaf certs). +/// +/// [`HsmEcc::ecc_sign`]: azihsm_fw_hsm_pal_traits::HsmEcc::ecc_sign +#[allow(async_fn_in_trait)] +pub trait TbsSigner { + /// Sign `digest` (48-byte SHA-384, natural big-endian) into `sig` + /// (`r || s`, LE-by-half, 96 B). The signer converts the digest to its + /// backend's operand order (a PKA/`ecc_sign_deterministic` signer reverses + /// it to little-endian). + async fn sign_digest( + &self, + io: &impl HsmIo, + digest: &DmaBuf, + sig: &mut DmaBuf, + ) -> HsmResult<()>; +} + +/// Build a Leaf certificate whose TBS signature is produced by `signer` +/// instead of the PAL's default `ecc_sign`. +/// +/// Identical to [`build_leaf_cert`] except for the signing step: use this +/// with a deterministic (RFC 6979) [`TbsSigner`] so the leaf is byte-stable +/// across regenerations. `pal` is still used for the SHA-384 over the TBS. +/// +/// Query/copy: `out = None` returns the worst-case size; `Some(buf)` builds. +pub async fn build_leaf_cert_with_signer<'a>( + pal: &(impl HsmCrypto + HsmAlloc + 'a), + io: &impl HsmIo, + alloc: &'a impl HsmScopedAlloc, + params: &LeafCertParams<'_>, + signer: &impl TbsSigner, + out: Option<&mut [u8]>, +) -> HsmResult { + build_signed_from_template( + pal, + io, + alloc, + crate::leaf_cert::TBS_TEMPLATE.len(), + |tbs| patch_leaf_tbs(tbs, params), + signer, + out, + ) + .await +} + +/// Shared core for the `build_*_with_signer` entry points: size-check the +/// template, then (in copy mode) patch a fresh TBS via `patch`, SHA-384 it, +/// sign it with `signer`, and assemble the DER certificate into `out`. +/// +/// The per-profile difference is exactly `template_len` + `patch`; keeping +/// this plumbing in one place avoids duplicating it per certificate profile. +/// In query mode (`out = None`) it returns the worst-case signed size without +/// allocating or patching. +async fn build_signed_from_template<'a>( + pal: &(impl HsmCrypto + HsmAlloc + 'a), + io: &impl HsmIo, + alloc: &'a impl HsmScopedAlloc, + template_len: usize, + patch: impl FnOnce(&mut [u8]) -> HsmResult<()>, + signer: &impl TbsSigner, + out: Option<&mut [u8]>, +) -> HsmResult { + if template_len > MAX_TBS_LEN { + return Err(HsmError::InvalidArg); + } + let max_size = max_signed_size(template_len); let Some(out) = out else { return Ok(max_size); }; if out.len() < max_size { return Err(HsmError::InvalidArg); } - let (tbs_dma, sig_dma) = sign(pal, io, alloc, priv_key, tbs_len, |tbs| { - patch_leaf_tbs(tbs, params) - }) - .await?; + + let tbs_dma = alloc.dma_alloc(template_len)?; + patch(tbs_dma)?; + + let digest_dma = alloc.dma_alloc(SHA384_DIGEST_LEN)?; + // Natural big-endian SHA-384 over the TBS; the `TbsSigner` reverses it to + // the PKA little-endian message hash (`big_endian = false` would be a + // per-word swap, not the full reversal the PKA sign needs). + pal.hash(io, HsmHashAlgo::Sha384, tbs_dma, digest_dma, true) + .await?; + + let sig_dma = alloc.dma_alloc(SIGNATURE_LEN)?; + signer.sign_digest(io, digest_dma, sig_dma).await?; + assemble_signed(out, tbs_dma, sig_dma) } -/// Common input validation shared by every builder. +struct EccP384Signer<'a, P: HsmCrypto, A: HsmScopedAlloc> { + pal: &'a P, + alloc: &'a A, + priv_key: &'a DmaBuf, +} + +impl TbsSigner for EccP384Signer<'_, P, A> { + async fn sign_digest( + &self, + io: &impl HsmIo, + digest: &DmaBuf, + sig: &mut DmaBuf, + ) -> HsmResult<()> { + // `ecc_sign` consumes the message hash in PKA-native little-endian: the + // full byte reversal of the natural big-endian SHA-384 digest that + // `build_signed_from_template` produces (per the `TbsSigner` contract). + let e = self.alloc.dma_alloc(SHA384_DIGEST_LEN)?; + for i in 0..SHA384_DIGEST_LEN { + e[i] = digest[SHA384_DIGEST_LEN - 1 - i]; + } + self.pal + .ecc_sign(io, HsmEccCurve::P384, self.priv_key, e, sig) + .await + } +} + fn preflight(priv_key: &DmaBuf, tbs_len: usize) -> HsmResult<()> { if priv_key.len() != PRIV_KEY_LEN { return Err(HsmError::InvalidArg); @@ -300,11 +417,19 @@ where patch(tbs_dma)?; let digest_dma = alloc.dma_alloc(SHA384_DIGEST_LEN)?; - pal.hash(io, HsmHashAlgo::Sha384, tbs_dma, digest_dma, false) + // Natural big-endian SHA-384, then a FULL byte reversal to the PKA-native + // little-endian message hash `ecc_sign` consumes. `big_endian = false` is + // only a per-word swap on Uno, not the full reversal the PKA path needs. + pal.hash(io, HsmHashAlgo::Sha384, tbs_dma, digest_dma, true) .await?; + let hash_le = alloc.dma_alloc(SHA384_DIGEST_LEN)?; + for i in 0..SHA384_DIGEST_LEN { + hash_le[i] = digest_dma[SHA384_DIGEST_LEN - 1 - i]; + } + let sig_dma = alloc.dma_alloc(SIGNATURE_LEN)?; - pal.ecc_sign(io, HsmEccCurve::P384, priv_key, digest_dma, sig_dma) + pal.ecc_sign(io, HsmEccCurve::P384, priv_key, hash_le, sig_dma) .await?; Ok((tbs_dma, sig_dma)) @@ -339,26 +464,16 @@ fn patch_root_tbs(out: &mut [u8], params: &RootCertParams<'_>) -> HsmResult<()> fn patch_leaf_tbs(out: &mut [u8], params: &LeafCertParams<'_>) -> HsmResult<()> { use crate::leaf_cert::*; validate_serial(params.serial_number)?; - if params.key_usage.unused_bits() > 7 { - return Err(HsmError::InvalidArg); - } - let subject_cn = pad_cn(params.subject_cn).ok_or(HsmError::InvalidArg)?; - let subject_sn = pad_sn(params.subject_sn).ok_or(HsmError::InvalidArg)?; - let issuer_cn = pad_cn(params.issuer_cn).ok_or(HsmError::InvalidArg)?; - let issuer_sn = pad_sn(params.issuer_sn).ok_or(HsmError::InvalidArg)?; out[..TBS_TEMPLATE.len()].copy_from_slice(&TBS_TEMPLATE); patch_field(out, PUBLIC_KEY_OFFSET, params.public_key); patch_field(out, SERIAL_NUMBER_OFFSET, params.serial_number); patch_field(out, NOT_BEFORE_OFFSET, params.not_before); patch_field(out, NOT_AFTER_OFFSET, params.not_after); - patch_field(out, ISSUER_CN_OFFSET, &issuer_cn); - patch_field(out, ISSUER_SN_OFFSET, &issuer_sn); - patch_field(out, SUBJECT_CN_OFFSET, &subject_cn); - patch_field(out, SUBJECT_SN_OFFSET, &subject_sn); + patch_field(out, ISSUER_CN_OFFSET, params.issuer_cn); + patch_field(out, SUBJECT_CN_OFFSET, params.subject_cn); patch_field(out, SUBJECT_KEY_ID_OFFSET, params.subject_key_id); patch_field(out, AUTHORITY_KEY_ID_OFFSET, params.authority_key_id); - patch_field(out, KEY_USAGE_OFFSET, ¶ms.key_usage.to_bytes()); Ok(()) } diff --git a/fw/core/crypto/x509-builder/src/leaf_cert.rs b/fw/core/crypto/x509-builder/src/leaf_cert.rs index 41bbe6caf..2c6c84dcb 100644 --- a/fw/core/crypto/x509-builder/src/leaf_cert.rs +++ b/fw/core/crypto/x509-builder/src/leaf_cert.rs @@ -1,50 +1,58 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// AUTO-GENERATED by x509-gen. Do not edit manually. +// Ported from the mcr-hsm reference firmware +// (`hsm/src/x509/build/azihsm_leaf_cert_tbs.rs`). +// +// WARNING: This file is hand-maintained, NOT auto-generated. Do NOT run the +// x509-gen generator to (re)produce it — the generator emits a generic 2-RDN +// (CN + serialNumber) leaf template that does NOT chain to the HSP-provisioned +// alias certificate and would silently break the PID cert chain. If the +// generator is ever run, restore this file from git. Regenerate the profile in +// mcr-hsm and re-port if the profile genuinely changes. -//! Leaf certificate TBS template (auto-generated). +//! Partition-id (PID) leaf certificate TBS template. +//! +//! The leaf's issuer and subject are each a single `commonName` RDN — issuer +//! `CN(64)`, subject `CN(32)`. The issuer CN is set byte-for-byte to the CP +//! alias certificate's subject CN so the PID leaf chains to the alias cert; +//! the 64-byte issuer CN is what the HSP-provisioned alias certificate uses. +//! Key usage (digitalSignature) and BasicConstraints(CA:FALSE) are fixed in +//! the template, so they are not variable fields. /// DER-encoded TBS template with placeholder bytes (0x5F) for variable fields. -pub const TBS_TEMPLATE: [u8; 537] = [ - 0x30, 0x82, 0x02, 0x15, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, +pub const TBS_TEMPLATE: [u8; 422] = [ + 0x30, 0x82, 0x01, 0xA2, 0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, - 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03, 0x30, 0x76, 0x31, 0x29, 0x30, - 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x20, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x0A, 0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x03, 0x30, 0x4B, 0x31, 0x49, 0x30, + 0x47, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x40, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x31, 0x49, 0x30, 0x47, 0x06, 0x03, 0x55, 0x04, - 0x05, 0x13, 0x40, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x22, 0x18, 0x0F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x18, 0x0F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x2B, 0x31, 0x29, + 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, 0x20, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x30, 0x22, 0x18, 0x0F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x18, 0x0F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x76, 0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, - 0x04, 0x03, 0x0C, 0x20, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2A, + 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x31, 0x49, 0x30, 0x47, 0x06, 0x03, 0x55, 0x04, 0x05, 0x13, 0x40, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, - 0x76, 0x30, 0x10, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x05, 0x2B, 0x81, - 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0xA3, 0x60, 0x30, 0x5E, 0x30, 0x0C, 0x06, 0x03, 0x55, - 0x1D, 0x13, 0x01, 0x01, 0xFF, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0E, 0x06, 0x03, 0x55, 0x1D, 0x0F, - 0x01, 0x01, 0xFF, 0x04, 0x04, 0x03, 0x02, 0x5F, 0x5F, 0x30, 0x1D, 0x06, 0x03, 0x55, 0x1D, 0x0E, - 0x04, 0x16, 0x04, 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x1F, 0x06, 0x03, 0x55, 0x1D, 0x23, 0x04, - 0x18, 0x30, 0x16, 0x80, 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, - 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0xA3, 0x63, 0x30, 0x61, 0x30, 0x0F, 0x06, 0x03, 0x55, 0x1D, 0x13, 0x01, 0x01, 0xFF, 0x04, + 0x05, 0x30, 0x03, 0x02, 0x01, 0x00, 0x30, 0x0E, 0x06, 0x03, 0x55, 0x1D, 0x0F, 0x01, 0x01, 0xFF, + 0x04, 0x04, 0x03, 0x02, 0x07, 0x80, 0x30, 0x1D, 0x06, 0x03, 0x55, 0x1D, 0x0E, 0x04, 0x16, 0x04, + 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x30, 0x1F, 0x06, 0x03, 0x55, 0x1D, 0x23, 0x04, 0x18, 0x30, 0x16, + 0x80, 0x14, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, + 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, ]; /// Offset of the PUBLIC_KEY field in TBS_TEMPLATE. -pub const PUBLIC_KEY_OFFSET: usize = 342; +pub const PUBLIC_KEY_OFFSET: usize = 224; /// Length of the PUBLIC_KEY field in bytes. pub const PUBLIC_KEY_LEN: usize = 97; @@ -54,46 +62,31 @@ pub const SERIAL_NUMBER_OFFSET: usize = 11; pub const SERIAL_NUMBER_LEN: usize = 20; /// Offset of the NOT_BEFORE field in TBS_TEMPLATE. -pub const NOT_BEFORE_OFFSET: usize = 167; +pub const NOT_BEFORE_OFFSET: usize = 124; /// Length of the NOT_BEFORE field in bytes. pub const NOT_BEFORE_LEN: usize = 15; /// Offset of the NOT_AFTER field in TBS_TEMPLATE. -pub const NOT_AFTER_OFFSET: usize = 184; +pub const NOT_AFTER_OFFSET: usize = 141; /// Length of the NOT_AFTER field in bytes. pub const NOT_AFTER_LEN: usize = 15; /// Offset of the ISSUER_CN field in TBS_TEMPLATE. pub const ISSUER_CN_OFFSET: usize = 56; /// Length of the ISSUER_CN field in bytes. -pub const ISSUER_CN_LEN: usize = 32; - -/// Offset of the ISSUER_SN field in TBS_TEMPLATE. -pub const ISSUER_SN_OFFSET: usize = 99; -/// Length of the ISSUER_SN field in bytes. -pub const ISSUER_SN_LEN: usize = 64; +pub const ISSUER_CN_LEN: usize = 64; /// Offset of the SUBJECT_CN field in TBS_TEMPLATE. -pub const SUBJECT_CN_OFFSET: usize = 212; +pub const SUBJECT_CN_OFFSET: usize = 169; /// Length of the SUBJECT_CN field in bytes. pub const SUBJECT_CN_LEN: usize = 32; -/// Offset of the SUBJECT_SN field in TBS_TEMPLATE. -pub const SUBJECT_SN_OFFSET: usize = 255; -/// Length of the SUBJECT_SN field in bytes. -pub const SUBJECT_SN_LEN: usize = 64; - /// Offset of the SUBJECT_KEY_ID field in TBS_TEMPLATE. -pub const SUBJECT_KEY_ID_OFFSET: usize = 484; +pub const SUBJECT_KEY_ID_OFFSET: usize = 369; /// Length of the SUBJECT_KEY_ID field in bytes. pub const SUBJECT_KEY_ID_LEN: usize = 20; /// Offset of the AUTHORITY_KEY_ID field in TBS_TEMPLATE. -pub const AUTHORITY_KEY_ID_OFFSET: usize = 517; +pub const AUTHORITY_KEY_ID_OFFSET: usize = 402; /// Length of the AUTHORITY_KEY_ID field in bytes. pub const AUTHORITY_KEY_ID_LEN: usize = 20; - -/// Offset of the KEY_USAGE field in TBS_TEMPLATE. -pub const KEY_USAGE_OFFSET: usize = 471; -/// Length of the KEY_USAGE field in bytes. -pub const KEY_USAGE_LEN: usize = 2; diff --git a/fw/core/crypto/x509-builder/src/lib.rs b/fw/core/crypto/x509-builder/src/lib.rs index 906fe4bc9..e60af4431 100644 --- a/fw/core/crypto/x509-builder/src/lib.rs +++ b/fw/core/crypto/x509-builder/src/lib.rs @@ -78,7 +78,7 @@ //! | Type | Module | Extensions | //! |------|--------|------------| //! | Root CA (self-signed) | [`root_cert`] | BasicConstraints(CA:TRUE), KeyUsage(keyCertSign+cRLSign), SKI | -//! | Leaf (end-entity) | [`leaf_cert`] | BasicConstraints(CA:FALSE), KeyUsage(variable), SKI, AKI | +//! | Leaf (end-entity) | [`leaf_cert`] | BasicConstraints(CA:FALSE), KeyUsage(digitalSignature, fixed), SKI, AKI | //! | CSR (PKCS#10) | [`csr`] | Subject DN only (CN + serialNumber) | /// Runtime certificate builder for Root CA and Leaf certificates. @@ -94,12 +94,15 @@ pub mod der_helpers; pub mod padding; // Auto-generated template modules (regenerated by -// `azihsm_fw_core_crypto_x509_builder_gen`). +// `azihsm_fw_core_crypto_x509_builder_gen`), except `leaf_cert` which is +// ported from the mcr-hsm reference firmware (single-CN PID leaf profile). /// CSR (PKCS#10) TBS template — auto-generated. pub mod csr; -/// Leaf (end-entity) certificate TBS template — auto-generated. +/// Partition-id (PID) leaf certificate TBS template — ported from the mcr-hsm +/// reference firmware (single `commonName` issuer/subject, chains to the CP +/// alias certificate). pub mod leaf_cert; /// Root CA (self-signed) certificate TBS template — auto-generated. diff --git a/fw/core/ddi/tbor/types/src/part_info.rs b/fw/core/ddi/tbor/types/src/part_info.rs index be733441f..94b365a22 100644 --- a/fw/core/ddi/tbor/types/src/part_info.rs +++ b/fw/core/ddi/tbor/types/src/part_info.rs @@ -115,7 +115,8 @@ pub struct TborPartInfoResp<'a> { #[tbor(buffer, len = 16)] pub pid: &'a [u8], - /// Raw ECC-P384 identity public-key coordinates (`x ‖ y`, 96 B). + /// Raw ECC-P384 identity public-key coordinates (`x ‖ y`, 96 B) in + /// natural big-endian (SEC1) order. #[tbor(buffer, len = 96)] pub pid_pub_key: &'a [u8], } diff --git a/fw/core/lib/src/ddi/mbor/establish_credential.rs b/fw/core/lib/src/ddi/mbor/establish_credential.rs index b1607b21e..0ba093574 100644 --- a/fw/core/lib/src/ddi/mbor/establish_credential.rs +++ b/fw/core/lib/src/ddi/mbor/establish_credential.rs @@ -30,6 +30,9 @@ use super::*; /// [`init_bk3`](super::init_bk3). const BK3_LEN: usize = 48; +/// Length of a raw P-384 identity public key (`x || y`, 48-byte coordinates). +const P384_PUB_RAW_LEN: usize = 2 * 48; + // ── Labels and metadata ────────────────────────────────────────────── /// KBKDF label for the BK3 session key derivation. @@ -370,9 +373,16 @@ async fn verify_pota_signature( signer_pub_key_raw: &DmaBuf, signature_raw: &DmaBuf, ) -> HsmResult<()> { - // `part_id_pub_key` returns the raw `x ‖ y` form (96 B); prepend - // the SEC1 `0x04` uncompressed-point tag in a fresh DMA buffer. - let id_pub_key_len = crate::part_state::part_id_pub_key(pal, io)?.len(); + // `part_id_pub_key` returns the raw `x ‖ y` form (96 B) in natural + // big-endian; prepend the SEC1 `0x04` uncompressed-point tag to build the + // `0x04 ‖ x_be ‖ y_be` form the host (and X.509 leaf) signs over. + let id_pub_key_len = { + let pk = crate::part_state::part_id_pub_key(pal, io)?; + if pk.len() != P384_PUB_RAW_LEN { + return Err(HsmError::EccInvalidKeyLength); + } + pk.len() + }; let id_uncompressed = pal.dma_alloc(io, id_pub_key_len + 1)?; id_uncompressed[0] = 0x04; { diff --git a/fw/core/lib/src/ddi/tbor/part_info.rs b/fw/core/lib/src/ddi/tbor/part_info.rs index e414a997d..4a6fd265e 100644 --- a/fw/core/lib/src/ddi/tbor/part_info.rs +++ b/fw/core/lib/src/ddi/tbor/part_info.rs @@ -22,6 +22,7 @@ use azihsm_fw_ddi_tbor_types::DeviceKind; use azihsm_fw_ddi_tbor_types::PartStateId; use azihsm_fw_ddi_tbor_types::TborPartInfoResp; use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmError; use azihsm_fw_hsm_pal_traits::HsmIo; use azihsm_fw_hsm_pal_traits::HsmPal; use azihsm_fw_hsm_pal_traits::HsmResult; @@ -33,6 +34,9 @@ use crate::part_state; /// not yet FIPS-approved. const FIPS_APPROVED: bool = false; +/// Length of a raw P-384 identity public key (`x || y`, 48-byte coordinates). +const P384_PUB_RAW_LEN: usize = 2 * 48; + /// Handle a TBOR `PartInfo` request. /// /// The caller (`dispatch`) has already structurally validated the @@ -50,7 +54,23 @@ pub(crate) fn handle<'p, P: HsmPal>( let owner_svn = part_state::part_owner_svn(pal); let mfgr_svn = part_state::part_mfgr_svn(pal); let pid = part_state::part_id(pal, io)?; - let pid_pub_key = part_state::part_id_pub_key(pal, io)?; + + // The identity public key is returned in natural big-endian SEC1 order by + // `part_id_pub_key` (normalized at the PAL), which is exactly what the host + // wants — matching the get-cert-chain leaf and the `EstablishCredential` + // POTA check. Copy it through directly. + let key_len = { + let pk = part_state::part_id_pub_key(pal, io)?; + if pk.len() != P384_PUB_RAW_LEN { + return Err(HsmError::EccInvalidKeyLength); + } + pk.len() + }; + let pid_pub_key = pal.dma_alloc(io, key_len)?; + { + let pk = part_state::part_id_pub_key(pal, io)?; + pid_pub_key[..key_len].copy_from_slice(&pk[..key_len]); + } let resp = pal.dma_alloc_var(io, |buf| { let frame = TborPartInfoResp::encode(buf, 0, FIPS_APPROVED)? diff --git a/fw/plat/uno/fw/Cargo.toml b/fw/plat/uno/fw/Cargo.toml index 1780bdb69..5f1fd7992 100644 --- a/fw/plat/uno/fw/Cargo.toml +++ b/fw/plat/uno/fw/Cargo.toml @@ -5,6 +5,7 @@ members = [ "app", "crates/bulk_copy", + "crates/cert_blob", "crates/error", "crates/fault", "crates/key_vault", @@ -43,6 +44,7 @@ azihsm_fw_bulk_copy = { path = "crates/bulk_copy" } azihsm_fw_single_cell = { path = "crates/single_cell" } azihsm_fw_static_init = { path = "crates/static_init" } azihsm_fw_static_ref = { path = "crates/static_ref" } +azihsm_fw_uno_cert_blob = { path = "crates/cert_blob" } azihsm_fw_uno_reg_cortex_m = { path = "reg/cortex-m" } azihsm_fw_uno_drivers_aes = { path = "drivers/aes" } @@ -77,6 +79,7 @@ azihsm_fw_core_crypto_gcm_buf = { path = "../../../core/crypto/gcm-buf" } azihsm_fw_core_crypto_hpke = { path = "../../../core/crypto/hpke" } azihsm_fw_core_crypto_key_derive = { path = "../../../core/crypto/key-derive" } azihsm_fw_core_crypto_key_masking = { path = "../../../core/crypto/key-masking" } +azihsm_fw_core_crypto_x509_builder = { path = "../../../core/crypto/x509-builder" } azihsm_fw_core_crypto_x509_chain = { path = "../../../core/crypto/x509-chain" } azihsm_fw_ddi_mbor = { path = "../../../core/ddi/mbor/codec" } azihsm_fw_ddi_mbor_api = { path = "../../../core/ddi/mbor/api" } @@ -105,6 +108,7 @@ pastey = "0.2.2" portable-atomic = "1" proc-macro2 = "1" quote = "1" +static_assertions = "1.1.0" syn = { features = ["full"], version = "2" } tock-registers = { git = "https://github.com/tock/tock.git", rev = "release-2.2" } zerocopy = "0.8.48" diff --git a/fw/plat/uno/fw/crates/cert_blob/Cargo.toml b/fw/plat/uno/fw/crates/cert_blob/Cargo.toml new file mode 100644 index 000000000..f8d22bfbe --- /dev/null +++ b/fw/plat/uno/fw/crates/cert_blob/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +edition = "2021" +name = "azihsm_fw_uno_cert_blob" +version = "0.1.0" + +[lib] +doctest = false + +[dependencies] +static_assertions = { workspace = true } +zerocopy = { features = ["derive"], workspace = true } + +[lints] +workspace = true diff --git a/fw/plat/uno/fw/crates/cert_blob/src/lib.rs b/fw/plat/uno/fw/crates/cert_blob/src/lib.rs new file mode 100644 index 000000000..40e8d0e2a --- /dev/null +++ b/fw/plat/uno/fw/crates/cert_blob/src/lib.rs @@ -0,0 +1,429 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Certificate Blob (CBLOB) container for the boot-time device-id certificate chain. +//! +//! The SP/HSP packs the device-id cert chain into a CBLOB in CP1/HSM DTCM at boot, so the +//! CP serves GetCertChainInfo (1108) / GetCertificate (1109) from local DTCM with no IPC. +//! Layout: header + descriptor table + packed DER + trailing SHA-256 digest (see +//! `cp/hsm/docs/CertChainStoreOnHsmDtcm.md`). `CertBlobHdr` / `CertBlobDesc` are a byte-for-byte +//! ABI mirror of the SP-side structs in `sp/src/dc_scm/soc_shared.h`; all fields little-endian. + +#![cfg_attr(not(test), no_std)] + +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; + +/// CBLOB magic: ASCII `"CERT"`. +pub const CBLOB_MAGIC: [u8; 4] = *b"CERT"; + +/// Major version (format identifier only; not gated on for field presence). +pub const CBLOB_VER_MAJOR: u8 = 1; + +/// Minor version (format identifier only). +pub const CBLOB_VER_MINOR: u8 = 1; + +/// `digest_alg` value for SHA-256. +pub const CBLOB_DIGEST_SHA256: u8 = 0x01; + +/// Trailing integrity digest length (SHA-256). +pub const CBLOB_DIGEST_LEN: usize = 32; + +/// Header size: base 16 B + 32 B `dev_id_chain_hash`. +pub const CBLOB_HDR_SIZE: usize = 48; + +/// Byte offset of `dev_id_chain_hash` within the header. +pub const CBLOB_DEV_ID_CHAIN_HASH_OFF: usize = 16; + +/// Descriptor size (one per certificate). +pub const CBLOB_DESC_SIZE: usize = 8; + +/// Required alignment for the blob base, offsets, and `total_size`. +pub const CBLOB_ALIGN: usize = 4; + +/// Hard upper bound on `total_size` (incl. digest). +pub const CBLOB_MAX_SIZE: usize = 16_384; + +/// Length of the dev-id chain thumbprint (SHA-256). +pub const DEV_ID_CHAIN_HASH_LEN: usize = 32; + +/// Maximum number of device-id chain certificates (root + intermediate + device-id). +pub const MAX_DEVID_CERTS: usize = 5; + +/// Maximum DER length of a single device-id chain certificate +/// (= HSP `MAX_FIPS_DEVID_CERT_LENGTH`). +pub const MAX_DEVID_CERT_LEN: usize = 2048; + +/// Size of the reserved CP1/HSM DTCM region that holds the CBLOB (16 KB). +pub const DEV_ID_CERT_BLOB_REGION_SIZE: usize = 0x4000; + +/// CBLOB header (48 bytes). Byte-for-byte ABI mirror of the SP-side `struct cert_blob_hdr`. +#[repr(C, align(4))] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct CertBlobHdr { + /// 0x00 magic `{ 'C','E','R','T' }`. + pub magic: [u8; 4], + /// 0x04 major version (format identifier). + pub ver_major: u8, + /// 0x05 minor version (format identifier). + pub ver_minor: u8, + /// 0x06 number of certificates in the chain. + pub cert_count: u16, + /// 0x08 whole blob incl. digest; `% 4 == 0`, `<= 16384`. + pub total_size: u16, + /// 0x0A digest algorithm (`0x01` = SHA-256). + pub digest_alg: u8, + /// 0x0B header size; the descriptor table starts here (= 48). + pub hdr_size: u8, + /// 0x0C reserved, must be 0. + pub reserved: u32, + /// 0x10 dev-id chain hash returned (combined with alias/PID) in GetCertChainInfo. + pub dev_id_chain_hash: [u8; DEV_ID_CHAIN_HASH_LEN], +} + +/// CBLOB descriptor (8 bytes, one per certificate). Byte-for-byte ABI mirror of the +/// SP-side `struct cert_blob_desc`. +#[repr(C, align(4))] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct CertBlobDesc { + /// +0x00 4-byte-aligned offset of the cert DER from the blob base. + pub offset: u32, + /// +0x04 true DER length (excludes padding). + pub length: u32, +} + +// Lock the ABI: these fail to compile if the layout drifts from the SP-side C structs. +static_assertions::const_assert_eq!(core::mem::size_of::(), CBLOB_HDR_SIZE); +static_assertions::const_assert_eq!(core::mem::align_of::(), CBLOB_ALIGN); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, ver_major), 0x04); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, ver_minor), 0x05); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, cert_count), 0x06); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, total_size), 0x08); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, digest_alg), 0x0A); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, hdr_size), 0x0B); +static_assertions::const_assert_eq!(core::mem::offset_of!(CertBlobHdr, reserved), 0x0C); +static_assertions::const_assert_eq!( + core::mem::offset_of!(CertBlobHdr, dev_id_chain_hash), + CBLOB_DEV_ID_CHAIN_HASH_OFF +); +static_assertions::const_assert_eq!(core::mem::size_of::(), CBLOB_DESC_SIZE); +static_assertions::const_assert_eq!(core::mem::align_of::(), CBLOB_ALIGN); +// The 16 KB region holds the worst-case chain (header + table + max certs + digest). +static_assertions::const_assert!( + CBLOB_HDR_SIZE + + MAX_DEVID_CERTS * CBLOB_DESC_SIZE + + MAX_DEVID_CERTS * MAX_DEVID_CERT_LEN + + CBLOB_DIGEST_LEN + <= DEV_ID_CERT_BLOB_REGION_SIZE +); + +/// Bounds-checked read-only view over a CBLOB in a DTCM byte region. Construct with +/// [`CertBlob::parse`]; all accessors are panic-free even on a corrupt region. +pub struct CertBlob<'a> { + /// The blob bytes, trimmed to `total_size`. + bytes: &'a [u8], + /// Validated certificate count. + cert_count: usize, + /// Validated header size (descriptor table base). + hdr_size: usize, + /// Dev-id chain hash (copied from the header in `parse`). + dev_id_chain_hash: [u8; DEV_ID_CHAIN_HASH_LEN], +} + +impl<'a> CertBlob<'a> { + /// Validate and overlay a CBLOB at the start of `region`, or `None` on any structural + /// check failure (bad magic/version/sizes, or an out-of-range/overlapping descriptor). + /// Does NOT verify the integrity digest -- the CP is a dumb reader; integrity is the host's job. + pub fn parse(region: &'a [u8]) -> Option { + let hdr = CertBlobHdr::read_from_bytes(region.get(0..CBLOB_HDR_SIZE)?).ok()?; + + if hdr.magic != CBLOB_MAGIC { + return None; + } + if hdr.ver_major != CBLOB_VER_MAJOR { + return None; + } + if hdr.digest_alg != CBLOB_DIGEST_SHA256 { + return None; + } + if hdr.reserved != 0 { + return None; + } + + let cert_count = hdr.cert_count as usize; + let total_size = hdr.total_size as usize; + let hdr_size = hdr.hdr_size as usize; + + // hdr_size >= 48 (lenient): the table is located via hdr_size, so a grown header still parses. + if hdr_size < CBLOB_HDR_SIZE || !hdr_size.is_multiple_of(CBLOB_ALIGN) { + return None; + } + if !total_size.is_multiple_of(CBLOB_ALIGN) + || total_size > CBLOB_MAX_SIZE + || total_size > region.len() + { + return None; + } + if cert_count == 0 || cert_count > MAX_DEVID_CERTS { + return None; + } + + // Cert data occupies [table_end .. data_end); the digest is the last 32 B. + let data_end = total_size.checked_sub(CBLOB_DIGEST_LEN)?; + let table_end = hdr_size.checked_add(cert_count.checked_mul(CBLOB_DESC_SIZE)?)?; + if table_end > data_end { + return None; + } + + // Each descriptor: aligned, in range, capped, and ascending + non-overlapping. + let mut prev_end = table_end; + for i in 0..cert_count { + let base = hdr_size + i * CBLOB_DESC_SIZE; + let desc = + CertBlobDesc::read_from_bytes(region.get(base..base + CBLOB_DESC_SIZE)?).ok()?; + let offset = desc.offset as usize; + let length = desc.length as usize; + if !offset.is_multiple_of(CBLOB_ALIGN) || offset < prev_end || offset > data_end { + return None; + } + if length == 0 || length > MAX_DEVID_CERT_LEN || length > data_end - offset { + return None; + } + prev_end = offset.checked_add(length)?; + } + + Some(CertBlob { + bytes: region.get(..total_size)?, + cert_count, + hdr_size, + dev_id_chain_hash: hdr.dev_id_chain_hash, + }) + } + + /// Number of certificates in the chain. + pub fn cert_count(&self) -> usize { + self.cert_count + } + + /// The 32-byte dev-id chain hash (header extension). + pub fn dev_id_chain_hash(&self) -> &[u8; DEV_ID_CHAIN_HASH_LEN] { + &self.dev_id_chain_hash + } + + /// Read descriptor `i` (ranges already validated in `parse`). + fn desc(&self, i: usize) -> Option { + if i >= self.cert_count { + return None; + } + let base = self.hdr_size + i * CBLOB_DESC_SIZE; + CertBlobDesc::read_from_bytes(self.bytes.get(base..base + CBLOB_DESC_SIZE)?).ok() + } + + /// DER length of certificate `i`, or `None` if `i >= cert_count`. + pub fn cert_len(&self, i: usize) -> Option { + Some(self.desc(i)?.length as u16) + } + + /// Borrow the packed DER bytes of certificate `i`, or `None` if `i >= cert_count`. + pub fn cert_der(&self, i: usize) -> Option<&'a [u8]> { + let desc = self.desc(i)?; + let offset = desc.offset as usize; + let length = desc.length as usize; + self.bytes.get(offset..offset.checked_add(length)?) + } + + /// Build a CBLOB into `out` from `certs` (+ `dev_id_chain_hash`). Cert id is the index. + /// Returns the `total_size` written (digest trailer left zeroed), or `None` if it + /// doesn't fit. No firmware caller, so the linker dead-strips it; used by tests/tools. + pub fn build( + out: &mut [u8], + certs: &[&[u8]], + dev_id_chain_hash: &[u8; DEV_ID_CHAIN_HASH_LEN], + ) -> Option { + let cert_count = certs.len(); + if cert_count == 0 || cert_count > MAX_DEVID_CERTS { + return None; + } + let table_end = CBLOB_HDR_SIZE + cert_count * CBLOB_DESC_SIZE; + + // Lay out the certs (4-byte aligned) to compute total_size up front. + let mut offsets = [0usize; MAX_DEVID_CERTS]; + let mut cursor = table_end; + for (i, c) in certs.iter().enumerate() { + offsets[i] = cursor; + cursor = cursor.checked_add(c.len())?; + cursor = (cursor + CBLOB_ALIGN - 1) & !(CBLOB_ALIGN - 1); + } + let digest_off = cursor; // already 4-byte aligned + let total_size = digest_off.checked_add(CBLOB_DIGEST_LEN)?; + if total_size > CBLOB_MAX_SIZE || total_size > out.len() { + return None; + } + + for b in out[..total_size].iter_mut() { + *b = 0; + } + + let hdr = CertBlobHdr { + magic: CBLOB_MAGIC, + ver_major: CBLOB_VER_MAJOR, + ver_minor: CBLOB_VER_MINOR, + cert_count: cert_count as u16, + total_size: total_size as u16, + digest_alg: CBLOB_DIGEST_SHA256, + hdr_size: CBLOB_HDR_SIZE as u8, + reserved: 0, + dev_id_chain_hash: *dev_id_chain_hash, + }; + out.get_mut(0..CBLOB_HDR_SIZE)? + .copy_from_slice(hdr.as_bytes()); + + for (i, c) in certs.iter().enumerate() { + let desc = CertBlobDesc { + offset: offsets[i] as u32, + length: c.len() as u32, + }; + let d = CBLOB_HDR_SIZE + i * CBLOB_DESC_SIZE; + out.get_mut(d..d + CBLOB_DESC_SIZE)? + .copy_from_slice(desc.as_bytes()); + out.get_mut(offsets[i]..offsets[i] + c.len())? + .copy_from_slice(c); + } + // Digest trailer [total_size-32 .. total_size) left zeroed. + Some(total_size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_thumb() -> [u8; 32] { + let mut t = [0u8; 32]; + for (i, b) in t.iter_mut().enumerate() { + *b = i as u8; + } + t + } + + #[test] + fn build_parse_roundtrip() { + let c0 = [0x30u8, 0x03, 0x01, 0x02, 0x03]; // 5 bytes + let c1 = [0x30u8, 0x01, 0xAA]; // 3 bytes + let c2 = [0x30u8; 100]; + let thumb = sample_thumb(); + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + + let total = CertBlob::build(&mut buf, &[&c0, &c1, &c2], &thumb).unwrap(); + assert_eq!(total % CBLOB_ALIGN, 0); + + let blob = CertBlob::parse(&buf).unwrap(); + assert_eq!(blob.cert_count(), 3); + assert_eq!(blob.dev_id_chain_hash(), &thumb); + assert_eq!(blob.cert_len(0), Some(5)); + assert_eq!(blob.cert_len(1), Some(3)); + assert_eq!(blob.cert_len(2), Some(100)); + assert_eq!(blob.cert_len(3), None); + assert_eq!(blob.cert_der(0), Some(&c0[..])); + assert_eq!(blob.cert_der(1), Some(&c1[..])); + assert_eq!(blob.cert_der(2), Some(&c2[..])); + assert_eq!(blob.cert_der(3), None); + } + + #[test] + fn certs_are_4byte_aligned() { + let c0 = [0x30u8; 5]; + let c1 = [0x30u8; 7]; + let thumb = sample_thumb(); + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0, &c1], &thumb).unwrap(); + let blob = CertBlob::parse(&buf).unwrap(); + // Read each descriptor's offset and require 4-byte alignment. + for i in 0..blob.cert_count() { + let base = CBLOB_HDR_SIZE + i * CBLOB_DESC_SIZE; + let desc = CertBlobDesc::read_from_bytes(&buf[base..base + CBLOB_DESC_SIZE]).unwrap(); + assert_eq!(desc.offset as usize % CBLOB_ALIGN, 0); + } + } + + #[test] + fn rejects_bad_magic() { + let c0 = [0x30u8; 8]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0], &sample_thumb()).unwrap(); + buf[0] = b'X'; + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_bad_major() { + let c0 = [0x30u8; 8]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0], &sample_thumb()).unwrap(); + buf[0x04] = 0x02; // ver_major = 2 + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_short_region() { + let buf = [0u8; CBLOB_HDR_SIZE - 1]; + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_zero_and_unpopulated() { + let buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_descriptor_out_of_range() { + let c0 = [0x30u8; 8]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0], &sample_thumb()).unwrap(); + buf[CBLOB_HDR_SIZE + 4..CBLOB_HDR_SIZE + 8].copy_from_slice(&0xFFFFu32.to_le_bytes()); + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_nonzero_reserved() { + let c0 = [0x30u8; 8]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0], &sample_thumb()).unwrap(); + buf[0x0C] = 0x01; // reserved != 0 + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_cert_too_long() { + let c0 = [0x30u8; 8]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0], &sample_thumb()).unwrap(); + let bad = (MAX_DEVID_CERT_LEN as u32) + 1; + buf[CBLOB_HDR_SIZE + 4..CBLOB_HDR_SIZE + 8].copy_from_slice(&bad.to_le_bytes()); + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn rejects_overlapping_descriptors() { + let c0 = [0x30u8; 64]; + let c1 = [0x30u8; 64]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + CertBlob::build(&mut buf, &[&c0, &c1], &sample_thumb()).unwrap(); + let d0 = + CertBlobDesc::read_from_bytes(&buf[CBLOB_HDR_SIZE..CBLOB_HDR_SIZE + CBLOB_DESC_SIZE]) + .unwrap(); + buf[CBLOB_HDR_SIZE + CBLOB_DESC_SIZE..CBLOB_HDR_SIZE + CBLOB_DESC_SIZE + 4] + .copy_from_slice(&d0.offset.to_le_bytes()); + assert!(CertBlob::parse(&buf).is_none()); + } + + #[test] + fn build_rejects_too_many() { + let c = [0x30u8; 4]; + let many: [&[u8]; MAX_DEVID_CERTS + 1] = [&c; MAX_DEVID_CERTS + 1]; + let mut buf = [0u8; DEV_ID_CERT_BLOB_REGION_SIZE]; + assert!(CertBlob::build(&mut buf, &many, &sample_thumb()).is_none()); + } +} diff --git a/fw/plat/uno/fw/drivers/upka/Cargo.toml b/fw/plat/uno/fw/drivers/upka/Cargo.toml index bb35fba61..e9302faa6 100644 --- a/fw/plat/uno/fw/drivers/upka/Cargo.toml +++ b/fw/plat/uno/fw/drivers/upka/Cargo.toml @@ -19,6 +19,7 @@ azihsm_fw_uno_error = { workspace = true } azihsm_fw_uno_pac = { workspace = true } azihsm_fw_uno_reg_soc = { workspace = true } bitfield-struct = { workspace = true } +cortex-m = { workspace = true } embassy-sync = { workspace = true } tock-registers = { workspace = true } zerocopy = { features = ["derive"], workspace = true } diff --git a/fw/plat/uno/fw/drivers/upka/src/executor.rs b/fw/plat/uno/fw/drivers/upka/src/executor.rs index 692b814c0..57d6a7197 100644 --- a/fw/plat/uno/fw/drivers/upka/src/executor.rs +++ b/fw/plat/uno/fw/drivers/upka/src/executor.rs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use core::sync::atomic::compiler_fence; -use core::sync::atomic::Ordering; - use azihsm_fw_static_ref::StaticRef; use azihsm_fw_uno_reg_soc::io_gsram::regs::IoGsramRegs; use azihsm_fw_uno_reg_soc::io_gsram::IO_GSRAM_BASE; @@ -13,6 +10,7 @@ use azihsm_fw_uno_reg_soc::upka::UpkaEngine; use azihsm_fw_uno_reg_soc::upka::ENGINE_STRIDE; use azihsm_fw_uno_reg_soc::upka::UPKA_BASE; use azihsm_fw_uno_reg_soc::upka::UPKA_ENGINE_STATUS; +use cortex_m::asm::dmb; use tock_registers::interfaces::Readable; use tock_registers::interfaces::Writeable; @@ -53,7 +51,12 @@ impl EngineExecutor { arg3: u32, ) { Self::write_descriptor(engine_id, opcode, result, arg1, arg2, arg3); - compiler_fence(Ordering::SeqCst); + // Hardware barrier (not just a compiler fence): ensure the descriptor + // stores are globally visible before the command-register write triggers + // the engine's AXI read of the operand addresses. A `compiler_fence` + // leaves the stores in the Cortex-M7 store buffer, so under concurrent + // load the engine can read stale operand addresses and raise BUS_ERROR. + dmb(); Self::submit_cmd(engine_id); } diff --git a/fw/plat/uno/fw/memory.x b/fw/plat/uno/fw/memory.x index 90b360298..da68a8317 100644 --- a/fw/plat/uno/fw/memory.x +++ b/fw/plat/uno/fw/memory.x @@ -5,10 +5,11 @@ MEMORY { FLASH : ORIGIN = 0x00000000, LENGTH = 512K /* DTCM — CPU-only, holds stack and .bss. - Upper 69 KB reserved (see rdl/soc/dtcm_map.rdl): - 0x2002_EC00 DTCM_IO_BUF[33] (66 KB) - 0x2003_F400 CRASHDUMP_BASE (1024 B) - 0x2003_F800 CORE_RUN_STATUS (4 B) + Upper region reserved (see rdl/soc/dtcm_map.rdl): + 0x2002_EC00 DTCM_IO_BUF[33] (49.5 KB, 1.5 KB each) + 0x2003_B400 DEV_ID_CERT_BLOB (16 KB) + 0x2003_F400 CRASHDUMP_BASE (1024 B) + 0x2003_F800 CORE_RUN_STATUS (4 B) LENGTH capped at 187K. */ RAM : ORIGIN = 0x20000000, LENGTH = 187K } diff --git a/fw/plat/uno/fw/pal/Cargo.toml b/fw/plat/uno/fw/pal/Cargo.toml index 0f8f6faf5..5dbf7b7f9 100644 --- a/fw/plat/uno/fw/pal/Cargo.toml +++ b/fw/plat/uno/fw/pal/Cargo.toml @@ -12,10 +12,12 @@ test = false [dependencies] azihsm_fw_core_crypto_key_derive = { workspace = true } +azihsm_fw_core_crypto_x509_builder = { workspace = true } azihsm_fw_hsm_pal_traits = { workspace = true } azihsm_fw_single_cell = { workspace = true } azihsm_fw_static_init = { workspace = true } azihsm_fw_static_ref = { workspace = true } +azihsm_fw_uno_cert_blob = { workspace = true } azihsm_fw_uno_drivers_aes = { workspace = true } azihsm_fw_uno_drivers_bks_store = { workspace = true } azihsm_fw_uno_drivers_boot_status = { workspace = true } diff --git a/fw/plat/uno/fw/pal/src/cert.rs b/fw/plat/uno/fw/pal/src/cert.rs index f5288f685..5440bfaff 100644 --- a/fw/plat/uno/fw/pal/src/cert.rs +++ b/fw/plat/uno/fw/pal/src/cert.rs @@ -1,62 +1,579 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! [`HsmCertStore`] stub for the Uno PAL. +//! [`HsmCertStore`] for the Uno PAL — partition-id (PID) leaf generation. //! -//! Certificate storage is not yet implemented on this platform. Every -//! method returns [`HsmError::UnsupportedCmd`] so the HSM core can -//! report the unsupported state to callers without panicking. +//! The served slot-0 chain is the device-id chain (from the DTCM CBLOB) + +//! the CP alias certificate (from GSRAM) + a partition-id (PID) leaf that +//! is generated on demand here. The PID leaf's subject public key is the +//! partition identity key; it is signed by the CP alias key using the +//! firmware's *deterministic* (RFC 6979) ECDSA-P384 so the leaf is +//! byte-stable across regenerations (cacheable). The GSRAM alias key is a +//! SEC1 `ECPrivateKey` DER blob; [`AliasSigner`] parses out the scalar and +//! converts it to the PKA little-endian operand order. #![allow(clippy::unused_async)] +use azihsm_fw_core_crypto_x509_builder::cert_builder::LeafCertParams; +use azihsm_fw_core_crypto_x509_builder::cert_builder::TbsSigner; +use azihsm_fw_core_crypto_x509_builder::cert_builder::build_leaf_cert_with_signer; use azihsm_fw_hsm_pal_traits::CertChainInfo; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmAlloc; use azihsm_fw_hsm_pal_traits::HsmCertStore; use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmHash; +use azihsm_fw_hsm_pal_traits::HsmHashAlgo; use azihsm_fw_hsm_pal_traits::HsmIo; use azihsm_fw_hsm_pal_traits::HsmPartId; use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; +use azihsm_fw_uno_drivers_part_store::PartStore; +use azihsm_fw_uno_drivers_upka::UpkaEccCurve; use crate::UnoHsmPal; +use crate::dev_id_cblob; +use crate::gsram_alias; + +/// P-384 field-element / operand width (bytes). +const P384_FIELD: usize = 48; +/// Raw P-384 public point `X ‖ Y` width (bytes). +const P384_PUB_XY_LEN: usize = 96; +/// Uncompressed P-384 public key `0x04 ‖ X ‖ Y` width (bytes). +const P384_UNCOMPRESSED_LEN: usize = 97; +/// Subject/Authority Key Identifier width (SHA-1, bytes). +const SKI_LEN: usize = 20; +/// Issuer commonName value width in the AZIHSM leaf profile (bytes). +const ISSUER_CN_LEN: usize = 64; +/// Subject commonName value width in the AZIHSM leaf profile (bytes). +const SUBJECT_CN_LEN: usize = 32; +/// Partition identifier width (bytes); its uppercase hex is the subject CN. +const PART_ID_LEN: usize = 16; +/// DER serial-number width (bytes). +const SERIAL_LEN: usize = 20; + +/// SHA-256 digest width (bytes) — cert thumbprint / chain hash component. +const SHA256_LEN: usize = 32; + +/// Worst-case DER size of the PID leaf certificate (single-CN 422-byte TBS plus +/// the ECDSA-P384 signature/algorithm/wrapper overhead). Used to size the +/// scratch buffer for thumbprint hashing; the builder rejects an undersized +/// buffer, so this is a safe upper bound with headroom. +const MAX_PID_CERT_DER: usize = 640; + +/// Certificates appended after the device-id chain: the GSRAM alias cert and +/// the generated PID leaf. +const TRAILING_CERTS: u8 = 2; + +/// Leaf validity window (dev values). TODO(cert-chain hw): align with policy. +const NOT_BEFORE: &[u8; 15] = b"20250101000000Z"; +const NOT_AFTER: &[u8; 15] = b"20350101000000Z"; + +/// [`TbsSigner`] that signs a leaf TBS with the CP alias key via the firmware's +/// deterministic (RFC 6979) ECDSA-P384, yielding a byte-stable signature. +struct AliasSigner<'a> { + pal: &'a UnoHsmPal, + /// CP alias private key bytes from GSRAM. + alias_key: &'a [u8], +} + +impl TbsSigner for AliasSigner<'_> { + async fn sign_digest( + &self, + io: &impl HsmIo, + digest: &DmaBuf, + sig: &mut DmaBuf, + ) -> HsmResult<()> { + // The GSRAM alias key is a SEC1 `ECPrivateKey` DER blob + // (`ECC_DER_P384_PRIVATE_NO_PUB_LENGTH`), not a raw scalar: the private + // key is the OCTET STRING value, stored big-endian. + let priv_be = der::get_ec_private_key(self.alias_key).ok_or(HsmError::InvalidArg)?; + // Enforce exact lengths: the TbsSigner contract is a 48-byte SHA-384 + // digest and a 96-byte `r || s` output. An oversized `sig` would make + // `sig.split_at_mut(P384_FIELD)` hand an oversized `s` to the signer. + if priv_be.len() != P384_FIELD || digest.len() != P384_FIELD || sig.len() != 2 * P384_FIELD + { + return Err(HsmError::InvalidArg); + } + + self.pal + .alloc_scoped_async(io, async |scope| { + // `ecc_sign_deterministic` operands are PKA little-endian. The + // DER private key is big-endian, so reverse it into `d`. + let d = scope.dma_alloc(P384_FIELD)?; + for i in 0..P384_FIELD { + d[i] = priv_be[P384_FIELD - 1 - i]; + } + + // `build_signed_from_template` hashes the TBS as a natural + // big-endian SHA-384 digest. `ecc_sign_deterministic` consumes + // the PKA little-endian message hash — the FULL byte reversal of + // the natural digest (not the per-word `big_endian = false` + // swap). This mirrors the est-cred POTA verify and matches the + // validated deterministic-sign KAT. + let e = scope.dma_alloc(P384_FIELD)?; + for i in 0..P384_FIELD { + e[i] = digest[P384_FIELD - 1 - i]; + } + let (r, s) = sig.split_at_mut(P384_FIELD); + let res = self + .pal + .ecc_sign_deterministic(io, UpkaEccCurve::P384, e, d, r, s) + .await; + d.zeroize(); + res + }) + .await + } +} + +/// Deterministic 20-byte DER serial derived from the 16-byte partition id +/// (positive INTEGER: leading byte non-zero with bit 7 clear). +fn pid_serial(part_id: &[u8]) -> [u8; SERIAL_LEN] { + let mut serial = [0u8; SERIAL_LEN]; + // Leading byte is a fixed non-zero, top-bit-clear tag; the remaining 16 + // bytes carry the partition id, and the low 3 bytes stay zero. + serial[0] = 0x40; + let n = part_id.len().min(PART_ID_LEN); + serial[1..1 + n].copy_from_slice(&part_id[..n]); + serial +} + +/// Uppercase-hex of a 16-byte partition id into a 32-byte subject CN value. +fn subject_cn_hex(part_id: &[u8]) -> [u8; SUBJECT_CN_LEN] { + let nib = |n: u8| if n < 10 { b'0' + n } else { b'A' + n - 10 }; + let mut out = [0u8; SUBJECT_CN_LEN]; + for (i, &b) in part_id.iter().take(PART_ID_LEN).enumerate() { + out[2 * i] = nib(b >> 4); + out[2 * i + 1] = nib(b & 0x0f); + } + out +} + +impl UnoHsmPal { + /// Generate the partition-id (PID) leaf certificate for `part_id`. + /// + /// Query/copy: `out = None` returns the worst-case DER size; `Some(buf)` + /// writes the cert and returns its length. The subject public key is the + /// partition identity key; the leaf is signed by the CP alias key via + /// deterministic ECDSA-P384. + async fn generate_pid_cert( + &self, + io: &impl HsmIo, + part_id: HsmPartId, + out: Option<&mut [u8]>, + ) -> HsmResult { + // Partition identity public key (X ‖ Y) — must be provisioned. + let part = PartStore::partition(part_id)?; + if part.id_key_id().is_none() { + return Err(HsmError::InvalidArg); + } + let id_pub = part.id_pub_key(); + // P-384-only path: require an exact `X ‖ Y` length. A longer key would + // silently ignore trailing bytes and could yield an invalid cert. + if id_pub.len() != P384_PUB_XY_LEN { + return Err(HsmError::InternalError); + } + // `id_pub` is the identity point in natural big-endian (`X_be ‖ Y_be`), + // normalized at the PAL; build the SEC1 uncompressed point directly. + let mut uncompressed = [0u8; P384_UNCOMPRESSED_LEN]; + uncompressed[0] = 0x04; + uncompressed[1..].copy_from_slice(&id_pub[..P384_PUB_XY_LEN]); + + // Subject Key Identifier = SHA-1(uncompressed public key). + let ski = self.sha1_20(io, &uncompressed).await?; + // Partition identifier → subject CN (uppercase hex) and serial number. + let id = part.id(); + if id.len() < PART_ID_LEN { + return Err(HsmError::InternalError); + } + let subject_cn = subject_cn_hex(&id[..PART_ID_LEN]); + let serial = pid_serial(&id[..PART_ID_LEN]); + + // Issuer DN and Authority Key Identifier come from the alias cert so the + // PID leaf chains to the CP alias certificate (subject CN → issuer CN, + // alias SKI → leaf AKI). + let alias_cert = gsram_alias::alias_cert(); + // Missing alias material (bring-up / provisioning failure) is a caller- + // facing `InvalidArg`, not an internal error — match the HsmCertStore docs + // and fail before attempting to parse an empty buffer. + if alias_cert.is_empty() { + return Err(HsmError::InvalidArg); + } + let mut issuer_cn = [0u8; ISSUER_CN_LEN]; + let cn = der::get_subject_cn(alias_cert).ok_or(HsmError::InternalError)?; + if cn.len() != ISSUER_CN_LEN { + return Err(HsmError::InternalError); + } + issuer_cn.copy_from_slice(cn); + + let mut authority_key_id = [0u8; SKI_LEN]; + // The leaf's AKI must be the alias cert's SKI; fail rather than emit a + // leaf with an all-zero AKI if the alias cert lacks the extension. + let aki = der::get_subject_key_identifier(alias_cert).ok_or(HsmError::InternalError)?; + if aki.len() != SKI_LEN { + return Err(HsmError::InternalError); + } + authority_key_id.copy_from_slice(aki); + + // Alias signing key from GSRAM. + let alias_key = gsram_alias::alias_key(); + if alias_key.is_empty() { + return Err(HsmError::InvalidArg); + } + + let params = LeafCertParams { + public_key: &uncompressed, + serial_number: &serial, + not_before: NOT_BEFORE, + not_after: NOT_AFTER, + issuer_cn: &issuer_cn, + subject_cn: &subject_cn, + subject_key_id: &ski, + authority_key_id: &authority_key_id, + }; + + let signer = AliasSigner { + pal: self, + alias_key, + }; + self.alloc_scoped_async(io, async |scope| { + build_leaf_cert_with_signer(self, io, scope, ¶ms, &signer, out).await + }) + .await + } + + /// SHA-1 over `data`, returning the 20-byte digest. + async fn sha1_20(&self, io: &impl HsmIo, data: &[u8]) -> HsmResult<[u8; SKI_LEN]> { + self.alloc_scoped_async(io, async |scope| { + let inp = scope.dma_alloc(data.len())?; + inp.copy_from_slice(data); + let outd = scope.dma_alloc(SKI_LEN)?; + // `big_endian = true`: the SKI is the standard NIST byte-order SHA-1 + // of the key (RFC 5280). `big_endian = false` would per-word swap the + // digest and encode an incorrect Subject Key Identifier. + self.hash(io, HsmHashAlgo::Sha1, inp, outd, true).await?; + let mut out = [0u8; SKI_LEN]; + out.copy_from_slice(&outd[..SKI_LEN]); + Ok(out) + }) + .await + } + + /// SHA-256 over `data`, returning the 32-byte digest. + async fn sha256_32(&self, io: &impl HsmIo, data: &[u8]) -> HsmResult<[u8; SHA256_LEN]> { + self.alloc_scoped_async(io, async |scope| { + let inp = scope.dma_alloc(data.len())?; + inp.copy_from_slice(data); + let outd = scope.dma_alloc(SHA256_LEN)?; + // `big_endian = true`: emit the standard (natural byte order) SHA-256 + // digest. The host and SP compute cert thumbprints over natural + // SHA-256; `false` here would per-word byte-swap the digest (the PKA + // operand order) and the chain thumbprint would never match. + self.hash(io, HsmHashAlgo::Sha256, inp, outd, true).await?; + let mut out = [0u8; SHA256_LEN]; + out.copy_from_slice(&outd[..SHA256_LEN]); + Ok(out) + }) + .await + } +} + +/// Minimal DER helpers for extracting fields from the alias certificate. +/// +/// Ported from the mcr-hsm reference (`cp/hsm/hsm/src/der/mod.rs`): no-alloc, +/// no_std byte scanners over a DER blob. +mod der { + /// Parse a DER length at `offset`; returns `(length, header_bytes)` where + /// `header_bytes` counts the length octets (excludes the tag). + fn parse_der_length(data: &[u8], offset: usize) -> Option<(usize, usize)> { + let byte0 = *data.get(offset)?; + if byte0 & 0x80 == 0 { + // short form + Some((byte0 as usize, 1)) + } else { + // long form: lower 7 bits = number of following length bytes. + let n = (byte0 & 0x7F) as usize; + // Reject oversized encodings: `n` length octets must fit in `usize` + // so the shift-accumulate below cannot overflow/wrap on a corrupt or + // adversarial DER blob. + if n == 0 || n > core::mem::size_of::() { + return None; + } + let value_start = offset.checked_add(1)?; + let value_end = value_start.checked_add(n)?; + if value_end > data.len() { + return None; + } + let mut val = 0usize; + for i in 0..n { + val = (val << 8) | (data[value_start + i] as usize); + } + Some((val, n + 1)) + } + } + + /// Return the Subject Common Name (OID 2.5.4.3) value bytes. When two CN + /// RDNs are present (issuer then subject), the second (subject) is chosen. + pub(super) fn get_subject_cn(der: &[u8]) -> Option<&[u8]> { + // OID TLV for 2.5.4.3 = 06 03 55 04 03 + const CN_OID: [u8; 5] = [0x06, 0x03, 0x55, 0x04, 0x03]; + + let mut first_pos = None; + let mut second_pos = None; + + for (i, w) in der.windows(CN_OID.len()).enumerate() { + if w == CN_OID { + if first_pos.is_none() { + first_pos = Some(i); + } else { + second_pos = Some(i); + break; + } + } + } + + let pos = second_pos.or(first_pos)?; + + // Skip past the OID TLV header + value. + let (oid_len, oid_len_bytes) = parse_der_length(der, pos + 1)?; + let idx = (pos + 1).checked_add(oid_len_bytes)?.checked_add(oid_len)?; + + // At idx: the string tag; its length follows at idx + 1. + let (val_len, val_len_bytes) = parse_der_length(der, idx.checked_add(1)?)?; + let start = idx.checked_add(1)?.checked_add(val_len_bytes)?; + let end = start.checked_add(val_len)?; + + der.get(start..end).filter(|bytes| !bytes.is_empty()) + } + + /// Return the Subject Key Identifier (OID 2.5.29.14) keyIdentifier bytes. + pub(super) fn get_subject_key_identifier(der: &[u8]) -> Option<&[u8]> { + // DER for OID 2.5.29.14 is: 06 03 55 1D 0E + const SKI_OID_TLV: &[u8; 5] = &[0x06, 0x03, 0x55, 0x1D, 0x0E]; + + // 1) locate the OID TLV + let pos = der + .windows(SKI_OID_TLV.len()) + .position(|w| w == SKI_OID_TLV)?; + + // 2) skip past the OID's length + value + let (oid_len, oid_len_bytes) = parse_der_length(der, pos + 1)?; + let mut idx = (pos + 1).checked_add(oid_len_bytes)?.checked_add(oid_len)?; + + // 3) skip OPTIONAL critical BOOLEAN (tag 0x01) + if der.get(idx) == Some(&0x01) { + let (bool_len, bool_len_bytes) = parse_der_length(der, idx.checked_add(1)?)?; + idx = idx + .checked_add(1)? + .checked_add(bool_len_bytes)? + .checked_add(bool_len)?; + } + + // 4) next must be the OCTET STRING (tag 0x04) wrapping the SKI + if der.get(idx) != Some(&0x04) { + return None; + } + let (ext_len, ext_len_bytes) = parse_der_length(der, idx.checked_add(1)?)?; + let ext_start = idx.checked_add(1)?.checked_add(ext_len_bytes)?; + let ext_end = ext_start.checked_add(ext_len)?; + let ext_bytes = der.get(ext_start..ext_end)?; + + // 5) ext_bytes is DER of SubjectKeyIdentifier ::= OCTET STRING, so + // ext_bytes[0] == 0x04, then length, then the keyIdentifier. + if ext_bytes.first() != Some(&0x04) { + return None; + } + let (ki_len, ki_len_bytes) = parse_der_length(ext_bytes, 1)?; + let ki_start = 1 + ki_len_bytes; + let ki_end = ki_start.checked_add(ki_len)?; + ext_bytes.get(ki_start..ki_end) + } + + /// Return the raw EC private-key scalar bytes (big-endian) from a SEC1 + /// `ECPrivateKey` DER blob: + /// + /// ```text + /// ECPrivateKey ::= SEQUENCE { + /// version INTEGER, -- 02 01 01 + /// privateKey OCTET STRING, -- 04 30 <48-byte big-endian scalar> + /// parameters [0] ... OPTIONAL, + /// publicKey [1] ... OPTIONAL + /// } + /// ``` + /// + /// The HSP provisions the CP alias key into GSRAM in this form (64 bytes, + /// `ECC_DER_P384_PRIVATE_NO_PUB_LENGTH`), so the raw scalar is the OCTET + /// STRING value — not the first bytes of the blob. + pub(super) fn get_ec_private_key(der: &[u8]) -> Option<&[u8]> { + // Outer SEQUENCE. + if *der.first()? != 0x30 { + return None; + } + let (_seq_len, seq_hdr) = parse_der_length(der, 1)?; + let mut idx = 1 + seq_hdr; + + // version INTEGER — skip tag + length + value. + if *der.get(idx)? != 0x02 { + return None; + } + let (ver_len, ver_hdr) = parse_der_length(der, idx.checked_add(1)?)?; + idx = idx + .checked_add(1)? + .checked_add(ver_hdr)? + .checked_add(ver_len)?; + + // privateKey OCTET STRING — its value is the raw scalar (big-endian). + if *der.get(idx)? != 0x04 { + return None; + } + let (key_len, key_hdr) = parse_der_length(der, idx.checked_add(1)?)?; + let start = idx.checked_add(1)?.checked_add(key_hdr)?; + der.get(start..start.checked_add(key_len)?) + } +} impl HsmCertStore for UnoHsmPal { - /// Not implemented. + /// Certificate-chain metadata for `(part_id, slot_id)`. + /// + /// Returns the served chain length (`device-id chain + alias + PID leaf`) + /// and the chain thumbprint + /// `SHA-256(dev_id_chain_hash ‖ SHA-256(alias_cert) ‖ SHA-256(pid_leaf))`, + /// matching the reference firmware. `dev_id_chain_hash` and the device-id + /// chain length come from the boot-time DTCM CBLOB. The PID leaf is + /// regenerated deterministically to hash it, so the thumbprint is stable and + /// matches the bytes [`get_cert`] serves. /// /// # Parameters - /// * `_io` — operation-scoped I/O context (ignored). - /// * `_part_id` — partition whose chain is queried (ignored). - /// * `_slot_id` — chain slot identifier (ignored). + /// * `io` — operation-scoped I/O context. + /// * `part_id` — partition whose chain is queried. + /// * `slot_id` — chain slot; only slot 0 is supported. /// /// # Returns - /// * Always `Err(HsmError::UnsupportedCmd)`. + /// * `Ok(CertChainInfo)` — chain length + thumbprint. + /// * `Err(HsmError::InvalidArg)` — non-zero slot, missing/invalid device-id + /// CBLOB, missing GSRAM alias certificate, or unprovisioned partition. async fn get_cert_chain_info( &self, - _io: &impl HsmIo, - _part_id: HsmPartId, - _slot_id: u8, + io: &impl HsmIo, + part_id: HsmPartId, + slot_id: u8, ) -> HsmResult { - Err(HsmError::UnsupportedCmd) + if slot_id != 0 { + return Err(HsmError::InvalidArg); + } + + let cblob = dev_id_cblob::dev_id_cert_blob().ok_or(HsmError::InvalidArg)?; + let alias = gsram_alias::alias_cert(); + if alias.is_empty() { + return Err(HsmError::InvalidArg); + } + + // count = device-id chain certs + alias + PID leaf. + let count = u8::try_from(cblob.cert_count()) + .ok() + .and_then(|k| k.checked_add(TRAILING_CERTS)) + .ok_or(HsmError::InternalError)?; + + self.alloc_scoped_async(io, async |scope| { + let leaf = scope.dma_alloc(MAX_PID_CERT_DER)?; + let leaf_len = self.generate_pid_cert(io, part_id, Some(leaf)).await?; + + let alias_hash = self.sha256_32(io, alias).await?; + let leaf_hash = self.sha256_32(io, &leaf[..leaf_len]).await?; + + // Thumbprint = SHA-256(dev_id_chain_hash ‖ H(alias) ‖ H(leaf)). + // `dev_id_chain_hash` is taken straight from the HSP-provisioned + // CBLOB header (SHA-256 over the device-id chain, alias excluded), + // exactly as the reference firmware does — the CP is a dumb reader + // and does not recompute it. + let mut combined = [0u8; 3 * SHA256_LEN]; + combined[..SHA256_LEN].copy_from_slice(cblob.dev_id_chain_hash()); + combined[SHA256_LEN..2 * SHA256_LEN].copy_from_slice(&alias_hash); + combined[2 * SHA256_LEN..].copy_from_slice(&leaf_hash); + let thumbprint = self.sha256_32(io, &combined).await?; + + Ok(CertChainInfo { count, thumbprint }) + }) + .await } - /// Not implemented. + /// Read one certificate from the chain (standard query/copy pattern). + /// + /// Chain order (leaf last): `idx 0..k-1` are the device-id chain certs from + /// the boot-time DTCM CBLOB (`k = CertBlob::cert_count`), `idx k` is the + /// GSRAM alias cert, and `idx k+1` is the generated PID leaf. `cert = None` + /// returns the size; `Some(buf)` copies the DER into `buf`. /// /// # Parameters - /// * `_io` — operation-scoped I/O context (ignored). - /// * `_part_id` — partition whose chain is queried (ignored). - /// * `_slot_id` — chain slot identifier (ignored). - /// * `_idx` — certificate index within the chain (ignored). - /// * `_cert` — destination buffer; `None` would normally request the - /// required size (ignored). + /// * `io` — operation-scoped I/O context. + /// * `part_id` — partition whose chain is read. + /// * `slot_id` — chain slot; only slot 0 is supported. + /// * `idx` — zero-based certificate index (`< count`). + /// * `cert` — `None` to query size, `Some(buf)` to copy. /// /// # Returns - /// * Always `Err(HsmError::UnsupportedCmd)`. + /// * `Ok(size)` — DER length that was (or would be) written. + /// * `Err(HsmError::InvalidArg)` — bad slot/index, small buffer, missing + /// CBLOB/alias cert, or unprovisioned partition. async fn get_cert( &self, - _io: &impl HsmIo, - _part_id: HsmPartId, - _slot_id: u8, - _idx: u8, - _cert: Option<&mut [u8]>, + io: &impl HsmIo, + part_id: HsmPartId, + slot_id: u8, + idx: u8, + cert: Option<&mut [u8]>, ) -> HsmResult { - Err(HsmError::UnsupportedCmd) + if slot_id != 0 { + return Err(HsmError::InvalidArg); + } + + let cblob = dev_id_cblob::dev_id_cert_blob().ok_or(HsmError::InvalidArg)?; + let devid_count = cblob.cert_count(); + let idx = idx as usize; + let count = devid_count + TRAILING_CERTS as usize; + if idx >= count { + return Err(HsmError::InvalidArg); + } + + // Device-id chain certs occupy the first `devid_count` indices. + if idx < devid_count { + let src = cblob.cert_der(idx).ok_or(HsmError::InvalidArg)?; + return copy_or_size(src, cert); + } + + // Then the GSRAM alias certificate. + if idx == devid_count { + let alias = gsram_alias::alias_cert(); + if alias.is_empty() { + return Err(HsmError::InvalidArg); + } + return copy_or_size(alias, cert); + } + + // Last index is the generated PID leaf. Build it deterministically into + // a worst-case-sized scratch buffer (the builder requires that), then + // copy the exact DER out. Because generation is deterministic, the query + // and copy passes produce the same length — satisfying the DDI + // query/copy contract without caching. + self.alloc_scoped_async(io, async |scope| { + let scratch = scope.dma_alloc(MAX_PID_CERT_DER)?; + let n = self.generate_pid_cert(io, part_id, Some(scratch)).await?; + copy_or_size(&scratch[..n], cert) + }) + .await + } +} + +/// Query/copy helper: with `dst = None` return `src.len()`; with `Some(buf)` +/// copy `src` into `buf` (requiring `buf.len() >= src.len()`) and return the +/// length. +fn copy_or_size(src: &[u8], dst: Option<&mut [u8]>) -> HsmResult { + if let Some(buf) = dst { + if buf.len() < src.len() { + return Err(HsmError::InvalidArg); + } + buf[..src.len()].copy_from_slice(src); } + Ok(src.len()) } diff --git a/fw/plat/uno/fw/pal/src/dev_id_cblob.rs b/fw/plat/uno/fw/pal/src/dev_id_cblob.rs new file mode 100644 index 000000000..86cfc0dc3 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/dev_id_cblob.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Read-only accessor for the device-id certificate CBLOB the HSP packs into +//! CP1/HSM DTCM at boot. +//! +//! The SP/HSP writes the device-id certificate chain (root + intermediates + +//! device-id) plus the chain thumbprint into the fixed `DEV_ID_CERT_BLOB` DTCM +//! region before releasing the CP cores, so the HSM serves GetCertChainInfo / +//! GetCertificate locally with no IPC. The [`cert_blob`](azihsm_fw_uno_cert_blob) +//! crate parses and bounds-checks the container. + +use azihsm_fw_uno_cert_blob::CertBlob; +use azihsm_fw_uno_reg_soc::hsm_dtcm; + +/// Base address of the device-id CBLOB region in DTCM. +const REGION_BASE: usize = (hsm_dtcm::HSM_DTCM_BASE + hsm_dtcm::DEV_ID_CERT_BLOB_OFFSET) as usize; +/// Size of the device-id CBLOB region (bytes). +const REGION_SIZE: usize = hsm_dtcm::DEV_ID_CERT_BLOB_SIZE as usize; + +/// Parse the boot-time device-id certificate CBLOB from DTCM. +/// +/// Returns `None` if the HSP did not populate a structurally valid blob (e.g. +/// on bring-up firmware where the HSP handshake is not yet implemented). +pub(crate) fn dev_id_cert_blob() -> Option> { + // SAFETY: `DEV_ID_CERT_BLOB` is a fixed, 'static DTCM region the HSP + // populated before the CP cores were released; `CertBlob::parse` + // bounds-checks the entire container structure internally. + let region = unsafe { core::slice::from_raw_parts(REGION_BASE as *const u8, REGION_SIZE) }; + CertBlob::parse(region) +} diff --git a/fw/plat/uno/fw/pal/src/gsram_alias.rs b/fw/plat/uno/fw/pal/src/gsram_alias.rs new file mode 100644 index 000000000..c35c3b636 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/gsram_alias.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Read-only accessors for the CP alias key and certificate the HSP +//! provisions into GSRAM. +//! +//! The HSP writes the CP alias private key and its device-id-signed alias +//! certificate into fixed GSRAM locations (base `0x6100_0000`) before +//! releasing the CP cores; the layout mirrors the reference firmware's +//! `GsRamMemMap` and the SP-side `sp/src/dc_scm/mem_map/gsram_mem_map.h`. +//! The HSM reads them locally (no IPC) to serve the alias certificate in the +//! cert chain and to sign the partition-id (PID) certificate with the alias +//! key. +//! +//! Each blob is preceded by a `u32` length; the SP writes `-1` +//! (`0xFFFF_FFFF`) when the value did not fit, which we treat as "absent". + +/// `alias_key_length` (u32). +const ALIAS_KEY_LEN_ADDR: usize = 0x6100_0B30; +/// `alias_key` — CP alias private key. +const ALIAS_KEY_ADDR: usize = 0x6100_0B34; +/// Maximum alias-key length (`GSRAM_MEM_MAP_ALIAS_KEY_SIZE`). +const ALIAS_KEY_MAX: usize = 0x40; + +/// `alias_cert_length` (u32). +const ALIAS_CERT_LEN_ADDR: usize = 0x6100_0B74; +/// `alias_cert` — alias certificate (DER) signed by the device id. +const ALIAS_CERT_ADDR: usize = 0x6100_0B78; +/// Maximum alias-cert length (`GSRAM_MEM_MAP_ALIAS_CERT_SIZE`). +const ALIAS_CERT_MAX: usize = 0x488; + +/// Read a length prefix, mapping the SP "too long" sentinel (`u32::MAX`) and +/// any out-of-range length (larger than the field's storage capacity) to 0 +/// (absent). A length past capacity would otherwise yield truncated, non-empty +/// data, so it is treated as unprovisioned rather than silently clamped. +#[inline] +fn read_len(len_addr: usize, max: usize) -> usize { + // SAFETY: `len_addr` is a fixed, 4-byte-aligned GSRAM address the HSP + // populated before the CP cores were released; the read is within the + // reserved SP-shared block. + let len = unsafe { core::ptr::read_volatile(len_addr as *const u32) } as usize; + if len > max { 0 } else { len } +} + +/// The alias certificate DER bytes, or an empty slice if the SP did not +/// provision one. +pub(crate) fn alias_cert() -> &'static [u8] { + let len = read_len(ALIAS_CERT_LEN_ADDR, ALIAS_CERT_MAX); + // SAFETY: `ALIAS_CERT_ADDR .. +len` lies within the `alias_cert` field + // (capped at `ALIAS_CERT_MAX`); the region is 'static GSRAM. + unsafe { core::slice::from_raw_parts(ALIAS_CERT_ADDR as *const u8, len) } +} + +/// The alias private key bytes, or an empty slice if the SP did not +/// provision one. +pub(crate) fn alias_key() -> &'static [u8] { + let len = read_len(ALIAS_KEY_LEN_ADDR, ALIAS_KEY_MAX); + // SAFETY: `ALIAS_KEY_ADDR .. +len` lies within the `alias_key` field + // (capped at `ALIAS_KEY_MAX`); the region is 'static GSRAM. + unsafe { core::slice::from_raw_parts(ALIAS_KEY_ADDR as *const u8, len) } +} diff --git a/fw/plat/uno/fw/pal/src/io.rs b/fw/plat/uno/fw/pal/src/io.rs index 06ed6547d..ad41199ab 100644 --- a/fw/plat/uno/fw/pal/src/io.rs +++ b/fw/plat/uno/fw/pal/src/io.rs @@ -17,7 +17,7 @@ //! | `IO_SQ[index]` | 64B SQE | Submission queue entry (read-only) | //! | `IO_CQ[index]` | 16B CQE | Completion queue entry (write) | //! | `IO_META[index]` | 8B metadata | Controller/queue IDs from IIC recv | -//! | `DTCM_IO_BUF[index]` | 2KB fmem | Fast DTCM workspace buffer | +//! | `DTCM_IO_BUF[index]` | 1.5KB fmem | Fast DTCM workspace buffer | //! | `SRAM_IO_BUF[index]` | 8KB smem | Large SRAM workspace buffer | //! //! The IIC controller DMAs incoming SQE data directly into `IO_SQ[index]` diff --git a/fw/plat/uno/fw/pal/src/lib.rs b/fw/plat/uno/fw/pal/src/lib.rs index 310aa6f2c..e13462e9c 100644 --- a/fw/plat/uno/fw/pal/src/lib.rs +++ b/fw/plat/uno/fw/pal/src/lib.rs @@ -38,7 +38,9 @@ mod alloc; mod cert; mod crypto; +mod dev_id_cblob; mod gdma; +mod gsram_alias; mod io; mod ipc; mod lock; diff --git a/fw/plat/uno/fw/pal/src/part.rs b/fw/plat/uno/fw/pal/src/part.rs index 2f6ac895a..7ad150229 100644 --- a/fw/plat/uno/fw/pal/src/part.rs +++ b/fw/plat/uno/fw/pal/src/part.rs @@ -239,7 +239,15 @@ impl UnoHsmPal { // field (selected by `kind`). This borrow of the PartStore slot is // strictly synchronous — no `.await` is reached while it is held. match kind { - HsmVaultKeyKind::Ecc384Private => part.set_id_pub_key(pub_buf)?, + HsmVaultKeyKind::Ecc384Private => { + // The PKA emits the public key little-endian; store the identity + // key big-endian (natural SEC1/DER order) so every host-facing + // consumer (PartInfo, POTA verify, X.509 leaf, session HPKE) + // reads `part_id_pub_key` directly without per-handler swaps. + pub_buf[..ID_PUB_KEY_LEN / 2].reverse(); + pub_buf[ID_PUB_KEY_LEN / 2..].reverse(); + part.set_id_pub_key(pub_buf)? + } HsmVaultKeyKind::EstablishCred => part.set_ec_pub_key(pub_buf)?, HsmVaultKeyKind::SessionEncryption => part.set_se_pub_key(pub_buf)?, _ => return Err(HsmError::InternalError), diff --git a/fw/plat/uno/fw/reg/soc/src/hsm_dtcm.rs b/fw/plat/uno/fw/reg/soc/src/hsm_dtcm.rs index 30c601c05..9223c82cf 100644 --- a/fw/plat/uno/fw/reg/soc/src/hsm_dtcm.rs +++ b/fw/plat/uno/fw/reg/soc/src/hsm_dtcm.rs @@ -10,8 +10,12 @@ pub const HSM_DTCM_BASE: u32 = 0x20000000; pub const CORE_RUN_STATUS_OFFSET: u32 = 0x3F800; pub const DTCM_IO_BUF_OFFSET: u32 = 0x2EC00; pub const DTCM_IO_BUF_COUNT: u32 = 33; -pub const DTCM_IO_BUF_STRIDE: u32 = 0x800; -pub const DTCM_IO_BUF_SIZE: u32 = 0x800; +pub const DTCM_IO_BUF_STRIDE: u32 = 0x600; +pub const DTCM_IO_BUF_SIZE: u32 = 0x600; +pub const DEV_ID_CERT_BLOB_OFFSET: u32 = 0x3B400; +pub const DEV_ID_CERT_BLOB_COUNT: u32 = 1; +pub const DEV_ID_CERT_BLOB_STRIDE: u32 = 0x4000; +pub const DEV_ID_CERT_BLOB_SIZE: u32 = 0x4000; pub const CRASHDUMP_BASE_OFFSET: u32 = 0x3F400; pub const CRASHDUMP_BASE_COUNT: u32 = 1; pub const CRASHDUMP_BASE_STRIDE: u32 = 0x400; @@ -29,7 +33,9 @@ pub mod regs { tock_registers::register_structs! { pub HsmDtcmRegs { (0x0 => _reserved0), - (0x2ec00 => pub dtcm_io_buf: [u8; 67584]), + (0x2ec00 => pub dtcm_io_buf: [u8; 50688]), + (0x3b200 => _reserved1), + (0x3b400 => pub dev_id_cert_blob: [u8; 16384]), (0x3f400 => pub crashdump_base: [u8; 1024]), (0x3f800 => pub core_run_status: crate::RW), (0x3f804 => @END), diff --git a/fw/plat/uno/rdl/soc/dtcm_map.rdl b/fw/plat/uno/rdl/soc/dtcm_map.rdl index 0c9a2eae7..12157ff5b 100644 --- a/fw/plat/uno/rdl/soc/dtcm_map.rdl +++ b/fw/plat/uno/rdl/soc/dtcm_map.rdl @@ -7,7 +7,8 @@ // (256 KB total). The lower portion is for linker-placed stack/.bss; // the upper portion holds fixed firmware data structures: // -// 0x2002_EC00 DTCM_IO_BUF[33] (66 KB, per-IO NonDma scratch) +// 0x2002_EC00 DTCM_IO_BUF[33] (49.5 KB, per-IO NonDma scratch, 1.5 KB each) +// 0x2003_B400 DEV_ID_CERT_BLOB (16 KB, HSP-pushed device-id cert CBLOB) // 0x2003_F400 CRASHDUMP_BASE (1024 B) // 0x2003_F800 CORE_RUN_STATUS (4 B) // @@ -20,12 +21,30 @@ // ══════════════════════════════════════════════════════════════ mem dtcm_io_buf_t { - desc = "2KB per-slot CPU-only scratch buffer. Indexed 1:1 with IO_SQ."; - mementries = 512; // 512 × 32 bits = 2048 B = 2 KB + desc = "1.5KB per-slot CPU-only scratch buffer. Indexed 1:1 with IO_SQ. + Shrunk from 2 KB to free the DTCM tail for DEV_ID_CERT_BLOB; + NonDma scratch has no callers today so 1.5 KB is ample."; + mementries = 384; // 384 × 32 bits = 1536 B = 1.5 KB memwidth = 32; }; +// ══════════════════════════════════════════════════════════════ +// Device-ID cert-chain CBLOB (HSP boot-push) +// ══════════════════════════════════════════════════════════════ + +mem dev_id_cert_blob_t { + desc = "Device-id certificate-chain CBLOB (16 KB). The HSP packs the + root/intermediate/device-id DER chain here at boot (fixed global + 0x6063_B400) before releasing the CP cores; the HSM serves + GetCertChainInfo/GetCertificate for the dev-id chain from local + DTCM with no IPC. Read-only from the HSM. See the cert_blob crate + for the container layout."; + mementries = 4096; // 4096 × 32 bits = 16384 B = 16 KB + memwidth = 32; +}; + + // ══════════════════════════════════════════════════════════════ // Crashdump + Core Status // ══════════════════════════════════════════════════════════════ @@ -58,8 +77,12 @@ addrmap hsm_dtcm { default hw = na; default sw = rw; - // Per-IO NonDma scratch (33 × 2 KB = 66 KB; slots 0..31 host, 32 admin) - dtcm_io_buf_t DTCM_IO_BUF[33] @ 0x2EC00 += 0x800; + // Per-IO NonDma scratch (33 × 1.5 KB = 49.5 KB; slots 0..31 host, 32 admin). + // Ends at 0x3B200, below the DEV_ID_CERT_BLOB region. + dtcm_io_buf_t DTCM_IO_BUF[33] @ 0x2EC00 += 0x600; + + // Device-id cert-chain CBLOB — HSP boot-push target (fixed global 0x6063_B400). + dev_id_cert_blob_t DEV_ID_CERT_BLOB @ 0x3B400; // Crashdump + heartbeat crashdump_base_t CRASHDUMP_BASE @ 0x3F400;