Skip to content
Draft
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
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 }
25 changes: 20 additions & 5 deletions fw/plat/uno/fw/drivers/upka/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ impl<const DEPTH: usize, const ENGINES: usize> UpkaEngine<'_, DEPTH, ENGINES> {
/// # Parameters
///
/// - `key_type`: RSA key type (size + format selector).
/// - `key`: DMA-capable private key buffer.
/// - `key`: DMA-capable private key buffer. For standard keys this is a
/// `d ‖ n` blob; for CRT keys it is a contiguous `param1 ‖ param2` blob
/// (`param1` = `p ‖ q ‖ dp ‖ dq`, `param2` = `n ‖ n1q ‖ n2p`) at least
/// `5 * mod_size` bytes long.
/// - `input`: DMA-capable input block buffer.
/// - `output`: DMA-capable output block buffer.
///
Expand All @@ -235,16 +238,28 @@ impl<const DEPTH: usize, const ENGINES: usize> UpkaEngine<'_, DEPTH, ENGINES> {
output: &mut DmaBuf,
) -> HsmResult<()> {
let mod_size = rsa_mod_size(key_type);
Self::ensure_cmd_input(
!key.is_empty() && input.len() == mod_size && output.len() == mod_size,
)?;

// CRT private keys are a contiguous `param1 ‖ param2` blob: `param1`
// (`p ‖ q ‖ dp ‖ dq`, 2·mod_size) is read from the key/arg2 pointer and
// `param2` (`n ‖ n1q ‖ n2p`, 3·mod_size) is read from arg3. Standard
// (non-CRT) keys are a single `d ‖ n` blob read from arg2 with arg3 = 0.
let (key_ok, arg3) = if rsa_is_crt(key_type) {
let param2_off = rsa_crt_param1_len(key_type);
(
key.len() >= param2_off + 3 * mod_size,
key.as_ptr() as u32 + param2_off as u32,
)
} else {
(!key.is_empty(), 0)
};
Self::ensure_cmd_input(key_ok && input.len() == mod_size && output.len() == mod_size)?;

self.execute_cmd(
rsa_priv_opcode(key_type),
output.as_mut_ptr() as u32,
input.as_ptr() as u32,
key.as_ptr() as u32,
0,
arg3,
)
.await
}
Expand Down
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
43 changes: 38 additions & 5 deletions fw/plat/uno/fw/drivers/upka/src/opcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,23 @@ fn rsa_opcode_index(key_type: UpkaRsaKeyType) -> usize {
}
}

/// Whether the selected RSA key type uses the CRT (Chinese Remainder Theorem)
/// key format.
///
/// # Parameters
///
/// - `key_type`: RSA key type selector.
///
/// # Returns
///
/// - `true` for CRT key types, `false` for standard (non-CRT) key types.
pub(crate) fn rsa_is_crt(key_type: UpkaRsaKeyType) -> bool {
matches!(
key_type,
UpkaRsaKeyType::Rsa2048Crt | UpkaRsaKeyType::Rsa3072Crt | UpkaRsaKeyType::Rsa4096Crt
)
}

/// Return the RSA private-key exponentiation opcode for the selected key type.
///
/// # Parameters
Expand All @@ -238,17 +255,33 @@ fn rsa_opcode_index(key_type: UpkaRsaKeyType) -> usize {
/// - Hardware opcode for RSA private exponentiation (CRT or standard).
pub(crate) fn rsa_priv_opcode(key_type: UpkaRsaKeyType) -> u32 {
let index = rsa_opcode_index(key_type);
let is_crt = matches!(
key_type,
UpkaRsaKeyType::Rsa2048Crt | UpkaRsaKeyType::Rsa3072Crt | UpkaRsaKeyType::Rsa4096Crt
);
if is_crt {
if rsa_is_crt(key_type) {
RSA_OPCODES[index].2
} else {
RSA_OPCODES[index].0
}
}

/// Return the length in bytes of a CRT private key's `param1` block
/// (`p ‖ q ‖ dp ‖ dq`), which is four half-operands of `mod_size / 2` bytes,
/// i.e. `2 * mod_size`.
///
/// The hardware reads a CRT private key as two sub-blocks: `param1` from the
/// key (arg2) pointer and `param2` (`n ‖ n1q ‖ n2p`, `3 * mod_size` bytes) from
/// the arg3 pointer. This helper gives the offset of `param2` within a
/// contiguous `param1 ‖ param2` key blob.
///
/// # Parameters
///
/// - `key_type`: RSA key type selector.
///
/// # Returns
///
/// - `param1` length in bytes (`2 * mod_size`).
pub(crate) fn rsa_crt_param1_len(key_type: UpkaRsaKeyType) -> usize {
2 * rsa_mod_size(key_type)
}

/// Return the RSA public-key exponentiation opcode for the selected key type.
///
/// # Parameters
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;

// 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
}
Loading