Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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 @@ -325,10 +325,15 @@ 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,
}

impl core::fmt::Debug for HsmError {
Expand Down
1 change: 1 addition & 0 deletions fw/plat/uno/fw/drivers/aes/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 }
9 changes: 9 additions & 0 deletions fw/plat/uno/fw/drivers/aes/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@ impl<const DEPTH: usize> AesExclusive<'_, DEPTH> {

fn submit_cmd(slot: usize) {
let desc_addr = IO_GSRAM_BASE + AES_CMD_OFFSET + (slot as u32) * AES_CMD_STRIDE;
// Order the descriptor writes (Normal GSRAM memory, written by the caller)
// 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 reject it (CMD_ERROR)
// — observed as a cold-boot failure on the first AES decrypt, where the
// descriptor slot still holds uninitialized GSRAM. Matches the reference
// firmware's `dmb()` before the doorbell and the other uno drivers
// (iic/oic/gdma/ipc).
cortex_m::asm::dmb();
AES.command.set(desc_addr);
}

Expand Down
1 change: 1 addition & 0 deletions fw/plat/uno/fw/drivers/sha/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 }
10 changes: 8 additions & 2 deletions fw/plat/uno/fw/drivers/sha/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,12 @@ impl<const DEPTH: usize> ShaExclusive<'_, DEPTH> {

fn submit_cmd(slot: usize) {
let desc_addr = IO_GSRAM_BASE + SHA_CMD_OFFSET + (slot as u32) * SHA_CMD_STRIDE;
// Order the descriptor writes (Normal memory, written by `write_cmd`) 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 reject it (CMD_ERROR). Matches the
// reference firmware and the other uno drivers (iic/oic/gdma/ipc).
cortex_m::asm::dmb();
SHA.command.set(desc_addr);
}

Expand All @@ -611,8 +617,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 @@ -523,6 +523,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 @@ -555,6 +567,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
}
49 changes: 49 additions & 0 deletions fw/plat/uno/fw/pal/src/self_test/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Cryptographic Algorithm Self-Tests (CAST) for the Uno HSM core.
//!
//! Implements the FIPS pre-operational and (future) periodic self-tests for
//! the HSM-core cryptographic algorithms. Each test runs a fixed known-answer
//! vector through the PAL crypto path and compares the result against the
//! expected output.
//!
//! # Memory
//!
//! All tests run on a dedicated, reserved IO slot
//! ([`crate::alloc::SELF_TEST_IO_INDEX`]) obtained via
//! [`UnoHsmPal::self_test_io`]. KAT operands are bump-allocated from that
//! slot's `SRAM_IO_BUF`, so the self-tests never contend with host IO or
//! partition-provisioning crypto (which uses the separate admin slot).
Comment on lines +13 to +17
//!
//! # Status
//!
//! AES-256-CBC, HKDF, and KBKDF are implemented. Further tests (per-engine
//! ECDSA / ECDH / RSA, and the RNG/DRBG FW-mode KAT) are appended to
//! [`run_pre_op`] as they are added — each is a direct call, with per-PKA-engine
//! tests wrapped in a `for engine in 0..PKA_ENGINES` loop.

mod aes_cbc;
mod kdf;
mod pka;
mod vectors;
Comment thread
msft-nathwania marked this conversation as resolved.

use azihsm_fw_hsm_pal_traits::HsmResult;

use crate::UnoHsmIo;
use crate::UnoHsmPal;

/// Runs the full pre-operational self-test suite.
///
/// Returns `Err` on the first failing test, leaving it to the caller (the boot
/// gate) to withhold the transition to the running state on failure. Mirrors
/// the reference firmware's `preops_cast` — positive known-answer tests only.
pub(crate) async fn run_pre_op(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> {
aes_cbc::run_aes_cbc(pal, io).await?;
kdf::run_hkdf(pal, io).await?;
kdf::run_kbkdf(pal, io).await?;
for engine in 0..pka::PKA_ENGINES {
pka::run_rsa_mod_exp_on_engine(pal, io, engine).await?;
}
Ok(())
}
Loading