Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion fw/core/crypto/x509-builder/gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,22 @@ 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(
"Leaf certificate TBS template (auto-generated).",
&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(),
Expand Down
191 changes: 153 additions & 38 deletions fw/core/crypto/x509-builder/src/cert_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -253,24 +253,141 @@ pub async fn build_leaf_cert<'a>(
priv_key: &DmaBuf,
out: Option<&mut [u8]>,
) -> HsmResult<usize> {
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<usize> {
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<usize> {
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?;
Comment thread
radutta99 marked this conversation as resolved.

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<P: HsmCrypto, A: HsmScopedAlloc> 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);
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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, &params.key_usage.to_bytes());
Ok(())
}

Expand Down
Loading
Loading