Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions fw/pal/traits/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,16 @@ pub enum HsmError {
/// Distinct from [`HsmError::InvalidArg`], which signals a
/// caller bug (unknown id, kind mismatch, etc.).
PartPropNotFound = 0x087000FF,

/// Returned by [`HsmSeedStore`](crate::HsmSeedStore)
/// (`mfgr_seed` / `owner_seed`) when no provisioned BKS seed row
/// carries the requested selector (SVN / owner id).
SeedNotFound = 0x08700100,

/// A cryptographic algorithm self-test (CAST) produced output that
/// did not match its known-answer vector.
SelfTestKatMismatch = 0x08700101,

// Firmware-internal diagnostic codes logged by the CPU fault and panic
// exception handlers (`azihsm_fw_uno_fault`). These are not DDI protocol
// statuses: they use the PAL diagnostic facility (`0x08F`) to stay clear of
Expand Down
4 changes: 2 additions & 2 deletions fw/plat/uno/fw/drivers/sha/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,8 @@ fn write_cmd(idx: usize, req: &ShaRequest<'_>, mode: u8) {
0
};
let ref_digest_addr = req.ref_digest.map_or(0, |d| d.as_ptr() as u32);
// Plain pointer writes — SHA_CMD is in DTCM, not MMIO.
// The COMMAND register write (submit_cmd) is the barrier.
// Plain pointer writes — SHA_CMD is in DTCM, not MMIO. A `dmb()` in
// `submit_cmd` orders these writes before the doorbell.
let entry_ptr = (&SHA_Q.sha_cmd[idx]) as *const ShaCmdEntry as *mut u32;
unsafe {
entry_ptr.add(0).write(cmd.into());
Expand Down
1 change: 1 addition & 0 deletions fw/plat/uno/fw/drivers/upka/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
13 changes: 9 additions & 4 deletions fw/plat/uno/fw/drivers/upka/src/executor.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,7 +50,15 @@ impl EngineExecutor {
arg3: u32,
) {
Self::write_descriptor(engine_id, opcode, result, arg1, arg2, arg3);
compiler_fence(Ordering::SeqCst);
// Order the descriptor writes (Normal GSRAM memory) before the doorbell
// write (Device memory). On Cortex-M7 a Device write does NOT order
// prior Normal-memory writes, so without this barrier the engine can
// fetch a partially-written descriptor and fault on the stale arg
// addresses (ERROR_BUS) — observed as a deterministic per-engine
// failure. A `compiler_fence` only constrains the compiler, not the
// hardware store buffer. Matches the reference firmware's `dmb()` before
// the doorbell and the other uno drivers (aes/sha/iic/oic/gdma/ipc).
cortex_m::asm::dmb();
Self::submit_cmd(engine_id);
}

Expand Down
22 changes: 15 additions & 7 deletions fw/plat/uno/fw/pal/src/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,23 @@ const HEAPS: usize = 2;

pub(crate) const IO_SLOTS: usize = SRAM_IO_BUF_COUNT as usize;

/// Dedicated admin/internal IO slot — the last of [`IO_SLOTS`].
/// Dedicated admin/internal IO slot.
///
/// IIC only hands out the host slots `0..ADMIN_IO_INDEX` (the first
/// [`IO_SLOTS`]` - 1` slots); the PAL reserves this final slot for
/// internal provisioning crypto (partition identity and enable-time
/// keygen), so its bump heaps never collide with a concurrent host IO.
/// It has full DMA (`SRAM_IO_BUF`) and NonDma (`DTCM_IO_BUF`) backing,
/// like any host slot.
pub(crate) const ADMIN_IO_INDEX: u16 = (IO_SLOTS - 1) as u16;
/// 32 slots); the PAL reserves this slot for internal provisioning
/// crypto (partition identity and enable-time keygen), so its bump
/// heaps never collide with a concurrent host IO. It has full DMA
/// (`SRAM_IO_BUF`) and NonDma (`DTCM_IO_BUF`) backing, like any host
/// slot.
pub(crate) const ADMIN_IO_INDEX: u16 = (IO_SLOTS - 2) as u16;

/// Dedicated self-test (CAST) IO slot — the last of [`IO_SLOTS`].
///
/// Reserved for the pre-operational and periodic cryptographic
/// algorithm self-tests, separate from [`ADMIN_IO_INDEX`] so the
/// periodic self-test never races partition provisioning over a
/// shared bump heap. Same DMA/NonDma backing as any host slot.
pub(crate) const SELF_TEST_IO_INDEX: u16 = (IO_SLOTS - 1) as u16;
Comment on lines +46 to +52
Comment on lines +46 to +52

// DTCM IO buffer region — per-IO NonDma scratch in upper DTCM.
// See dtcm_map.rdl: DTCM_IO_BUF[33] @ offset 0x2EC00 from DTCM base.
Expand Down
17 changes: 17 additions & 0 deletions fw/plat/uno/fw/pal/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ use tock_registers::interfaces::Writeable;

use crate::UnoHsmPal;
use crate::alloc::ADMIN_IO_INDEX;
use crate::alloc::SELF_TEST_IO_INDEX;
use crate::alloc::reset_io_alloc;

/// Typed overlay of the IO GSRAM region.
Expand Down Expand Up @@ -87,6 +88,22 @@ impl UnoHsmIo {
io
}

/// Constructs an IO handle over the dedicated self-test slot
/// ([`SELF_TEST_IO_INDEX`]).
///
/// Used by the cryptographic algorithm self-tests (CAST), which run
/// without a host IO. KAT operands are allocated from this slot's
/// `SRAM_IO_BUF` via the bump allocator. No partition context applies,
/// so `IO_META` is left untouched ([`pid`](HsmIo::pid) is unused by the
/// self-test path).
///
/// [`SELF_TEST_IO_INDEX`]: crate::alloc::SELF_TEST_IO_INDEX
pub(crate) fn self_test() -> Self {
Self {
index: SELF_TEST_IO_INDEX,
}
}

/// Returns a reference to the IO_META entry for this slot.
#[inline]
fn io_meta(&self) -> &IoMetaEntry {
Expand Down
1 change: 1 addition & 0 deletions fw/plat/uno/fw/pal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod lock;
mod pal;
mod part;
mod seed;
mod self_test;
mod session;
mod vault;

Expand Down
24 changes: 24 additions & 0 deletions fw/plat/uno/fw/pal/src/pal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,18 @@ impl UnoHsmPal {
WAKE_TABLE[irq as usize](self, irq);
}
}

// AES/SHA completion is detected by POLLING the engine STATUS, not by the
// `AES_DONE`/`SHA_DONE` edge interrupt. On this silicon the done IRQ does
// not reliably re-arm for back-to-back commands (the first op completes
// and raises the IRQ; the second completes in hardware but no second IRQ
// is delivered), which silently hangs any sequential AES/SHA sequence
// (pre-op self-tests, partition-provisioning HMAC-SHA384 KDF + AES-CBC
// key masking, …). The reference firmware polls BUSY for exactly this
// reason. `wake()` is a guarded no-op when no flag bit is set, so calling
// it every poll is cheap (one MMIO read) and idempotent with the IRQ path.
self.aes.wake();
self.sha.wake();
}

/// Handle an incoming IPC message.
Expand Down Expand Up @@ -560,6 +572,18 @@ impl UnoHsmPal {
if !self.try_ack_state_change(buf, IoProcessorState::Start, "WaitStart") {
return;
}
// Pre-operational self-test gate (FIPS): the in-scope algorithm
// KATs must pass before the HSM exposes any crypto. Crypto
// engines are construct-ready (AES/SHA/PKA need no boot-time
// init), so this runs before `on_boot_complete`. On failure we
// withhold the transition — the PAL never reaches `Running`,
// never publishes `BootStatus::Run`.
let io = crate::io::UnoHsmIo::self_test();
if let Err(_e) = crate::self_test::run_pre_op(self, &io).await {
error!("selftest", _e, "pre-operational self-test FAILED");
return;
}
info!("selftest", "pre-operational self-test PASSED");
self.on_boot_complete()
}
BootPhase::Running => {
Expand Down
78 changes: 78 additions & 0 deletions fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! AES-256-CBC cryptographic algorithm self-test (CAST).
//!
//! Runs a fixed known-answer vector through the HSM AES engine in **both**
//! directions — encrypt the plaintext and compare against the expected
//! ciphertext, then decrypt that ciphertext and compare back to the plaintext —
//! matching the reference firmware's `aes_cbc_self_test`. Operands are staged
//! into the self-test IO slot's DMA buffer via the bump allocator (see
//! [`crate::self_test`]).

use azihsm_fw_hsm_pal_traits::AesOp;
use azihsm_fw_hsm_pal_traits::HsmAes;
use azihsm_fw_hsm_pal_traits::HsmAlloc;
use azihsm_fw_hsm_pal_traits::HsmError;
use azihsm_fw_hsm_pal_traits::HsmResult;
use azihsm_fw_hsm_pal_traits::HsmScopedAlloc;
use azihsm_fw_uno_trace::tracing::error;

use super::vectors::AES_CBC_256_KAT;
use crate::UnoHsmIo;
use crate::UnoHsmPal;

/// Runs the AES-256-CBC known-answer test against the HSM AES engine.
///
/// Encrypts the KAT plaintext and verifies the ciphertext, then decrypts the
/// ciphertext and verifies the plaintext (round trip), matching the reference
/// firmware. Returns [`HsmError::SelfTestKatMismatch`] on any mismatch (or any
/// error surfaced by the AES engine / allocator).
pub(super) async fn run_aes_cbc(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> {
let v = &AES_CBC_256_KAT;

pal.alloc_scoped_async(io, async |scope| {
// Stage the shared key/IV into DMA-visible memory (the self-test slot).
// The IV buffer is not mutated by the CBC path (it copies into internal
// scratch), so the same `iv` is reused for both directions.
let key = scope.dma_alloc(v.key.len())?;
key.copy_from_slice(v.key);
let iv = scope.dma_alloc(v.iv.len())?;
iv.copy_from_slice(v.iv);

// ── Encrypt: plaintext → ciphertext, verify against the vector ──
let pt = scope.dma_alloc(v.plaintext.len())?;
pt.copy_from_slice(v.plaintext);
let ct_out = scope.dma_alloc_zeroed(v.ciphertext.len())?;
pal.aes_cbc_enc_dec(io, AesOp::Encrypt, &*key, &*pt, &*iv, &mut *ct_out, None)
.await?;
// KAT vectors are public, fixed test data — a plain slice comparison is
// correct; no constant-time compare is needed.
if &ct_out[..] != v.ciphertext {
error!(
"selftest",
HsmError::SelfTestKatMismatch,
"AES-CBC encrypt KAT mismatch"
);
return Err(HsmError::SelfTestKatMismatch);
}

// ── Decrypt: ciphertext → plaintext, verify against the vector ──
let ct = scope.dma_alloc(v.ciphertext.len())?;
ct.copy_from_slice(v.ciphertext);
let pt_out = scope.dma_alloc_zeroed(v.plaintext.len())?;
pal.aes_cbc_enc_dec(io, AesOp::Decrypt, &*key, &*ct, &*iv, &mut *pt_out, None)
.await?;
if &pt_out[..] != v.plaintext {
error!(
"selftest",
HsmError::SelfTestKatMismatch,
"AES-CBC decrypt KAT mismatch"
);
return Err(HsmError::SelfTestKatMismatch);
}

Ok::<(), HsmError>(())
})
.await
}
111 changes: 111 additions & 0 deletions fw/plat/uno/fw/pal/src/self_test/kdf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Key-derivation-function cryptographic algorithm self-tests (CAST).
//!
//! Runs fixed known-answer vectors through the HSM KDF path and compares the
//! derived key material against the expected output, matching the reference
//! firmware's `hkdf_self_test_256` (and, later, `kbkdf_self_test_512`).
//! Operands are staged into the self-test IO slot's DMA buffer via the bump
//! allocator (see [`crate::self_test`]).

use azihsm_fw_hsm_pal_traits::HsmAlloc;
use azihsm_fw_hsm_pal_traits::HsmError;
use azihsm_fw_hsm_pal_traits::HsmHashAlgo;
use azihsm_fw_hsm_pal_traits::HsmKdf;
use azihsm_fw_hsm_pal_traits::HsmResult;
use azihsm_fw_hsm_pal_traits::HsmScopedAlloc;
use azihsm_fw_uno_trace::tracing::error;

use super::vectors::HKDF_SHA256_KAT;
use super::vectors::KBKDF_SHA512_KAT;
use crate::UnoHsmIo;
use crate::UnoHsmPal;

/// Runs the HKDF (HMAC-SHA-256) known-answer test.
///
/// Performs HKDF-Extract (`salt` + `ikm` → PRK) then HKDF-Expand
/// (PRK + `info` → OKM) and compares the derived OKM against the expected
/// vector. Returns [`HsmError::SelfTestKatMismatch`] on a mismatch (or any
/// error surfaced by the KDF path / allocator).
pub(super) async fn run_hkdf(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> {
let v = &HKDF_SHA256_KAT;
let algo = HsmHashAlgo::Sha256;

pal.alloc_scoped_async(io, async |scope| {
// Stage the KAT operands into DMA-visible memory (the self-test slot).
let salt = scope.dma_alloc(v.salt.len())?;
salt.copy_from_slice(v.salt);
let ikm = scope.dma_alloc(v.ikm.len())?;
ikm.copy_from_slice(v.ikm);
let prk = scope.dma_alloc_zeroed(algo.digest_len())?;

// HKDF-Extract: PRK = HMAC-Hash(salt, IKM).
pal.hkdf_extract(io, algo, Some(&*salt), &*ikm, &mut *prk)
.await?;

// HKDF-Expand: OKM = Expand(PRK, info, L).
let info = scope.dma_alloc(v.info.len())?;
info.copy_from_slice(v.info);
let okm = scope.dma_alloc_zeroed(v.okm.len())?;
pal.hkdf_expand(io, algo, &*prk, Some(&*info), &mut *okm)
.await?;

// KAT vectors are public, fixed test data — a plain slice comparison is
// correct; no constant-time compare is needed.
if &okm[..] != v.okm {
error!(
"selftest",
HsmError::SelfTestKatMismatch,
"HKDF KAT mismatch"
);
return Err(HsmError::SelfTestKatMismatch);
}

Ok::<(), HsmError>(())
})
.await
}

/// Runs the KBKDF (SP 800-108 counter mode, HMAC-SHA-512) known-answer test.
///
/// Derives output via [`sp800_108_kdf`](HsmKdf::sp800_108_kdf) — the same
/// production path used by the `KbkdfCounterHmacDerive` DDI op — and compares
/// against the expected vector. Returns [`HsmError::SelfTestKatMismatch`] on a
/// mismatch (or any error surfaced by the KDF path / allocator).
pub(super) async fn run_kbkdf(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> {
let v = &KBKDF_SHA512_KAT;

pal.alloc_scoped_async(io, async |scope| {
// Stage the KAT operands into DMA-visible memory (the self-test slot).
let key = scope.dma_alloc(v.key.len())?;
key.copy_from_slice(v.key);
let label = scope.dma_alloc(v.label.len())?;
label.copy_from_slice(v.label);
let context = scope.dma_alloc(v.context.len())?;
context.copy_from_slice(v.context);
let okm = scope.dma_alloc_zeroed(v.okm.len())?;

pal.sp800_108_kdf(
io,
HsmHashAlgo::Sha512,
&*key,
Some(&*label),
Some(&*context),
&mut *okm,
)
.await?;

if &okm[..] != v.okm {
error!(
"selftest",
HsmError::SelfTestKatMismatch,
"KBKDF KAT mismatch"
);
return Err(HsmError::SelfTestKatMismatch);
}

Ok::<(), HsmError>(())
})
.await
}
Loading