From 236cf3e7fa179e771774c9aa5502c108d833d340 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Mon, 29 Jun 2026 17:26:13 -0700 Subject: [PATCH 01/19] Initial wiring for the preops selftest. Working AES-CBC encrypt/decrypt KAT --- fw/pal/traits/src/error.rs | 4 + fw/plat/uno/fw/drivers/sha/src/api.rs | 4 +- fw/plat/uno/fw/pal/src/alloc.rs | 22 ++++-- fw/plat/uno/fw/pal/src/io.rs | 17 +++++ fw/plat/uno/fw/pal/src/lib.rs | 1 + fw/plat/uno/fw/pal/src/pal.rs | 24 ++++++ fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs | 85 +++++++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/mod.rs | 83 ++++++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/vectors.rs | 58 ++++++++++++++ fw/plat/uno/fw/reg/soc/src/io_gsram.rs | 6 +- fw/plat/uno/rdl/soc/dtcm_map.rdl | 3 + fw/plat/uno/rdl/soc/gsram_map.rdl | 9 ++- 12 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs create mode 100644 fw/plat/uno/fw/pal/src/self_test/mod.rs create mode 100644 fw/plat/uno/fw/pal/src/self_test/vectors.rs diff --git a/fw/pal/traits/src/error.rs b/fw/pal/traits/src/error.rs index 2913fc736..14a4a26bd 100644 --- a/fw/pal/traits/src/error.rs +++ b/fw/pal/traits/src/error.rs @@ -430,6 +430,10 @@ pub enum HsmError { /// opted in via policy. SdPeerCloningNotAllowed = 0x0870010A, + /// A cryptographic algorithm self-test (CAST) produced output that + /// did not match its known-answer vector. + SelfTestKatMismatch = 0x0870010B, + // 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 diff --git a/fw/plat/uno/fw/drivers/sha/src/api.rs b/fw/plat/uno/fw/drivers/sha/src/api.rs index 0339293f6..ede366139 100644 --- a/fw/plat/uno/fw/drivers/sha/src/api.rs +++ b/fw/plat/uno/fw/drivers/sha/src/api.rs @@ -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()); diff --git a/fw/plat/uno/fw/pal/src/alloc.rs b/fw/plat/uno/fw/pal/src/alloc.rs index 2f43d200e..da7280961 100644 --- a/fw/plat/uno/fw/pal/src/alloc.rs +++ b/fw/plat/uno/fw/pal/src/alloc.rs @@ -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. diff --git a/fw/plat/uno/fw/pal/src/io.rs b/fw/plat/uno/fw/pal/src/io.rs index ad41199ab..967c85f11 100644 --- a/fw/plat/uno/fw/pal/src/io.rs +++ b/fw/plat/uno/fw/pal/src/io.rs @@ -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. @@ -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 { diff --git a/fw/plat/uno/fw/pal/src/lib.rs b/fw/plat/uno/fw/pal/src/lib.rs index e13462e9c..467d4010c 100644 --- a/fw/plat/uno/fw/pal/src/lib.rs +++ b/fw/plat/uno/fw/pal/src/lib.rs @@ -47,6 +47,7 @@ mod lock; mod pal; mod part; mod seed; +mod self_test; mod session; mod vault; diff --git a/fw/plat/uno/fw/pal/src/pal.rs b/fw/plat/uno/fw/pal/src/pal.rs index cd0ae716e..1a2a4efe9 100644 --- a/fw/plat/uno/fw/pal/src/pal.rs +++ b/fw/plat/uno/fw/pal/src/pal.rs @@ -531,6 +531,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. @@ -563,6 +575,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 => { diff --git a/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs b/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs new file mode 100644 index 000000000..de933fa25 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs @@ -0,0 +1,85 @@ +// 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 azihsm_fw_uno_trace::tracing::info; + +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())?; + info!("selftest", "AES-CBC encrypt 64B: submit"); + pal.aes_cbc_enc_dec(io, AesOp::Encrypt, &*key, &*pt, &*iv, &mut *ct_out, None) + .await?; + info!("selftest", "AES-CBC encrypt: engine ok"); + // 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); + } + info!("selftest", "AES-CBC encrypt: verified"); + + // ── 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())?; + info!("selftest", "AES-CBC decrypt 64B: submit"); + pal.aes_cbc_enc_dec(io, AesOp::Decrypt, &*key, &*ct, &*iv, &mut *pt_out, None) + .await?; + info!("selftest", "AES-CBC decrypt: engine ok"); + if &pt_out[..] != v.plaintext { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "AES-CBC decrypt KAT mismatch" + ); + return Err(HsmError::SelfTestKatMismatch); + } + info!("selftest", "AES-CBC decrypt: verified"); + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs new file mode 100644 index 000000000..fcd7f9ca5 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -0,0 +1,83 @@ +// 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). +//! +//! # Status +//! +//! AES-256-CBC is implemented as the initial test. Further tests (HKDF, KBKDF, +//! per-engine ECDSA / ECDH / RSA, and the RNG/DRBG FW-mode KAT) extend the +//! [`SelfTest`] enum and the [`run_one`] dispatch. + +mod aes_cbc; +mod vectors; + +use azihsm_fw_hsm_pal_traits::HsmResult; + +use crate::UnoHsmIo; +use crate::UnoHsmPal; + +/// Identifies a single cryptographic algorithm self-test. +// Variants beyond `AesCbc` (and their constructors) land with the boot gate +// and per-engine tests in a later milestone. +#[allow(dead_code)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum SelfTest { + /// AES-256-CBC known-answer test (HSM AES engine). + AesCbc, +} + +/// One scheduled self-test unit: a [`SelfTest`] optionally pinned to a specific +/// hardware engine instance. +/// +/// `engine` is `None` for tests that are not engine-scoped (such as +/// [`SelfTest::AesCbc`]); per-PKA-engine tests (added later) carry +/// `Some(index)` to validate each engine individually. +// +// `engine` is read once per-engine tests are wired (later milestone). +#[allow(dead_code)] +#[derive(Clone, Copy)] +pub struct SelfTestItem { + /// Which self-test to run. + pub test: SelfTest, + /// Target PKA engine instance, if the test is engine-scoped. + pub engine: Option, +} + +/// Runs a single self-test item to completion. +/// +/// The caller decides which items to run; `run_one` itself is +/// context-agnostic. +// +// Wired into the boot gate and the periodic self-test task in a later +// milestone; retained here as the dispatch entry point. +#[allow(dead_code)] +pub(crate) async fn run_one(pal: &UnoHsmPal, io: &UnoHsmIo, item: SelfTestItem) -> HsmResult<()> { + match item.test { + SelfTest::AesCbc => aes_cbc::run_aes_cbc(pal, io).await, + } +} + +/// 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?; + Ok(()) +} diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs new file mode 100644 index 000000000..4c74681db --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Known-answer test (KAT) vectors for the cryptographic algorithm +//! self-tests. +//! +//! Vector fields are `&'static [u8]` slices rather than owned arrays because +//! byte lengths vary across algorithms (and would otherwise force a distinct +//! struct per size). The `&[..]` array literals are promoted to `.rodata` in +//! const context (rvalue static promotion), so the data lives in flash with +//! no allocation and no runtime cost. + +/// AES-CBC known-answer vector. +/// +/// Holds the full plaintext/ciphertext pair so the self-test can exercise both +/// directions (encrypt then decrypt) against the same vector, matching the +/// reference firmware's `aes_cbc_self_test`. +pub(super) struct AesCbcKat { + /// Cipher key. + pub key: &'static [u8], + /// Initialization vector (one AES block). + pub iv: &'static [u8], + /// Plaintext (block multiple). + pub plaintext: &'static [u8], + /// Expected ciphertext for `plaintext` under (`key`, `iv`). + pub ciphertext: &'static [u8], +} + +/// AES-256-CBC known-answer test vector. +/// +/// Source: NIST SP 800-38A, F.2.5 (CBC-AES256.Encrypt) / F.2.6 +/// (CBC-AES256.Decrypt) — all four blocks. Byte-for-byte identical to the +/// reference firmware's `AES_SELF_TEST_{KEY,IV,PLAIN_TEXT,CIPHER_TEXT}`. +pub(super) const AES_CBC_256_KAT: AesCbcKat = AesCbcKat { + key: &[ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, + 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, + 0xdf, 0xf4, + ], + iv: &[ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, + ], + plaintext: &[ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, + 0x2a, 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, + 0x8e, 0x51, 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, + 0x0a, 0x52, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, + 0xe6, 0x6c, 0x37, 0x10, + ], + ciphertext: &[ + 0xf5, 0x8c, 0x4c, 0x04, 0xd6, 0xe5, 0xf1, 0xba, 0x77, 0x9e, 0xab, 0xfb, 0x5f, 0x7b, 0xfb, + 0xd6, 0x9c, 0xfc, 0x4e, 0x96, 0x7e, 0xdb, 0x80, 0x8d, 0x67, 0x9f, 0x77, 0x7b, 0xc6, 0x70, + 0x2c, 0x7d, 0x39, 0xf2, 0x33, 0x69, 0xa9, 0xd9, 0xba, 0xcf, 0xa5, 0x30, 0xe2, 0x63, 0x04, + 0x23, 0x14, 0x61, 0xb2, 0xeb, 0x05, 0xe2, 0xc3, 0x9b, 0xe9, 0xfc, 0xda, 0x6c, 0x19, 0x07, + 0x8c, 0x6a, 0x9d, 0x1b, + ], +}; diff --git a/fw/plat/uno/fw/reg/soc/src/io_gsram.rs b/fw/plat/uno/fw/reg/soc/src/io_gsram.rs index bafc706b5..f16e44b97 100644 --- a/fw/plat/uno/fw/reg/soc/src/io_gsram.rs +++ b/fw/plat/uno/fw/reg/soc/src/io_gsram.rs @@ -66,7 +66,7 @@ pub const HSM_SEED_TABLE_COUNT: u32 = 2; pub const HSM_SEED_TABLE_STRIDE: u32 = 0x30; pub const HSM_SEED_TABLE_SIZE: u32 = 0x30; pub const SRAM_IO_BUF_OFFSET: u32 = 0x20D60; -pub const SRAM_IO_BUF_COUNT: u32 = 33; +pub const SRAM_IO_BUF_COUNT: u32 = 34; pub const SRAM_IO_BUF_STRIDE: u32 = 0x4800; pub const SRAM_IO_BUF_SIZE: u32 = 0x4800; @@ -347,8 +347,8 @@ pub mod regs { (0x154a4 => pub sha_cmd: [super::ShaCmdEntry; 32]), (0x158a4 => _reserved6), (0x20c58 => pub io_meta: [super::IoMetaEntry; 33]), - (0x20d60 => pub sram_io_buf: [u8; 608256]), - (0xb5560 => @END), + (0x20d60 => pub sram_io_buf: [u8; 626688]), + (0xb9d60 => @END), } } } diff --git a/fw/plat/uno/rdl/soc/dtcm_map.rdl b/fw/plat/uno/rdl/soc/dtcm_map.rdl index 12157ff5b..912014b6f 100644 --- a/fw/plat/uno/rdl/soc/dtcm_map.rdl +++ b/fw/plat/uno/rdl/soc/dtcm_map.rdl @@ -78,6 +78,9 @@ addrmap hsm_dtcm { default sw = rw; // Per-IO NonDma scratch (33 × 1.5 KB = 49.5 KB; slots 0..31 host, 32 admin). + // Stays 33 (not 34): the self-test slot (SRAM index 33) uses DMA buffers + // only — all crypto drivers allocate from the SRAM (Dma) heap and never the + // DTCM (NonDma) heap — so it needs no DTCM scratch. // Ends at 0x3B200, below the DEV_ID_CERT_BLOB region. dtcm_io_buf_t DTCM_IO_BUF[33] @ 0x2EC00 += 0x600; diff --git a/fw/plat/uno/rdl/soc/gsram_map.rdl b/fw/plat/uno/rdl/soc/gsram_map.rdl index 96ef00c21..dce10c58e 100644 --- a/fw/plat/uno/rdl/soc/gsram_map.rdl +++ b/fw/plat/uno/rdl/soc/gsram_map.rdl @@ -569,9 +569,14 @@ addrmap io_gsram { upka_cmd_entry_t UPKA_ENGINE_CMD[16] @ 0x15364 += 0x14; sha_cmd_entry_t SHA_CMD[32] @ 0x154A4 += 0x20; - // ─── Heap (per-IO buffers) — 33 slots: 0..31 host (IIC), 32 admin ─── + // ─── Heap (per-IO buffers) — 34 SRAM slots: 0..31 host (IIC), 32 admin, + // 33 self-test (CAST). IO_META stays 33: only host+admin slots carry + // per-IO metadata (controller/queue id); the self-test slot never uses + // io_meta() (it does not ride the host IO path), so it needs no entry. + // Keeping IO_META at 33 also preserves SRAM_IO_BUF's 32-byte alignment + // at 0x20D60 (growing IO_META would shift SRAM_IO_BUF off alignment). ── io_meta_entry_t IO_META[33] @ 0x20C58 += 0x08; - sram_io_buf_t SRAM_IO_BUF[33] @ 0x20D60 += 0x4800; + sram_io_buf_t SRAM_IO_BUF[34] @ 0x20D60 += 0x4800; // ─── Partition persistent store (65 entries, 3072 B each) ─── part_store_t PART_STORE[65] @ 0xBB000 += 0xC00; From 70456d2940a8ab6f194fe57266c8b4750903b4fd Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 2 Jul 2026 10:05:38 -0700 Subject: [PATCH 02/19] Add HKDF KAT for preops gate --- fw/plat/uno/fw/pal/src/self_test/mod.rs | 2 + fw/plat/uno/fw/pal/src/self_test/vectors.rs | 52 +++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index fcd7f9ca5..02b12fae2 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -23,6 +23,7 @@ //! [`SelfTest`] enum and the [`run_one`] dispatch. mod aes_cbc; +mod kdf; mod vectors; use azihsm_fw_hsm_pal_traits::HsmResult; @@ -79,5 +80,6 @@ pub(crate) async fn run_one(pal: &UnoHsmPal, io: &UnoHsmIo, item: SelfTestItem) /// 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?; Ok(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 4c74681db..9685ac6ab 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -18,10 +18,13 @@ pub(super) struct AesCbcKat { /// Cipher key. pub key: &'static [u8], + /// Initialization vector (one AES block). pub iv: &'static [u8], + /// Plaintext (block multiple). pub plaintext: &'static [u8], + /// Expected ciphertext for `plaintext` under (`key`, `iv`). pub ciphertext: &'static [u8], } @@ -56,3 +59,52 @@ pub(super) const AES_CBC_256_KAT: AesCbcKat = AesCbcKat { 0x8c, 0x6a, 0x9d, 0x1b, ], }; + +/// HKDF (RFC 5869, HMAC-SHA-256) known-answer vector. +/// +/// Exercises HKDF-Extract (`salt` + `ikm` → PRK) followed by HKDF-Expand +/// (PRK + `info` → OKM), comparing the derived `okm` against the expected +/// vector. Byte-for-byte identical to the reference firmware's +/// `HKDF_SELF_TEST_256_{SALT,IKM,INFO,DKM}`. +pub(super) struct HkdfKat { + /// Salt (HKDF-Extract). + pub salt: &'static [u8], + + /// Input keying material (HKDF-Extract). + pub ikm: &'static [u8], + + /// Context/application info (HKDF-Expand): PartyU ‖ PartyV ‖ length. + pub info: &'static [u8], + + /// Expected derived keying material (output of HKDF-Expand). + pub okm: &'static [u8], +} + +/// HKDF-SHA-256 known-answer test vector (32-byte OKM). +pub(super) const HKDF_SHA256_KAT: HkdfKat = HkdfKat { + salt: &[ + 0xb1, 0xd6, 0x09, 0xd7, 0x5e, 0x4d, 0x0b, 0xe8, 0x1d, 0xca, 0xc2, 0xa8, 0x9a, 0xbc, 0xf0, + 0xa8, 0xac, 0xd7, 0x7d, 0xb8, 0xb7, 0x64, 0x17, 0x87, 0x21, 0xe6, 0xbe, 0x68, 0xcc, 0xf5, + 0x42, 0x51, 0x38, 0xa0, 0x56, 0xa2, 0xae, 0x37, 0x1a, 0x1e, 0x72, 0x7c, 0x15, 0xb5, 0x02, + 0x6d, 0xcf, 0x8e, 0xe7, 0x6d, 0x42, 0x08, 0x0e, 0x9c, 0x98, 0x08, 0x78, 0xb7, 0x0e, 0x78, + 0x6c, 0x7d, 0x64, 0x32, + ], + ikm: &[ + 0x7e, 0x4a, 0x88, 0x71, 0x5d, 0xe4, 0xd8, 0x78, 0x8f, 0x8f, 0x06, 0x9a, 0xb1, 0x05, 0x8a, + 0x42, 0xa1, 0xba, 0xc4, 0xb1, 0x9e, 0x7a, 0x4c, 0xe8, 0xcf, 0x7e, 0xa0, 0x4c, 0xc6, 0xbd, + 0x64, 0xf7, + ], + info: &[ + // PartyU ID (16 bytes) + 0x23, 0x6b, 0x2b, 0x47, 0x5c, 0xb1, 0xed, 0x72, 0x08, 0x5f, 0x11, 0xc0, 0x53, 0xbf, 0xb2, + 0x40, // PartyV ID (16 bytes) + 0x01, 0x5f, 0x5d, 0x21, 0xbb, 0x74, 0x4d, 0x2d, 0xd4, 0xf0, 0x17, 0x24, 0x03, 0x4d, 0x1b, + 0xab, // Length (256 bits) (4 bytes) + 0x00, 0x00, 0x01, 0x00, + ], + okm: &[ + 0x05, 0x69, 0xc2, 0x57, 0x2a, 0x11, 0x3d, 0x89, 0x70, 0x57, 0xdd, 0xac, 0xcc, 0xf1, 0x19, + 0xd9, 0x83, 0x7d, 0x7a, 0x5a, 0xcd, 0x14, 0xaa, 0xcb, 0xf7, 0xbb, 0xf7, 0x5d, 0x42, 0xbd, + 0x5c, 0x1d, + ], +}; From 771f18f7468fb38d481ced33cbd3c5d8fe25c06a Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 2 Jul 2026 12:04:36 -0700 Subject: [PATCH 03/19] Add KBKDF self test and vectors * Clean up unused APIs --- fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs | 7 --- fw/plat/uno/fw/pal/src/self_test/mod.rs | 50 ++------------------ fw/plat/uno/fw/pal/src/self_test/vectors.rs | 52 +++++++++++++++++++++ 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs b/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs index de933fa25..3dca97831 100644 --- a/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs +++ b/fw/plat/uno/fw/pal/src/self_test/aes_cbc.rs @@ -17,7 +17,6 @@ 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 azihsm_fw_uno_trace::tracing::info; use super::vectors::AES_CBC_256_KAT; use crate::UnoHsmIo; @@ -45,10 +44,8 @@ pub(super) async fn run_aes_cbc(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> let pt = scope.dma_alloc(v.plaintext.len())?; pt.copy_from_slice(v.plaintext); let ct_out = scope.dma_alloc_zeroed(v.ciphertext.len())?; - info!("selftest", "AES-CBC encrypt 64B: submit"); pal.aes_cbc_enc_dec(io, AesOp::Encrypt, &*key, &*pt, &*iv, &mut *ct_out, None) .await?; - info!("selftest", "AES-CBC encrypt: engine ok"); // KAT vectors are public, fixed test data — a plain slice comparison is // correct; no constant-time compare is needed. if &ct_out[..] != v.ciphertext { @@ -59,16 +56,13 @@ pub(super) async fn run_aes_cbc(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> ); return Err(HsmError::SelfTestKatMismatch); } - info!("selftest", "AES-CBC encrypt: verified"); // ── 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())?; - info!("selftest", "AES-CBC decrypt 64B: submit"); pal.aes_cbc_enc_dec(io, AesOp::Decrypt, &*key, &*ct, &*iv, &mut *pt_out, None) .await?; - info!("selftest", "AES-CBC decrypt: engine ok"); if &pt_out[..] != v.plaintext { error!( "selftest", @@ -77,7 +71,6 @@ pub(super) async fn run_aes_cbc(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> ); return Err(HsmError::SelfTestKatMismatch); } - info!("selftest", "AES-CBC decrypt: verified"); Ok::<(), HsmError>(()) }) diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index 02b12fae2..2b0e96272 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -18,9 +18,10 @@ //! //! # Status //! -//! AES-256-CBC is implemented as the initial test. Further tests (HKDF, KBKDF, -//! per-engine ECDSA / ECDH / RSA, and the RNG/DRBG FW-mode KAT) extend the -//! [`SelfTest`] enum and the [`run_one`] dispatch. +//! 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; @@ -31,48 +32,6 @@ use azihsm_fw_hsm_pal_traits::HsmResult; use crate::UnoHsmIo; use crate::UnoHsmPal; -/// Identifies a single cryptographic algorithm self-test. -// Variants beyond `AesCbc` (and their constructors) land with the boot gate -// and per-engine tests in a later milestone. -#[allow(dead_code)] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] -pub enum SelfTest { - /// AES-256-CBC known-answer test (HSM AES engine). - AesCbc, -} - -/// One scheduled self-test unit: a [`SelfTest`] optionally pinned to a specific -/// hardware engine instance. -/// -/// `engine` is `None` for tests that are not engine-scoped (such as -/// [`SelfTest::AesCbc`]); per-PKA-engine tests (added later) carry -/// `Some(index)` to validate each engine individually. -// -// `engine` is read once per-engine tests are wired (later milestone). -#[allow(dead_code)] -#[derive(Clone, Copy)] -pub struct SelfTestItem { - /// Which self-test to run. - pub test: SelfTest, - /// Target PKA engine instance, if the test is engine-scoped. - pub engine: Option, -} - -/// Runs a single self-test item to completion. -/// -/// The caller decides which items to run; `run_one` itself is -/// context-agnostic. -// -// Wired into the boot gate and the periodic self-test task in a later -// milestone; retained here as the dispatch entry point. -#[allow(dead_code)] -pub(crate) async fn run_one(pal: &UnoHsmPal, io: &UnoHsmIo, item: SelfTestItem) -> HsmResult<()> { - match item.test { - SelfTest::AesCbc => aes_cbc::run_aes_cbc(pal, io).await, - } -} - /// Runs the full pre-operational self-test suite. /// /// Returns `Err` on the first failing test, leaving it to the caller (the boot @@ -81,5 +40,6 @@ pub(crate) async fn run_one(pal: &UnoHsmPal, io: &UnoHsmIo, item: SelfTestItem) 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?; Ok(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 9685ac6ab..c488246c8 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -108,3 +108,55 @@ pub(super) const HKDF_SHA256_KAT: HkdfKat = HkdfKat { 0x5c, 0x1d, ], }; + +/// KBKDF (SP 800-108 counter mode, HMAC-SHA-512) known-answer vector. +/// +/// Exercises the production [`sp800_108_kdf`](azihsm_fw_hsm_pal_traits::HsmKdf::sp800_108_kdf) +/// path, which builds each PRF input as `counter ‖ label ‖ 0x00 ‖ context ‖ L` +/// and derives `output = HMAC-SHA-512(key, …)`. +/// +/// The reference firmware's KBKDF self-test (and the raw NIST CAVP counter-mode +/// vectors) use an **opaque `FixedInputData`** blob, which uno's structured KDF +/// cannot reproduce (it always inserts the `0x00` separator and appends its own +/// `L`). So this `okm` was computed for uno's exact construction with a +/// KDF-CTR implementation **validated to reproduce the NIST CAVP HMAC-SHA-512 +/// answers byte-for-byte**; `key` is a real NIST CAVP HMAC-SHA-512 key. +pub(super) struct KbkdfKat { + /// Key-derivation key (KDK), 64 bytes (NIST CAVP HMAC-SHA-512 key). + pub key: &'static [u8], + + /// Label (application-specific purpose string). + pub label: &'static [u8], + + /// Context (application-specific context bytes). + pub context: &'static [u8], + + /// Expected derived output (40 bytes; L = 320 bits, single SHA-512 block). + pub okm: &'static [u8], +} + +/// KBKDF-SHA-512 known-answer test vector (40-byte output). +pub(super) const KBKDF_SHA512_KAT: KbkdfKat = KbkdfKat { + key: &[ + 0xdd, 0x5d, 0xbd, 0x45, 0x59, 0x3e, 0xe2, 0xac, 0x13, 0x97, 0x48, 0xe7, 0x64, 0x5b, 0x45, + 0x0f, 0x22, 0x3d, 0x2f, 0xf2, 0x97, 0xb7, 0x3f, 0xd7, 0x1c, 0xbc, 0xeb, 0xe7, 0x1d, 0x41, + 0x65, 0x3c, 0x95, 0x0b, 0x88, 0x50, 0x0d, 0xe5, 0x32, 0x2d, 0x99, 0xef, 0x18, 0xdf, 0xdd, + 0x30, 0x42, 0x82, 0x94, 0xc4, 0xb3, 0x09, 0x4f, 0x4c, 0x95, 0x43, 0x34, 0xe5, 0x93, 0xbd, + 0x98, 0x2e, 0xc6, 0x14, + ], + // "azihsm-kbkdf-selftest" + label: &[ + 0x61, 0x7a, 0x69, 0x68, 0x73, 0x6d, 0x2d, 0x6b, 0x62, 0x6b, 0x64, 0x66, 0x2d, 0x73, 0x65, + 0x6c, 0x66, 0x74, 0x65, 0x73, 0x74, + ], + // "uno-hsm-cast-vector" + context: &[ + 0x75, 0x6e, 0x6f, 0x2d, 0x68, 0x73, 0x6d, 0x2d, 0x63, 0x61, 0x73, 0x74, 0x2d, 0x76, 0x65, + 0x63, 0x74, 0x6f, 0x72, + ], + okm: &[ + 0x97, 0x39, 0xea, 0x79, 0xc6, 0x1c, 0xf6, 0x73, 0x15, 0xbf, 0xb1, 0xc6, 0xfd, 0x3e, 0x0d, + 0x06, 0xee, 0xea, 0x91, 0x51, 0x93, 0x58, 0x79, 0xf1, 0xba, 0x87, 0x40, 0x4f, 0xd7, 0x36, + 0x7c, 0x66, 0x83, 0x37, 0x05, 0x13, 0x5e, 0x8b, 0x9e, 0x4f, + ], +}; From c5e22bcdefc611bba9ee019ee2017ac567570f78 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 2 Jul 2026 16:29:33 -0700 Subject: [PATCH 04/19] Add RSA self test and fix pka driver issue --- fw/pal/traits/src/error.rs | 1 + fw/plat/uno/fw/pal/src/self_test/mod.rs | 4 + fw/plat/uno/fw/pal/src/self_test/pka.rs | 82 +++++++++++++++ fw/plat/uno/fw/pal/src/self_test/vectors.rs | 105 ++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 fw/plat/uno/fw/pal/src/self_test/pka.rs diff --git a/fw/pal/traits/src/error.rs b/fw/pal/traits/src/error.rs index 14a4a26bd..ac5ec14a1 100644 --- a/fw/pal/traits/src/error.rs +++ b/fw/pal/traits/src/error.rs @@ -345,6 +345,7 @@ 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). diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index 2b0e96272..de905eb39 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -25,6 +25,7 @@ mod aes_cbc; mod kdf; +mod pka; mod vectors; use azihsm_fw_hsm_pal_traits::HsmResult; @@ -41,5 +42,8 @@ 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(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs new file mode 100644 index 000000000..9c58b559c --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Per-engine PKA cryptographic algorithm self-tests (CAST). +//! +//! Unlike the algorithm-level KATs (AES / HKDF / KBKDF), the PKA tests validate +//! **each hardware PKA engine individually**: they pin a specific engine via +//! [`UpkaDriver::acquire_engine`] and run the operation on that engine, so the +//! caller can cover all [`PKA_ENGINES`] engines. +//! +//! The initial test is the RSA-2048 modular-exponentiation (private-key) +//! known-answer test, ported from the reference firmware's +//! `rsa_mod_exp_self_test`: it computes `plaintext = c^d mod n` on the pinned +//! engine and compares against the expected plaintext `k`. 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::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; +use azihsm_fw_uno_drivers_upka::UpkaRsaKeyType; +use azihsm_fw_uno_reg_soc::io_gsram::UPKA_ENGINE_CMD_COUNT; +use azihsm_fw_uno_trace::tracing::error; + +use super::vectors::RSA_2K_MOD_EXP_KAT; +use crate::UnoHsmIo; +use crate::UnoHsmPal; + +/// Number of hardware PKA engines to validate (one self-test run each). +pub(super) const PKA_ENGINES: u8 = UPKA_ENGINE_CMD_COUNT as u8; + +/// RSA-2048 modulus size in bytes. +const RSA_2K_LEN: usize = 256; + +/// Runs the RSA-2048 mod-exp (private-key) known-answer test on PKA engine +/// `engine`. +/// +/// Computes `c^d mod n` on the pinned engine and compares the result against +/// the expected plaintext. Returns [`HsmError::SelfTestKatMismatch`] on a +/// mismatch, or any error surfaced by the PKA engine / allocator. +pub(super) async fn run_rsa_mod_exp_on_engine( + pal: &UnoHsmPal, + io: &UnoHsmIo, + engine: u8, +) -> HsmResult<()> { + let v = &RSA_2K_MOD_EXP_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // Private key is laid out as `d ‖ n` (exponent then modulus). + let key = scope.dma_alloc(RSA_2K_LEN * 2)?; + key[..RSA_2K_LEN].copy_from_slice(v.d); + key[RSA_2K_LEN..].copy_from_slice(v.n); + // Input ciphertext `c` and output plaintext buffer. + let input = scope.dma_alloc(RSA_2K_LEN)?; + input.copy_from_slice(v.c); + let output = scope.dma_alloc_zeroed(RSA_2K_LEN)?; + + // Pin the requested engine for the operation, release afterwards. + let mut eng = pal.pka.acquire_engine(engine).await?; + let outcome = eng + .rsa_mod_exp_priv(UpkaRsaKeyType::Rsa2048, &*key, &*input, &mut *output) + .await; + let release = eng.release().await; + outcome?; + release?; + + // KAT vectors are public, fixed test data — a plain slice comparison is + // correct; no constant-time compare is needed. + if &output[..] != v.k { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K mod-exp KAT mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index c488246c8..d1cee70c5 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -160,3 +160,108 @@ pub(super) const KBKDF_SHA512_KAT: KbkdfKat = KbkdfKat { 0x7c, 0x66, 0x83, 0x37, 0x05, 0x13, 0x5e, 0x8b, 0x9e, 0x4f, ], }; + +/// RSA-2048 modular-exponentiation (private-key) known-answer vector. +/// +/// Exercises the raw RSA private-key operation `plaintext = c^d mod n` on a PKA +/// engine, comparing against the expected plaintext `k`. Byte-for-byte +/// identical to the reference firmware's `KAT_RSA_TEST_2K_TEST_VECTORS` +/// (`n`, `d`, `c`, `k`). All values are little-endian, matching the LE-native +/// PKA wire format. +pub(super) struct RsaModExpKat { + /// Modulus `n` (256 bytes). + pub n: &'static [u8], + + /// Private exponent `d` (256 bytes). + pub d: &'static [u8], + + /// Ciphertext input `c` (256 bytes). + pub c: &'static [u8], + + /// Expected plaintext output `k` (256 bytes). + pub k: &'static [u8], +} + +/// RSA-2048 mod-exp (private-key) known-answer test vector. +pub(super) const RSA_2K_MOD_EXP_KAT: RsaModExpKat = RsaModExpKat { + n: &[ + 0xb1, 0x94, 0x6c, 0xfa, 0x0a, 0x3d, 0xff, 0x4e, 0xab, 0x05, 0xd2, 0xca, 0xcc, 0xdd, 0x6b, + 0xfd, 0x45, 0x89, 0x40, 0xda, 0xd0, 0xea, 0x71, 0x97, 0x47, 0x03, 0xc3, 0x77, 0x30, 0xe8, + 0xea, 0xd8, 0x69, 0xb9, 0x0f, 0x16, 0xa1, 0x42, 0x16, 0xcb, 0x89, 0x36, 0xdf, 0x5a, 0x2a, + 0x8b, 0x97, 0x0e, 0x96, 0xe9, 0xe7, 0x8c, 0x82, 0xf5, 0x6d, 0xf2, 0x2a, 0x72, 0xfe, 0x9a, + 0x7e, 0x68, 0x1b, 0x0b, 0x6a, 0x40, 0x44, 0x54, 0x3f, 0xe5, 0xe9, 0xd3, 0x33, 0x5d, 0x1b, + 0x6b, 0x1b, 0xad, 0xde, 0x58, 0x1c, 0xaf, 0x60, 0x7d, 0xd9, 0xa1, 0x98, 0x03, 0x57, 0x56, + 0x13, 0x9f, 0x44, 0xef, 0x57, 0x52, 0xed, 0x52, 0x69, 0x73, 0x17, 0x04, 0x3f, 0x19, 0xfb, + 0xc9, 0x8f, 0x54, 0x15, 0xb3, 0x4a, 0x24, 0x49, 0x9c, 0x63, 0x3c, 0xe5, 0x0b, 0xbc, 0x60, + 0x9e, 0x22, 0x49, 0x8a, 0xbc, 0x22, 0x0f, 0x87, 0xf6, 0x83, 0x63, 0x6a, 0x65, 0x0b, 0x9b, + 0x48, 0xe3, 0x5e, 0x04, 0x8c, 0xf5, 0x0d, 0xc6, 0x10, 0x73, 0xdf, 0x96, 0xaf, 0x24, 0x55, + 0x7c, 0xd2, 0x69, 0x2b, 0x9a, 0xd7, 0x9d, 0x7c, 0xef, 0x59, 0x24, 0xf6, 0xad, 0xd2, 0x52, + 0x62, 0x72, 0x2d, 0xa0, 0x94, 0x0a, 0x30, 0xa1, 0xaf, 0x1d, 0xf7, 0xdb, 0x55, 0xcb, 0xb8, + 0xbc, 0x7d, 0x83, 0x95, 0xea, 0x3b, 0xe3, 0x46, 0x88, 0xf4, 0xf9, 0xd6, 0x13, 0xa6, 0xc7, + 0xc4, 0x9d, 0x29, 0xe1, 0x8c, 0x29, 0x5e, 0xf3, 0x0f, 0x26, 0xb9, 0x41, 0x5a, 0xc9, 0xcf, + 0x6e, 0xe1, 0xc9, 0xed, 0xdd, 0x87, 0x74, 0xef, 0xf4, 0x97, 0x3b, 0xfe, 0x77, 0x26, 0xd7, + 0xa5, 0x86, 0xa1, 0xf7, 0x25, 0x07, 0x97, 0x02, 0x86, 0x0c, 0x39, 0x35, 0x55, 0x5b, 0x92, + 0x64, 0xbb, 0x13, 0x7a, 0x36, 0xc3, 0xd4, 0x47, 0xe2, 0xbf, 0x20, 0x7f, 0x80, 0x24, 0xb8, + 0xc3, + ], + d: &[ + 0x39, 0xe0, 0x6c, 0x00, 0x27, 0x47, 0x86, 0x6d, 0xf1, 0xe9, 0xb9, 0x08, 0x60, 0x01, 0x8e, + 0xaa, 0xc6, 0x82, 0x41, 0x67, 0xe9, 0xb1, 0xbc, 0x83, 0x6e, 0x58, 0x35, 0x6e, 0x0d, 0xc7, + 0xd1, 0xc5, 0xb7, 0x47, 0x57, 0xa2, 0xca, 0x2f, 0xba, 0x47, 0xc8, 0xb2, 0x01, 0x7b, 0x63, + 0x49, 0x1a, 0x5c, 0xd3, 0x53, 0x2b, 0xb6, 0x27, 0xa1, 0x34, 0xf1, 0x0c, 0x5a, 0x2e, 0xec, + 0xa6, 0x4f, 0xd6, 0x99, 0x87, 0xc9, 0x78, 0xcc, 0x82, 0xf2, 0x4a, 0x0e, 0xda, 0x2e, 0x45, + 0xce, 0xde, 0x9a, 0x49, 0x61, 0x9a, 0xd4, 0x45, 0x34, 0x97, 0x29, 0xd7, 0x7b, 0x3d, 0x6a, + 0x1e, 0x00, 0x32, 0xd6, 0xbe, 0xcf, 0x90, 0x1f, 0x7d, 0x6f, 0x6e, 0x5e, 0xe9, 0xed, 0x34, + 0x75, 0x61, 0x31, 0x15, 0x3d, 0xcd, 0xc3, 0xea, 0x1f, 0x89, 0x1e, 0xd4, 0x3a, 0x54, 0x64, + 0x9f, 0x1a, 0x27, 0x0d, 0x6b, 0x02, 0x33, 0x38, 0x25, 0x47, 0xe5, 0x50, 0x44, 0xa4, 0xa8, + 0x0e, 0xf8, 0x01, 0x1c, 0xd3, 0xb5, 0x05, 0xea, 0x6f, 0x8a, 0x9f, 0x3d, 0x35, 0x51, 0xe4, + 0x96, 0xc1, 0x4a, 0x7b, 0x04, 0xe5, 0x1c, 0x64, 0x0f, 0xa7, 0xc3, 0x08, 0xb4, 0xa2, 0x13, + 0x9d, 0x05, 0x59, 0x27, 0xcf, 0x51, 0x4a, 0x82, 0xb1, 0xdb, 0x1f, 0xdb, 0xcc, 0x34, 0xe3, + 0xe2, 0x6d, 0x9e, 0xe1, 0xda, 0x7b, 0x33, 0x1e, 0x2b, 0x08, 0x4c, 0xc0, 0x00, 0x1e, 0xab, + 0x77, 0x81, 0xea, 0x1e, 0xc3, 0x8b, 0x6c, 0xee, 0x90, 0xc4, 0xbb, 0xd6, 0xde, 0x7a, 0x92, + 0xd9, 0xff, 0x75, 0xd4, 0x42, 0x8a, 0xa6, 0x41, 0x87, 0x64, 0x3a, 0x93, 0x96, 0x42, 0x75, + 0x37, 0xa2, 0x56, 0x43, 0x99, 0x82, 0xb9, 0xdb, 0xc5, 0x17, 0x6d, 0xcb, 0xd8, 0xcd, 0x1e, + 0x9a, 0x15, 0xc8, 0xce, 0x1d, 0x37, 0xf9, 0x0c, 0xba, 0xec, 0xc5, 0xdc, 0xe8, 0x7c, 0x3d, + 0x18, + ], + c: &[ + 0x73, 0xf7, 0x6e, 0x60, 0xf3, 0xd9, 0x09, 0xaf, 0xb2, 0x27, 0xd5, 0xf8, 0x90, 0xa0, 0xeb, + 0xcb, 0x73, 0xea, 0x8c, 0x72, 0xe2, 0xab, 0xb6, 0xa4, 0x22, 0xe5, 0xdd, 0xa1, 0xf6, 0x1c, + 0x4e, 0xa8, 0x80, 0xa3, 0x5f, 0x88, 0xcf, 0x31, 0x30, 0xbf, 0xa3, 0xee, 0x19, 0x84, 0x4d, + 0xaf, 0xb7, 0xd0, 0x62, 0xff, 0x2b, 0x19, 0xe6, 0x56, 0xa7, 0xd4, 0x0d, 0x75, 0x51, 0x01, + 0xa9, 0x11, 0xa6, 0xfe, 0x36, 0x26, 0x9c, 0x0f, 0xbf, 0xad, 0xa3, 0xfa, 0xeb, 0xf2, 0xd9, + 0x19, 0x93, 0xbc, 0xd7, 0x1c, 0x12, 0xfa, 0xf7, 0x09, 0xff, 0x9c, 0xa9, 0xff, 0xf3, 0x6d, + 0xbb, 0x24, 0x30, 0x81, 0xfa, 0x66, 0x7e, 0x45, 0x87, 0x7e, 0x99, 0x0c, 0x78, 0x1c, 0x0a, + 0x83, 0x95, 0x75, 0x8d, 0xfd, 0x94, 0x24, 0x36, 0x79, 0x95, 0x26, 0xc7, 0x68, 0xd1, 0x65, + 0x37, 0x1e, 0xdf, 0x03, 0xa1, 0x06, 0x94, 0x04, 0x9e, 0x8a, 0xfc, 0x66, 0x2d, 0x84, 0x53, + 0xab, 0x97, 0x37, 0x42, 0x69, 0xcd, 0x02, 0xc0, 0x4d, 0x07, 0x94, 0x2b, 0x19, 0x3a, 0x76, + 0x67, 0xc8, 0x73, 0x32, 0x48, 0x3e, 0x55, 0x0b, 0x5c, 0x53, 0xe6, 0xab, 0xba, 0x01, 0x8c, + 0xda, 0x0c, 0x00, 0x7e, 0xfd, 0x26, 0x45, 0x1e, 0xd7, 0x04, 0x6f, 0x72, 0x65, 0x7f, 0x03, + 0x0b, 0x66, 0x9a, 0x1a, 0xf8, 0xbb, 0x56, 0x37, 0xbc, 0x50, 0xaa, 0x1d, 0xc5, 0x04, 0x02, + 0x37, 0xb6, 0x18, 0x6d, 0x37, 0x0c, 0xbe, 0x67, 0x34, 0x85, 0x34, 0xec, 0x12, 0x62, 0xc7, + 0x54, 0x4f, 0x60, 0xc4, 0x37, 0xfc, 0xe5, 0x59, 0xe8, 0xba, 0xb5, 0xca, 0xbc, 0x30, 0xaf, + 0x48, 0x30, 0x61, 0xf4, 0xfa, 0x02, 0x09, 0xae, 0xd2, 0x47, 0xb6, 0x33, 0x0b, 0xe3, 0x2b, + 0x24, 0x82, 0xf2, 0xce, 0x4d, 0x63, 0x87, 0x67, 0x2d, 0x7b, 0x7f, 0x58, 0xeb, 0x95, 0xb6, + 0x53, + ], + k: &[ + 0x0d, 0xd1, 0xc1, 0x57, 0xef, 0x85, 0xae, 0x6d, 0x12, 0x7d, 0x9f, 0x39, 0x25, 0xf0, 0x1a, + 0x8a, 0xd2, 0x9b, 0x95, 0x10, 0x45, 0x09, 0x01, 0x82, 0xde, 0x62, 0x7a, 0x6d, 0x37, 0x4b, + 0xe8, 0x11, 0x36, 0xca, 0x83, 0xfd, 0xdb, 0x69, 0x36, 0xca, 0x12, 0xa6, 0xf8, 0x58, 0x3c, + 0xb6, 0x8f, 0x52, 0xf1, 0x14, 0xce, 0x48, 0x07, 0x0d, 0xe3, 0x53, 0xd2, 0x23, 0xa9, 0x8f, + 0x5c, 0x2b, 0x51, 0x83, 0x3e, 0x25, 0x24, 0x93, 0xe5, 0xc0, 0x91, 0x43, 0xfe, 0x0d, 0xb2, + 0x60, 0xa8, 0xe3, 0x7d, 0x77, 0x9f, 0xea, 0x9b, 0x84, 0xe4, 0xc4, 0xd9, 0xdb, 0xd9, 0x4f, + 0x56, 0x29, 0x42, 0xe5, 0x73, 0x4e, 0x6d, 0xba, 0x37, 0xd1, 0x7e, 0xf2, 0x23, 0x1c, 0xec, + 0xa0, 0x87, 0x6a, 0x3c, 0x06, 0x55, 0x6e, 0xd4, 0xdf, 0xbb, 0xf9, 0x23, 0x91, 0xcf, 0x03, + 0x83, 0x5a, 0x15, 0x9c, 0x94, 0x7a, 0x30, 0x02, 0xd8, 0x23, 0xa4, 0x13, 0xcb, 0xa2, 0x6b, + 0x69, 0x5a, 0xbd, 0xd1, 0x45, 0x41, 0xb5, 0xcc, 0x12, 0xd9, 0xfa, 0x55, 0xbd, 0xd6, 0x59, + 0x2b, 0x9c, 0x87, 0x3d, 0x7a, 0x58, 0x2c, 0xd2, 0xd2, 0x13, 0x5d, 0xad, 0x2e, 0x69, 0xbf, + 0xbf, 0xe6, 0xac, 0xbf, 0xa4, 0x3f, 0xc5, 0xfd, 0x52, 0xd0, 0xc3, 0x4c, 0x4b, 0x45, 0xc7, + 0xf4, 0x82, 0x3f, 0x58, 0xfe, 0xb9, 0xe1, 0x66, 0x1f, 0xa9, 0xbe, 0xa5, 0xb7, 0x62, 0xc9, + 0x5c, 0xf8, 0xb9, 0x6c, 0xe2, 0x60, 0x0b, 0xc5, 0x4d, 0x6c, 0xfe, 0x24, 0x85, 0x8b, 0x7f, + 0x36, 0xe3, 0xc3, 0xa3, 0x68, 0xc5, 0xa2, 0x5d, 0xf6, 0xaa, 0x05, 0x51, 0xfc, 0xbe, 0xca, + 0xcf, 0x84, 0xc1, 0xe5, 0xa0, 0xb0, 0xc6, 0xaf, 0x93, 0xd0, 0xd2, 0xff, 0x12, 0xb1, 0x4a, + 0x73, 0xfc, 0x50, 0x22, 0xad, 0x96, 0x44, 0xc0, 0xdb, 0x9e, 0x93, 0x1c, 0x3a, 0xc5, 0xa5, + 0x00, + ], +}; From eab32f53d2b928c50f5faccb19d535d61482b5d9 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 2 Jul 2026 16:45:50 -0700 Subject: [PATCH 05/19] Add missing kdf.rs file --- fw/plat/uno/fw/pal/src/self_test/kdf.rs | 111 ++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 fw/plat/uno/fw/pal/src/self_test/kdf.rs diff --git a/fw/plat/uno/fw/pal/src/self_test/kdf.rs b/fw/plat/uno/fw/pal/src/self_test/kdf.rs new file mode 100644 index 000000000..e32f0b776 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/kdf.rs @@ -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 +} From 3fb6c3d61eed1674039b27e4f6310658e198df3e Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Mon, 6 Jul 2026 16:07:33 -0700 Subject: [PATCH 06/19] Add RSA-2K CRT self tests * Modify the driver to support this test --- fw/plat/uno/fw/drivers/upka/src/engine.rs | 25 ++- fw/plat/uno/fw/drivers/upka/src/opcode.rs | 43 +++++- fw/plat/uno/fw/pal/src/self_test/mod.rs | 12 +- fw/plat/uno/fw/pal/src/self_test/pka.rs | 69 ++++++++- fw/plat/uno/fw/pal/src/self_test/vectors.rs | 161 ++++++++++++++++++++ 5 files changed, 288 insertions(+), 22 deletions(-) diff --git a/fw/plat/uno/fw/drivers/upka/src/engine.rs b/fw/plat/uno/fw/drivers/upka/src/engine.rs index a82f96c06..584a5fc42 100644 --- a/fw/plat/uno/fw/drivers/upka/src/engine.rs +++ b/fw/plat/uno/fw/drivers/upka/src/engine.rs @@ -387,7 +387,10 @@ impl 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. /// @@ -404,16 +407,28 @@ impl 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 } diff --git a/fw/plat/uno/fw/drivers/upka/src/opcode.rs b/fw/plat/uno/fw/drivers/upka/src/opcode.rs index db6d32a7e..3bd94bf0f 100644 --- a/fw/plat/uno/fw/drivers/upka/src/opcode.rs +++ b/fw/plat/uno/fw/drivers/upka/src/opcode.rs @@ -341,6 +341,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 @@ -352,17 +369,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 diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index de905eb39..d21ddaa55 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -18,10 +18,11 @@ //! //! # 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. +//! AES-256-CBC, HKDF, KBKDF, and per-engine RSA-2048 mod-exp (standard and CRT) +//! are implemented. Further tests (per-engine ECDSA / ECDH, 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; @@ -45,5 +46,8 @@ pub(crate) async fn run_pre_op(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> for engine in 0..pka::PKA_ENGINES { pka::run_rsa_mod_exp_on_engine(pal, io, engine).await?; } + for engine in 0..pka::PKA_ENGINES { + pka::run_rsa_mod_exp_crt_on_engine(pal, io, engine).await?; + } Ok(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs index 9c58b559c..f6aaa5920 100644 --- a/fw/plat/uno/fw/pal/src/self_test/pka.rs +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -8,12 +8,12 @@ //! [`UpkaDriver::acquire_engine`] and run the operation on that engine, so the //! caller can cover all [`PKA_ENGINES`] engines. //! -//! The initial test is the RSA-2048 modular-exponentiation (private-key) -//! known-answer test, ported from the reference firmware's -//! `rsa_mod_exp_self_test`: it computes `plaintext = c^d mod n` on the pinned -//! engine and compares against the expected plaintext `k`. Operands are staged -//! into the self-test IO slot's DMA buffer via the bump allocator (see -//! [`crate::self_test`]). +//! The RSA tests are ported from the reference firmware's +//! `rsa_mod_exp_self_test` / `rsa_mod_exp_crt_self_test`: they compute +//! `plaintext = c^d mod n` on the pinned engine — using either a standard +//! `d ‖ n` key or a CRT `param1 ‖ param2` key — and compare against the expected +//! plaintext `k`. 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; @@ -23,6 +23,7 @@ use azihsm_fw_uno_drivers_upka::UpkaRsaKeyType; use azihsm_fw_uno_reg_soc::io_gsram::UPKA_ENGINE_CMD_COUNT; use azihsm_fw_uno_trace::tracing::error; +use super::vectors::RSA_2K_CRT_KAT; use super::vectors::RSA_2K_MOD_EXP_KAT; use crate::UnoHsmIo; use crate::UnoHsmPal; @@ -65,8 +66,6 @@ pub(super) async fn run_rsa_mod_exp_on_engine( outcome?; release?; - // KAT vectors are public, fixed test data — a plain slice comparison is - // correct; no constant-time compare is needed. if &output[..] != v.k { error!( "selftest", @@ -80,3 +79,57 @@ pub(super) async fn run_rsa_mod_exp_on_engine( }) .await } + +/// Runs the RSA-2048 CRT mod-exp (private-key) known-answer test on PKA engine +/// `engine`. +/// +/// Ported from the reference firmware's `rsa_mod_exp_crt_self_test`. The CRT +/// private key is staged as a contiguous `param1 ‖ param2` blob: `param1` +/// (`p ‖ q ‖ dp ‖ dq`, `2 * RSA_2K_LEN`) is read from the key pointer and +/// `param2` (`n ‖ n1q ‖ n2p`, `3 * RSA_2K_LEN`) is read by the hardware from the +/// arg3 pointer that [`rsa_mod_exp_priv`](azihsm_fw_uno_drivers_upka::UpkaEngine::rsa_mod_exp_priv) +/// derives for CRT key types. Computes `c^d mod n` on the pinned engine and +/// compares against the expected plaintext. Returns +/// [`HsmError::SelfTestKatMismatch`] on a mismatch, or any error surfaced by the +/// PKA engine / allocator. +pub(super) async fn run_rsa_mod_exp_crt_on_engine( + pal: &UnoHsmPal, + io: &UnoHsmIo, + engine: u8, +) -> HsmResult<()> { + let v = &RSA_2K_CRT_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // CRT private key blob: `param1` (p‖q‖dp‖dq) followed by `param2` + // (n‖n1q‖n2p). The driver passes the blob base as arg2 (param1) and + // `base + param1.len()` as arg3 (param2) for CRT key types. + let key = scope.dma_alloc(v.param1.len() + v.param2.len())?; + key[..v.param1.len()].copy_from_slice(v.param1); + key[v.param1.len()..].copy_from_slice(v.param2); + // Input ciphertext `c` and output plaintext buffer. + let input = scope.dma_alloc(RSA_2K_LEN)?; + input.copy_from_slice(v.c); + let output = scope.dma_alloc_zeroed(RSA_2K_LEN)?; + + // Pin the requested engine for the operation, release afterwards. + let mut eng = pal.pka.acquire_engine(engine).await?; + let outcome = eng + .rsa_mod_exp_priv(UpkaRsaKeyType::Rsa2048Crt, &*key, &*input, &mut *output) + .await; + let release = eng.release().await; + outcome?; + release?; + + if &output[..] != v.k { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K CRT mod-exp KAT mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index d1cee70c5..2ab11fa1f 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -265,3 +265,164 @@ pub(super) const RSA_2K_MOD_EXP_KAT: RsaModExpKat = RsaModExpKat { 0x00, ], }; + +/// RSA-2048 CRT modular-exponentiation (private-key) known-answer vector. +/// +/// Exercises the raw RSA-CRT private-key operation on a PKA engine: the +/// hardware reads `param1` (`p ‖ q ‖ dp ‖ dq`, 512 bytes) from the key +/// pointer and `param2` (`n ‖ n1q ‖ n2p`, 768 bytes) from the arg3 pointer, +/// computes `plaintext = c^d mod n`, and the result is compared against `k`. +/// Byte-for-byte identical to the reference firmware's +/// `KAT_RSA_TEST_2K_CRT_TEST_VECTORS` (the trailing `param2.ext` field is not +/// read by the operation and is omitted). All values are little-endian, +/// matching the LE-native PKA wire format. +pub(super) struct Rsa2kCrtKat { + /// CRT parameter block 1: `p ‖ q ‖ dp ‖ dq` (512 bytes). + pub param1: &'static [u8], + + /// CRT parameter block 2: `n ‖ n1q ‖ n2p` (768 bytes). + pub param2: &'static [u8], + + /// Ciphertext input `c` (256 bytes). + pub c: &'static [u8], + + /// Expected plaintext output `k` (256 bytes). + pub k: &'static [u8], +} + +/// RSA-2048 CRT mod-exp (private-key) known-answer test vector. +pub(super) const RSA_2K_CRT_KAT: Rsa2kCrtKat = Rsa2kCrtKat { + // param1 = p ‖ q ‖ dp ‖ dq (4 × 128 bytes) + param1: &[ + 0xd5, 0x31, 0x82, 0x4a, 0xbb, 0x1a, 0xd1, 0x6b, 0xc3, 0x8e, 0x3f, 0x13, 0x52, 0x07, 0x3b, + 0x3c, 0xa1, 0x04, 0x3e, 0xc3, 0xc8, 0x6a, 0xf9, 0x87, 0x53, 0x8c, 0x82, 0x96, 0x9e, 0xa9, + 0x97, 0x3d, 0x66, 0xa7, 0xc0, 0x53, 0x25, 0xce, 0x05, 0x50, 0x20, 0x5c, 0x94, 0x6a, 0xc1, + 0xc9, 0xab, 0x92, 0x90, 0xdf, 0xc1, 0xf1, 0xd1, 0xbb, 0xd1, 0x0a, 0xa0, 0x82, 0xd9, 0xf4, + 0x16, 0xb1, 0x33, 0x12, 0x54, 0x78, 0x2e, 0x55, 0x82, 0xa2, 0x73, 0xd5, 0x8f, 0x2a, 0xc2, + 0x6c, 0x2c, 0xfa, 0x27, 0x56, 0x08, 0x57, 0x7c, 0xec, 0x17, 0x20, 0xff, 0x3d, 0xe2, 0xc3, + 0xfb, 0x9b, 0x31, 0x98, 0xff, 0xb3, 0x72, 0x42, 0x7a, 0x30, 0x42, 0x6f, 0xfa, 0x05, 0xb8, + 0x7c, 0xfa, 0xc6, 0xdd, 0xb7, 0x33, 0xec, 0x38, 0xdf, 0x67, 0x45, 0x5e, 0xdc, 0xd4, 0x19, + 0xb4, 0xbc, 0x59, 0x0b, 0xd7, 0xb4, 0x9f, 0xff, 0xe9, 0x7c, 0x1a, 0xda, 0xc7, 0x43, 0x14, + 0x89, 0xd1, 0xb2, 0x39, 0xbb, 0x4f, 0x4a, 0xf2, 0x49, 0x24, 0x6c, 0x18, 0xc6, 0x36, 0xfa, + 0x4e, 0x9c, 0x75, 0x9e, 0x68, 0xee, 0x54, 0x2e, 0x15, 0x4c, 0x9d, 0x98, 0x89, 0x90, 0xce, + 0x34, 0x17, 0xfb, 0x5d, 0xce, 0xd9, 0x95, 0x62, 0x02, 0x50, 0x4e, 0x85, 0xce, 0xdc, 0x76, + 0x25, 0xfd, 0xe9, 0x11, 0x97, 0x6a, 0xad, 0x19, 0x32, 0xf0, 0xfa, 0xd2, 0xce, 0xaa, 0xfc, + 0xbc, 0x46, 0x53, 0x09, 0x3a, 0x9a, 0x77, 0x74, 0x65, 0x9a, 0x9b, 0x2a, 0x01, 0x2b, 0x6b, + 0x09, 0xc4, 0xe7, 0x7d, 0x68, 0x41, 0xe0, 0x5f, 0xd3, 0x72, 0x51, 0x19, 0x8c, 0xcf, 0x8a, + 0xaf, 0x00, 0x30, 0x53, 0x33, 0xba, 0x31, 0x75, 0xa1, 0x84, 0x8c, 0x2f, 0x5c, 0xa0, 0xd8, + 0xf5, 0xe7, 0x5d, 0x85, 0x8c, 0xc8, 0x0e, 0x6f, 0x95, 0x69, 0xa3, 0x4a, 0x43, 0xb2, 0x7a, + 0xf9, 0xb9, 0xa8, 0x38, 0xcc, 0x66, 0x57, 0x44, 0x3e, 0x19, 0x0a, 0x38, 0x32, 0xc7, 0xe0, + 0x79, 0x75, 0x35, 0x43, 0xf6, 0x66, 0x79, 0xb3, 0xd0, 0x02, 0xa7, 0x46, 0x32, 0x5f, 0x29, + 0x75, 0xdb, 0x75, 0x7f, 0x7a, 0xaa, 0x9b, 0x7e, 0xb7, 0x3c, 0xc8, 0xae, 0x3a, 0x2d, 0x30, + 0x35, 0x22, 0x8e, 0x4f, 0x1d, 0x93, 0x25, 0x64, 0xb6, 0x72, 0xc4, 0x75, 0xdf, 0x4c, 0x71, + 0x7b, 0xde, 0xba, 0x6e, 0x6e, 0x83, 0xe1, 0x93, 0x8d, 0x58, 0x87, 0xdd, 0x2a, 0x47, 0xb4, + 0xe1, 0xd6, 0x8e, 0xf9, 0x3e, 0x3c, 0x62, 0x62, 0x1c, 0x99, 0x21, 0xf7, 0xa9, 0xb7, 0x07, + 0x2d, 0xf4, 0xd0, 0x62, 0xab, 0xb1, 0x1d, 0xb6, 0xa6, 0x0e, 0xbc, 0xc6, 0x18, 0x3f, 0xa7, + 0x44, 0xae, 0xd6, 0x4d, 0x37, 0x0a, 0x26, 0x1e, 0x6d, 0x26, 0xb2, 0xda, 0x80, 0x29, 0xd2, + 0xd2, 0xd2, 0x3c, 0xbe, 0x24, 0x3d, 0x06, 0x8a, 0x73, 0x21, 0x63, 0x3f, 0x36, 0xbd, 0xa6, + 0xc1, 0x74, 0x2b, 0x84, 0x52, 0x0c, 0xc5, 0x21, 0x95, 0xe6, 0xa8, 0x80, 0x15, 0x87, 0x5b, + 0x33, 0xf8, 0x15, 0x4b, 0xc8, 0x65, 0xdc, 0x89, 0x6c, 0x04, 0x76, 0xdc, 0x21, 0xc5, 0x77, + 0x7c, 0xbe, 0x32, 0xaa, 0x50, 0xb8, 0xc4, 0xe4, 0x6c, 0x69, 0x90, 0xc8, 0x10, 0x48, 0x3d, + 0xb9, 0x76, 0x7d, 0xfc, 0x1c, 0x40, 0x98, 0x20, 0x75, 0xc1, 0xd4, 0x33, 0xd6, 0x82, 0x88, + 0x3f, 0xed, 0xcc, 0x05, 0x43, 0x8f, 0x58, 0x97, 0x71, 0x5d, 0xd4, 0x38, 0x2f, 0x8e, 0xe6, + 0x0a, 0x96, 0xa3, 0xce, 0x47, 0x24, 0xa1, 0x0f, 0xb8, 0x38, 0x98, 0x79, 0x62, 0x6d, 0x56, + 0xf8, 0xf1, 0x0d, 0x4e, 0x50, 0x39, 0xe6, 0x06, 0xa2, 0xa5, 0xee, 0x84, 0x98, 0xd8, 0x22, + 0xfb, 0x1d, 0xbd, 0x71, 0x43, 0x42, 0xd8, 0xd8, 0xfb, 0xe9, 0xd3, 0xb2, 0x61, 0xd8, 0xd9, + 0xf1, 0xb5, + ], + // param2 = n ‖ n1q ‖ n2p (3 × 256 bytes) + param2: &[ + 0xdd, 0x86, 0x44, 0x52, 0xc9, 0xf7, 0x9f, 0x7e, 0xe9, 0xaf, 0x12, 0x1d, 0xa7, 0xa2, 0x18, + 0x07, 0x86, 0x61, 0x24, 0xee, 0x33, 0xfe, 0x80, 0x66, 0x38, 0x45, 0x91, 0x9c, 0x4c, 0xa3, + 0xd7, 0xbe, 0xaa, 0x28, 0xac, 0xbe, 0xe0, 0x75, 0x4e, 0x16, 0xc0, 0x8b, 0x4d, 0x2e, 0xaa, + 0xf1, 0x4b, 0x35, 0x78, 0x0b, 0xb5, 0x43, 0x05, 0x8c, 0xa3, 0x31, 0xc3, 0x55, 0x88, 0x86, + 0x5c, 0x52, 0x15, 0xfc, 0x9b, 0x3c, 0xd1, 0x51, 0x70, 0x1f, 0xdd, 0x7c, 0xb8, 0x59, 0xf7, + 0x16, 0x0e, 0x15, 0x4e, 0xac, 0x2b, 0x00, 0xeb, 0x94, 0x62, 0xe8, 0x6b, 0x2f, 0x7a, 0xcc, + 0x95, 0x07, 0x7c, 0xd9, 0xaa, 0x6c, 0xc7, 0x09, 0xfa, 0x94, 0x6f, 0xbf, 0x00, 0x59, 0xb1, + 0x0a, 0x6d, 0xef, 0x6d, 0x24, 0x3e, 0x22, 0x02, 0x01, 0x3d, 0xf8, 0x45, 0x2d, 0xc3, 0x74, + 0x0c, 0x0b, 0xb6, 0x68, 0x56, 0xe6, 0x52, 0x15, 0x46, 0x65, 0x69, 0xc6, 0x36, 0x1c, 0x56, + 0xc4, 0x27, 0x44, 0x8e, 0xee, 0xea, 0x85, 0xf5, 0x6e, 0x7a, 0x08, 0x29, 0x4a, 0x34, 0xe5, + 0xe8, 0x63, 0x39, 0x5e, 0x3e, 0xe6, 0x7d, 0xa4, 0xc3, 0x09, 0x61, 0x84, 0xd8, 0x20, 0x15, + 0x97, 0x20, 0x15, 0x27, 0x55, 0xbd, 0x1d, 0xe3, 0xd2, 0x12, 0xa5, 0xda, 0xfc, 0xd7, 0x7c, + 0xb7, 0x5b, 0x30, 0x40, 0x52, 0x36, 0x66, 0xd5, 0x59, 0xd1, 0xa5, 0xeb, 0x57, 0x8f, 0xd7, + 0x0b, 0xb9, 0xf3, 0x07, 0x27, 0xe7, 0x1d, 0xfa, 0x4b, 0xeb, 0x00, 0xe2, 0xca, 0xdc, 0x83, + 0xc6, 0xcf, 0xd2, 0x20, 0x8e, 0xf0, 0x48, 0x2f, 0x48, 0xa6, 0xc1, 0x3f, 0x17, 0xaa, 0x5f, + 0x86, 0x97, 0x39, 0xff, 0x91, 0xbd, 0xe7, 0x3a, 0x8c, 0x91, 0x59, 0x9d, 0x5b, 0x87, 0xc6, + 0xfd, 0x2d, 0x9c, 0x51, 0xc8, 0x98, 0xdf, 0x7e, 0xdf, 0xe1, 0x96, 0x95, 0x01, 0xdb, 0x1c, + 0xf9, 0x36, 0x73, 0xa3, 0xe2, 0x94, 0xcc, 0xc7, 0x7f, 0x76, 0x0d, 0xf9, 0xec, 0xaa, 0x19, + 0xc9, 0x54, 0xb6, 0x8b, 0x27, 0x27, 0xae, 0x8f, 0xfa, 0xe8, 0xa7, 0xf0, 0x85, 0xab, 0x16, + 0x79, 0x83, 0xf1, 0xfe, 0x04, 0x72, 0x4a, 0x47, 0x53, 0xe9, 0x03, 0xea, 0xaa, 0xc5, 0x0c, + 0x3a, 0x86, 0x8a, 0x0c, 0x80, 0x4d, 0xa7, 0xc2, 0x19, 0x6f, 0x7f, 0x11, 0xa7, 0xdc, 0xac, + 0xce, 0x14, 0x1c, 0x93, 0x54, 0x1f, 0xcf, 0xaa, 0x25, 0x69, 0xb1, 0x69, 0xf8, 0x52, 0x75, + 0xb6, 0xc9, 0x57, 0xfa, 0x2e, 0x79, 0x32, 0x1c, 0xb5, 0xfb, 0xb2, 0xdb, 0xc6, 0x49, 0xeb, + 0xc4, 0xf7, 0xfd, 0xfa, 0x8b, 0xa6, 0x16, 0x04, 0x12, 0xb7, 0x45, 0x66, 0xe4, 0x08, 0xcc, + 0xe3, 0xcb, 0xeb, 0x83, 0x9d, 0x30, 0x93, 0xba, 0x47, 0x60, 0xe2, 0x49, 0xc9, 0x00, 0x56, + 0x9f, 0x22, 0x63, 0xb1, 0xa8, 0x98, 0x0f, 0xd5, 0x7f, 0x60, 0x99, 0xbc, 0x4c, 0x32, 0xe6, + 0x0c, 0x55, 0x33, 0x04, 0x9e, 0xad, 0xc3, 0x09, 0x80, 0x70, 0x84, 0x4f, 0x38, 0x01, 0x12, + 0xf6, 0xaa, 0x89, 0x83, 0x21, 0xba, 0x9d, 0x3b, 0x15, 0x85, 0x9b, 0xc9, 0xa5, 0xfc, 0x57, + 0x9c, 0x18, 0x8c, 0xaa, 0xf2, 0xc7, 0x04, 0x81, 0x2e, 0x18, 0x16, 0x8c, 0xac, 0xd4, 0xa0, + 0x96, 0x4a, 0xfe, 0x29, 0xc4, 0x3c, 0x38, 0xc9, 0xc7, 0xc6, 0xa2, 0xcf, 0xa9, 0xc5, 0x09, + 0xde, 0xbf, 0x74, 0x52, 0x6e, 0xb2, 0x9e, 0x27, 0x2a, 0x1a, 0xfe, 0x5d, 0xd6, 0x6c, 0xa0, + 0x70, 0x12, 0x08, 0xdb, 0x5d, 0x92, 0x9c, 0x1e, 0xca, 0x9b, 0x91, 0x34, 0x1f, 0xbe, 0x2d, + 0x42, 0x8f, 0x1c, 0x97, 0xc4, 0x94, 0xb7, 0x87, 0xa4, 0x5a, 0x35, 0xa8, 0xd7, 0x70, 0x29, + 0x77, 0x12, 0x10, 0x7d, 0x94, 0xdc, 0xb4, 0x9f, 0xc4, 0x52, 0xbf, 0x11, 0xe1, 0x1b, 0x39, + 0x07, 0x1a, 0xa8, 0x13, 0xa1, 0x6f, 0x34, 0x2b, 0xd8, 0xfe, 0x72, 0xa2, 0x19, 0x30, 0xfc, + 0x88, 0x4f, 0xb2, 0xcf, 0xd5, 0xfc, 0xc6, 0x85, 0x6e, 0x86, 0x7d, 0x90, 0x54, 0x0b, 0xf1, + 0x35, 0x2a, 0x54, 0xcd, 0xab, 0x23, 0x3a, 0x74, 0x99, 0x22, 0x65, 0x12, 0xd6, 0xe0, 0x87, + 0x21, 0x70, 0x6b, 0xc1, 0x28, 0xf8, 0xbd, 0x0d, 0x81, 0xeb, 0x1c, 0x24, 0x20, 0x1c, 0x79, + 0xdb, 0xb7, 0x47, 0x36, 0x82, 0xa7, 0x7c, 0x6d, 0x26, 0x2c, 0x07, 0x6e, 0x73, 0x84, 0x65, + 0xe4, 0x40, 0x4d, 0xb6, 0x1a, 0x1f, 0x33, 0xf9, 0xe3, 0x35, 0x99, 0xaf, 0x0c, 0xa5, 0xe5, + 0x8e, 0x07, 0x9e, 0x09, 0x81, 0x4d, 0x04, 0x56, 0xc3, 0xf7, 0x42, 0x4f, 0x09, 0xdb, 0xf7, + 0x8c, 0xcd, 0x3e, 0x81, 0x6b, 0xd0, 0xf3, 0xaa, 0x67, 0xba, 0xa0, 0x5a, 0xae, 0x7c, 0x2c, + 0x6d, 0xd5, 0xe9, 0xa7, 0x04, 0xc0, 0xbd, 0xd6, 0x7d, 0x95, 0xe5, 0xcb, 0xac, 0x79, 0x04, + 0x36, 0x49, 0x6f, 0xf4, 0x3f, 0xf0, 0x40, 0x27, 0x7c, 0x75, 0xfe, 0xf5, 0xb8, 0xf0, 0x48, + 0x22, 0xef, 0x3d, 0xda, 0xb5, 0x3c, 0x84, 0x48, 0x42, 0x8f, 0x3e, 0x6e, 0x97, 0xde, 0xdb, + 0xc8, 0x78, 0x7e, 0x94, 0x6a, 0x34, 0x8d, 0xb8, 0x9c, 0xb4, 0xba, 0xfc, 0x18, 0x2e, 0x28, + 0x37, 0xe6, 0x6c, 0x5d, 0x06, 0x7c, 0x15, 0xfe, 0x9c, 0x0d, 0x93, 0x2e, 0xd6, 0x41, 0x92, + 0x85, 0xf9, 0x4b, 0x44, 0xa1, 0x99, 0x74, 0x48, 0xf6, 0xcf, 0x31, 0xed, 0xa2, 0x0b, 0x5e, + 0x3c, 0x13, 0xb4, 0xc7, 0xf7, 0xc2, 0xfb, 0x53, 0x2a, 0x65, 0xac, 0x14, 0x8d, 0x20, 0x59, + 0x7c, 0x1d, 0xf7, 0x7a, 0xa2, 0x3a, 0xfd, 0x05, 0x60, 0x96, 0x31, 0x5c, 0xb1, 0xc5, 0xea, + 0x5d, 0x4f, 0xeb, 0x1d, 0x1f, 0xbd, 0xeb, 0xe3, 0x3f, 0xba, 0x8c, 0x22, 0x85, 0xb4, 0xe5, + 0xa1, 0x15, 0xdf, + ], + c: &[ + 0x2a, 0xd3, 0x97, 0x6a, 0xe8, 0x42, 0x2e, 0x1a, 0x19, 0xe4, 0x4d, 0xa2, 0xb3, 0xf5, 0x63, + 0x2e, 0xc0, 0x19, 0x5a, 0x58, 0x46, 0xee, 0x82, 0x53, 0x06, 0x80, 0x28, 0x74, 0x37, 0x6e, + 0xd1, 0x54, 0x2a, 0x41, 0x46, 0x41, 0xc5, 0xf1, 0x25, 0xfa, 0x34, 0xf4, 0x80, 0x9d, 0x49, + 0x1d, 0x11, 0xb1, 0xb3, 0x22, 0xa6, 0x23, 0xe3, 0x92, 0x6f, 0x53, 0x61, 0xbe, 0x3d, 0xed, + 0xf6, 0x50, 0x17, 0xf5, 0x95, 0xf5, 0x5c, 0xa8, 0x6e, 0x27, 0x42, 0x69, 0x14, 0xc8, 0x64, + 0x14, 0x27, 0x95, 0xc9, 0xfa, 0x4d, 0xcb, 0xf9, 0xc5, 0x8c, 0x36, 0x28, 0x0c, 0x34, 0xdf, + 0x74, 0xfa, 0x38, 0x45, 0xc1, 0x73, 0xe0, 0xd9, 0x4d, 0xdc, 0xc4, 0xf3, 0x68, 0x05, 0x6e, + 0x1f, 0x90, 0x72, 0x93, 0x86, 0x67, 0xf8, 0xbc, 0xe5, 0xf6, 0x39, 0xd9, 0xfb, 0xfa, 0xc8, + 0x46, 0x6b, 0xf4, 0x8f, 0xec, 0xcb, 0x7e, 0x42, 0xaf, 0x6e, 0xee, 0x3d, 0xeb, 0xb8, 0x65, + 0xdd, 0x90, 0x91, 0x63, 0xd1, 0xd6, 0x68, 0xc1, 0x99, 0xbc, 0x0b, 0x73, 0xc1, 0xd2, 0x70, + 0xd9, 0x4b, 0x84, 0x26, 0xf0, 0x05, 0x8e, 0xe3, 0x11, 0x7a, 0xee, 0x9f, 0x33, 0xae, 0xcf, + 0xd5, 0xe7, 0xc3, 0x83, 0x15, 0x36, 0xcb, 0x4d, 0x3f, 0x56, 0xd7, 0x6b, 0xed, 0x19, 0x9b, + 0x53, 0xc6, 0xb3, 0xd7, 0xeb, 0x77, 0x44, 0x70, 0x02, 0xc2, 0x47, 0x97, 0x03, 0x88, 0x82, + 0x30, 0x0b, 0xac, 0xe3, 0xf3, 0x7d, 0x9c, 0x8b, 0xa9, 0x1b, 0x0d, 0x28, 0x65, 0xa7, 0x7f, + 0xbc, 0xc5, 0x31, 0x01, 0xc3, 0x4d, 0xe4, 0x7a, 0x18, 0x20, 0x10, 0x11, 0x7f, 0xcd, 0xf8, + 0x41, 0xbe, 0xab, 0xcf, 0x30, 0x75, 0x74, 0x89, 0x03, 0xec, 0xc1, 0x69, 0x35, 0x73, 0x15, + 0x0a, 0xd8, 0x80, 0xb7, 0x13, 0xa3, 0x52, 0x71, 0xdd, 0xd9, 0x41, 0x1d, 0x2d, 0x23, 0x63, + 0xc0, + ], + k: &[ + 0xdf, 0x26, 0xb1, 0x63, 0xc2, 0x78, 0x76, 0x7a, 0x79, 0x82, 0xa9, 0x89, 0x46, 0x2a, 0x38, + 0x65, 0x47, 0xb4, 0x9b, 0x0c, 0xe2, 0xa5, 0xe2, 0x9f, 0x71, 0xd9, 0xfe, 0x9c, 0x30, 0xe1, + 0x4e, 0x5e, 0x66, 0xa1, 0x6c, 0xd9, 0xd1, 0x28, 0x38, 0x90, 0x55, 0xd1, 0x23, 0xd9, 0xe7, + 0x9a, 0xbf, 0x54, 0xb5, 0xc2, 0x86, 0xa7, 0x9a, 0xa8, 0xe9, 0x7e, 0xc0, 0xe1, 0x9b, 0xc1, + 0x95, 0x9a, 0x1d, 0x15, 0xf1, 0x2f, 0x8c, 0x97, 0x87, 0x0b, 0xa9, 0xd6, 0x8c, 0xc1, 0x28, + 0x11, 0xa5, 0x6a, 0x3b, 0xb1, 0x14, 0x40, 0x61, 0x08, 0x25, 0x79, 0x6a, 0x49, 0xd4, 0x68, + 0xcd, 0xc9, 0xc2, 0xd0, 0x2d, 0x76, 0x59, 0x8a, 0x27, 0x97, 0x3d, 0x59, 0x60, 0xc5, 0xf5, + 0x0b, 0xce, 0x28, 0xd8, 0xd3, 0x45, 0xf4, 0x3c, 0xe5, 0x43, 0x25, 0xdb, 0x52, 0xfe, 0x79, + 0x88, 0x24, 0xae, 0xad, 0x11, 0xbb, 0x16, 0xfa, 0x76, 0x68, 0x57, 0xd0, 0x4a, 0x4a, 0xf7, + 0xd4, 0x68, 0x67, 0x2f, 0x16, 0xd9, 0x0e, 0x73, 0x96, 0x04, 0x6a, 0x46, 0xf8, 0x15, 0x69, + 0x3e, 0x85, 0xb1, 0xce, 0x54, 0x64, 0xda, 0x92, 0x70, 0x18, 0x1f, 0x82, 0x33, 0x3b, 0x07, + 0x15, 0x05, 0x7b, 0xbe, 0x8d, 0x61, 0xd4, 0x00, 0x50, 0x5f, 0x0e, 0x44, 0x87, 0x81, 0x78, + 0x2b, 0xf7, 0xc0, 0xe5, 0x2a, 0x1d, 0xd9, 0xe6, 0x75, 0x8f, 0xd3, 0x48, 0x2d, 0x90, 0xd3, + 0xcf, 0xcc, 0xf4, 0x22, 0x32, 0xcf, 0x35, 0x7e, 0x59, 0xa4, 0xd4, 0x9f, 0xd4, 0xd5, 0xf4, + 0x0c, 0x9e, 0x74, 0x33, 0x1e, 0x12, 0xc9, 0xcb, 0x53, 0x2c, 0x39, 0xe8, 0xd7, 0x02, 0x77, + 0x4a, 0x4f, 0x84, 0xf0, 0x1d, 0xe6, 0x72, 0x72, 0x16, 0x9c, 0x9d, 0x1e, 0xd1, 0xcd, 0x61, + 0x8f, 0x69, 0xff, 0x61, 0x49, 0x57, 0xef, 0x83, 0x66, 0x8e, 0xdc, 0x2d, 0x7e, 0xd6, 0x14, + 0xbf, + ], +}; From 4d1d7975132863881af5aa20b59ed85591ec5cb5 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 9 Jul 2026 13:51:02 -0700 Subject: [PATCH 07/19] Remove AES and SHA wake up after rebasing with main The issue is fixed on the mainline (SHA error interrupt was not enabled) --- fw/plat/uno/fw/pal/src/pal.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/fw/plat/uno/fw/pal/src/pal.rs b/fw/plat/uno/fw/pal/src/pal.rs index 1a2a4efe9..0b1e5abb9 100644 --- a/fw/plat/uno/fw/pal/src/pal.rs +++ b/fw/plat/uno/fw/pal/src/pal.rs @@ -531,18 +531,6 @@ 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. From b1cf6f9b52c8d725d879e22656564f0482deac0d Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 9 Jul 2026 14:38:22 -0700 Subject: [PATCH 08/19] Add ECDH x16 engine self tests --- fw/plat/uno/fw/pal/src/self_test/mod.rs | 3 + fw/plat/uno/fw/pal/src/self_test/pka.rs | 83 +++++++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/vectors.rs | 74 ++++++++++++++++++ 3 files changed, 160 insertions(+) diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index d21ddaa55..3709476c1 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -49,5 +49,8 @@ pub(crate) async fn run_pre_op(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> for engine in 0..pka::PKA_ENGINES { pka::run_rsa_mod_exp_crt_on_engine(pal, io, engine).await?; } + for engine in 0..pka::PKA_ENGINES { + pka::run_ecdh_on_engine(pal, io, engine).await?; + } Ok(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs index f6aaa5920..d0c2fa921 100644 --- a/fw/plat/uno/fw/pal/src/self_test/pka.rs +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -19,10 +19,13 @@ 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_drivers_upka::UpkaEccCurve; use azihsm_fw_uno_drivers_upka::UpkaRsaKeyType; use azihsm_fw_uno_reg_soc::io_gsram::UPKA_ENGINE_CMD_COUNT; use azihsm_fw_uno_trace::tracing::error; +use super::vectors::ECDH_384_KAT; +use super::vectors::ECDH_P384_PRIME_LE; use super::vectors::RSA_2K_CRT_KAT; use super::vectors::RSA_2K_MOD_EXP_KAT; use crate::UnoHsmIo; @@ -34,6 +37,9 @@ pub(super) const PKA_ENGINES: u8 = UPKA_ENGINE_CMD_COUNT as u8; /// RSA-2048 modulus size in bytes. const RSA_2K_LEN: usize = 256; +/// NIST P-384 field element / shared-secret size in bytes. +const ECDH_384_LEN: usize = 48; + /// Runs the RSA-2048 mod-exp (private-key) known-answer test on PKA engine /// `engine`. /// @@ -133,3 +139,80 @@ pub(super) async fn run_rsa_mod_exp_crt_on_engine( }) .await } + +/// Runs the ECDH P-384 (ECC CDH primitive) known-answer test on PKA engine +/// `engine`. +/// +/// Ported from the reference firmware's `ecdh_self_test`. Derives the shared +/// secret `z = d_iut * Q_cavs` via a single point multiplication on the pinned +/// engine (the driver performs the required per-call Montgomery-constant setup +/// from the curve prime) and compares against the expected `z_iut`. +/// +/// # Endianness +/// +/// The peer public key (`qcavs_x ‖ qcavs_y`), private scalar (`d_iut`), and +/// curve prime are little-endian, matching the PKA operand order, and are fed +/// verbatim. The engine emits the shared secret little-endian; the expected +/// `z_iut` is big-endian, so the little-endian engine output is reversed before +/// comparison (mirroring the production `ecdh_derive`). +/// +/// Returns [`HsmError::SelfTestKatMismatch`] on a mismatch, or any error +/// surfaced by the PKA engine / allocator. +pub(super) async fn run_ecdh_on_engine( + pal: &UnoHsmPal, + io: &UnoHsmIo, + engine: u8, +) -> HsmResult<()> { + let v = &ECDH_384_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // Peer public key as a contiguous `X ‖ Y` block (little-endian). + let pub_key = scope.dma_alloc(ECDH_384_LEN * 2)?; + pub_key[..ECDH_384_LEN].copy_from_slice(v.qcavs_x); + pub_key[ECDH_384_LEN..].copy_from_slice(v.qcavs_y); + // Private scalar (little-endian). + let priv_key = scope.dma_alloc(ECDH_384_LEN)?; + priv_key.copy_from_slice(v.d_iut); + // Curve prime and Montgomery-constant scratch for the point-multiply. + let prime = scope.dma_alloc(ECDH_384_LEN)?; + prime.copy_from_slice(ECDH_P384_PRIME_LE); + let mont_result = scope.dma_alloc(ECDH_384_LEN)?; + // Derived shared-secret output (little-endian). + let secret = scope.dma_alloc_zeroed(ECDH_384_LEN)?; + // Status word for the hardware on-curve validation the driver runs + // between the Montgomery-constant setup and the ECDH compute. + let point_valid = scope.dma_alloc(4)?; + + // Pin the requested engine for the operation, release afterwards. + let mut eng = pal.pka.acquire_engine(engine).await?; + let outcome = eng + .ecdh_derive( + UpkaEccCurve::P384, + &*priv_key, + &*pub_key, + &mut *secret, + &*prime, + &mut *mont_result, + &mut *point_valid, + ) + .await; + let release = eng.release().await; + outcome?; + release?; + + // The engine writes the secret little-endian; `z_iut` is big-endian. + // Compare the reversed output against the expected big-endian value. + let matches = secret[..ECDH_384_LEN].iter().rev().eq(v.z_iut.iter()); + if !matches { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "ECDH P-384 KAT mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 2ab11fa1f..8304b4e7e 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -426,3 +426,77 @@ pub(super) const RSA_2K_CRT_KAT: Rsa2kCrtKat = Rsa2kCrtKat { 0xbf, ], }; + +/// NIST P-384 curve prime modulus in PKA little-endian operand order. +/// +/// Required by the ECDH point-multiply (the per-call Montgomery-constant setup +/// reads it). Identical to `crypto::ecc::PRIME384_LE`; duplicated here to keep +/// the self-test module self-contained (the constant is the immutable NIST +/// P-384 prime and cannot drift). +pub(super) const ECDH_P384_PRIME_LE: &[u8] = &[ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, + 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +]; + +/// ECDH P-384 (ECC CDH primitive) known-answer test vector. +/// +/// Ported from the reference firmware's `KAT_ECDH_TEST_384_TEST_VECTORS` +/// (ECCCDH primitive test vectors, NIST CAVP component testing). The shared +/// secret is computed as `z = d_iut * Q_cavs` (a single point multiplication) +/// and compared against `z_iut`. +/// +/// # Endianness +/// +/// The peer public-key coordinates (`qcavs_x`, `qcavs_y`) and the private +/// scalar (`d_iut`) are **little-endian** — the PKA engine's native operand +/// order — and are fed to the driver verbatim. The expected shared secret +/// `z_iut` is **big-endian**, byte-for-byte identical to the reference vector +/// (which stores it in vault order). The PKA point-multiply emits the secret +/// little-endian, so the runner reverses the engine output to big-endian before +/// comparing (mirroring the production `ecdh_derive`). +pub(super) struct Ecdh384Kat { + /// Peer public-key X coordinate (48 bytes, little-endian). + pub qcavs_x: &'static [u8], + + /// Peer public-key Y coordinate (48 bytes, little-endian). + pub qcavs_y: &'static [u8], + + /// Local private scalar (48 bytes, little-endian). + pub d_iut: &'static [u8], + + /// Expected shared secret (48 bytes, big-endian). + pub z_iut: &'static [u8], +} + +/// ECDH P-384 known-answer test vector. +/// +/// Byte-for-byte identical to the reference firmware's +/// `KAT_ECDH_TEST_384_TEST_VECTORS` (first 48 bytes of each field; the reference +/// arrays are padded to the P-521 maximum length). +pub(super) const ECDH_384_KAT: Ecdh384Kat = Ecdh384Kat { + qcavs_x: &[ + 0x66, 0x00, 0xc5, 0xe0, 0x58, 0x8c, 0xd6, 0xf2, 0xdb, 0x1a, 0x09, 0x00, 0xb4, 0x66, 0x44, + 0x73, 0x72, 0x32, 0x51, 0xe7, 0x7f, 0xa2, 0xfd, 0x2e, 0x59, 0x64, 0xe7, 0x52, 0xaf, 0x9e, + 0x7b, 0x69, 0x47, 0xab, 0x04, 0xae, 0x38, 0x28, 0x5d, 0xb0, 0xe8, 0x5f, 0x3b, 0x0c, 0x97, + 0x6b, 0xc7, 0xa7, + ], + qcavs_y: &[ + 0x7a, 0x43, 0xc8, 0x6f, 0x76, 0x64, 0x8c, 0x46, 0x2b, 0x99, 0x60, 0xb0, 0x32, 0x5a, 0x90, + 0xd0, 0x5e, 0x91, 0x51, 0x34, 0x24, 0xdf, 0xfe, 0x1e, 0x66, 0x9b, 0x74, 0x49, 0x8a, 0xc3, + 0xc4, 0x39, 0x08, 0x1a, 0xb9, 0x69, 0x99, 0x3a, 0xd4, 0xae, 0x79, 0xb8, 0x1c, 0x2e, 0x9f, + 0xf1, 0x68, 0xac, + ], + d_iut: &[ + 0xa1, 0x98, 0xcf, 0xc9, 0x43, 0x4d, 0xab, 0x99, 0xf6, 0x8c, 0xa8, 0x5d, 0x20, 0x3b, 0x46, + 0xad, 0x74, 0x17, 0x66, 0x8a, 0x81, 0xb6, 0x18, 0x56, 0xb1, 0xe1, 0x22, 0x4d, 0x89, 0x38, + 0x8c, 0xeb, 0xa0, 0x6b, 0x91, 0x67, 0xc0, 0x38, 0xad, 0x27, 0x50, 0xd9, 0xf0, 0x68, 0x2a, + 0x12, 0xc3, 0x3c, + ], + z_iut: &[ + 0x5f, 0x9d, 0x29, 0xdc, 0x5e, 0x31, 0xa1, 0x63, 0x06, 0x03, 0x56, 0x21, 0x36, 0x69, 0xc8, + 0xce, 0x13, 0x2e, 0x22, 0xf5, 0x7c, 0x9a, 0x04, 0xf4, 0x0b, 0xa7, 0xfc, 0xea, 0xd4, 0x93, + 0xb4, 0x57, 0xe5, 0x62, 0x1e, 0x76, 0x6c, 0x40, 0xa2, 0xe3, 0xd4, 0xd6, 0xa0, 0x4b, 0x25, + 0xe5, 0x33, 0xf1, + ], +}; From b1bd40ca0568d1b1e1ca55890476ce06804bd881 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Thu, 9 Jul 2026 15:13:43 -0700 Subject: [PATCH 09/19] Add OAEP-decode to the RSA mod exp selftest --- fw/plat/uno/fw/pal/src/self_test/pka.rs | 88 ++++++++++++++++++++- fw/plat/uno/fw/pal/src/self_test/vectors.rs | 13 +++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs index d0c2fa921..17b7fa5db 100644 --- a/fw/plat/uno/fw/pal/src/self_test/pka.rs +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -17,6 +17,9 @@ use azihsm_fw_hsm_pal_traits::HsmAlloc; 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::HsmKdf; use azihsm_fw_hsm_pal_traits::HsmResult; use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; use azihsm_fw_uno_drivers_upka::UpkaEccCurve; @@ -26,6 +29,7 @@ use azihsm_fw_uno_trace::tracing::error; use super::vectors::ECDH_384_KAT; use super::vectors::ECDH_P384_PRIME_LE; +use super::vectors::OAEP_KEK_SELF_TEST; use super::vectors::RSA_2K_CRT_KAT; use super::vectors::RSA_2K_MOD_EXP_KAT; use crate::UnoHsmIo; @@ -41,11 +45,14 @@ const RSA_2K_LEN: usize = 256; const ECDH_384_LEN: usize = 48; /// Runs the RSA-2048 mod-exp (private-key) known-answer test on PKA engine -/// `engine`. +/// `engine`, followed by the OAEP-decode (SHA-256) KEK check. /// /// Computes `c^d mod n` on the pinned engine and compares the result against -/// the expected plaintext. Returns [`HsmError::SelfTestKatMismatch`] on a -/// mismatch, or any error surfaced by the PKA engine / allocator. +/// the expected plaintext, then OAEP-decodes that plaintext block and compares +/// the recovered KEK against the expected value (mirroring the reference +/// firmware's `rsa_mod_exp_self_test` + `decode_oaep_kek_self_test`). Returns +/// [`HsmError::SelfTestKatMismatch`] on a mismatch, or any error surfaced by +/// the PKA / SHA engine or the allocator. pub(super) async fn run_rsa_mod_exp_on_engine( pal: &UnoHsmPal, io: &UnoHsmIo, @@ -81,6 +88,81 @@ pub(super) async fn run_rsa_mod_exp_on_engine( return Err(HsmError::SelfTestKatMismatch); } + // ── OAEP-decode tail (SHA-256, empty label) ────────────────────────── + // Ported from the reference `decode_oaep_kek_self_test`: the mod-exp + // output block is little-endian, so it is reversed into the big-endian + // OAEP-encoded message `EM = 0x00 ‖ maskedSeed ‖ maskedDB`, then + // OAEP-decoded with the SHA/MGF1 primitives (identical steps to the + // production `rsa_oaep_decrypt`) and the recovered 16-byte KEK is + // compared against the expected value. This also exercises SHA-256 and + // MGF1 on the HSM SHA engine. + const H_LEN: usize = 32; // SHA-256 digest length. + let em = scope.dma_alloc(RSA_2K_LEN)?; + for (i, &b) in output[..RSA_2K_LEN].iter().enumerate() { + em[RSA_2K_LEN - 1 - i] = b; + } + + if em[0] != 0x00 { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K OAEP leading byte nonzero on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + // Recover seed: seed = maskedSeed XOR MGF(maskedDB, hLen). + { + let (seed, db) = em[1..RSA_2K_LEN].split_at_mut(H_LEN); + pal.mgf1_xor(io, HsmHashAlgo::Sha256, db, seed).await?; + } + // Recover DB: DB = maskedDB XOR MGF(seed, dbLen). + { + let (seed, db) = em[1..RSA_2K_LEN].split_at_mut(H_LEN); + pal.mgf1_xor(io, HsmHashAlgo::Sha256, seed, db).await?; + } + + // Verify lHash' == SHA-256(empty label). + let label_hash = scope.dma_alloc(H_LEN)?; + let empty = &input[..0]; + pal.hash(io, HsmHashAlgo::Sha256, empty, label_hash, true) + .await?; + let db = &em[1 + H_LEN..RSA_2K_LEN]; + let db_hash: &[u8] = &db[..H_LEN]; + let label_hash_slice: &[u8] = &label_hash[..H_LEN]; + if db_hash != label_hash_slice { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K OAEP lHash mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + // DB = lHash' ‖ PS(0x00…) ‖ 0x01 ‖ M — locate the 0x01 separator, then + // recover and check the message M against the expected KEK. + let ps_and_m = &db[H_LEN..]; + let sep = match ps_and_m.iter().position(|&x| x == 0x01) { + Some(s) if ps_and_m[..s].iter().all(|&x| x == 0x00) => s, + _ => { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K OAEP separator invalid on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + }; + let kek: &[u8] = &db[H_LEN + sep + 1..]; + if kek != OAEP_KEK_SELF_TEST { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "RSA-2K OAEP KEK mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + Ok::<(), HsmError>(()) }) .await diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 8304b4e7e..cfc478d92 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -266,6 +266,19 @@ pub(super) const RSA_2K_MOD_EXP_KAT: RsaModExpKat = RsaModExpKat { ], }; +/// Expected KEK recovered by OAEP-decoding (SHA-256, empty label) the RSA-2048 +/// mod-exp plaintext block [`RSA_2K_MOD_EXP_KAT`]`.k`. +/// +/// This is the tail of the reference firmware's RSA mod-exp self-test: after the +/// PKA mod-exp produces the padded plaintext, the SHA engine OAEP-decodes it and +/// the recovered key-encryption key is compared against this constant. Byte-for- +/// byte identical to the reference firmware's `OAEP_KEK_SELF_TEST` +/// (`drivers/crypto/sha/src/sha.rs`). Natural (big-endian) byte order — it is +/// extracted from the big-endian OAEP-encoded message. +pub(super) const OAEP_KEK_SELF_TEST: &[u8] = &[ + 0xf0, 0x81, 0x50, 0x2a, 0xe5, 0x33, 0x54, 0x59, 0x95, 0xdf, 0x87, 0xc4, 0x2a, 0x19, 0x20, 0x21, +]; + /// RSA-2048 CRT modular-exponentiation (private-key) known-answer vector. /// /// Exercises the raw RSA-CRT private-key operation on a PKA engine: the From 12b36803d4dbef1e3de16725ecb7471e4b857176 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Fri, 10 Jul 2026 12:37:42 -0700 Subject: [PATCH 10/19] Port ECDSA x16 engine self tests --- fw/plat/uno/fw/drivers/upka/src/engine.rs | 242 ++++++++++++++++++++ fw/plat/uno/fw/drivers/upka/src/opcode.rs | 93 ++++++++ fw/plat/uno/fw/pal/src/self_test/mod.rs | 7 +- fw/plat/uno/fw/pal/src/self_test/pka.rs | 164 +++++++++++++ fw/plat/uno/fw/pal/src/self_test/vectors.rs | 96 ++++++++ 5 files changed, 600 insertions(+), 2 deletions(-) diff --git a/fw/plat/uno/fw/drivers/upka/src/engine.rs b/fw/plat/uno/fw/drivers/upka/src/engine.rs index 584a5fc42..8842bb2e2 100644 --- a/fw/plat/uno/fw/drivers/upka/src/engine.rs +++ b/fw/plat/uno/fw/drivers/upka/src/engine.rs @@ -379,6 +379,248 @@ impl UpkaEngine<'_, DEPTH, ENGINES> { .await } + /// Compute the Montgomery constant for `modulus` on this engine. + /// + /// Establishes the active modulus for the subsequent field operations on the + /// same engine acquisition (`execute_cmd` does not wipe engine state between + /// commands). `result` receives the natural-form scratch output. + /// + /// This is a low-level primitive intended for orchestrating a raw ECDSA + /// sign sequence (e.g. the FIPS self-test); production ECDSA/ECDH use the + /// composite [`ecc_sign`](Self::ecc_sign) / [`ecdh_derive`](Self::ecdh_derive) + /// helpers. + /// + /// # Returns + /// + /// - `Ok(())`: Montgomery constant established for `modulus`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mont_const_calc( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + modulus: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= point_size(curve) && modulus.len() >= point_size(curve), + )?; + self.field_unary(mont_const_calc_opcode(curve), result, modulus) + .await + } + + /// Multiply an affine point by a scalar (`result = scalar · point`). + /// + /// `point_xy` is the contiguous `X ‖ Y` affine point (each coordinate + /// `point_size` bytes, LE); `scalar` is `point_size` bytes (LE). `result` + /// receives the X coordinate of the product (`point_size` bytes, LE). + /// + /// # Returns + /// + /// - `Ok(())`: Product X coordinate was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_point_mul( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + point_xy: &DmaBuf, + scalar: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= point_size(curve) + && point_xy.len() >= point_size(curve) * 2 + && scalar.len() >= point_size(curve), + )?; + self.execute_cmd( + ecc_point_mul_opcode(curve), + result.as_mut_ptr() as u32, + point_xy.as_ptr() as u32, + scalar.as_ptr() as u32, + 0, + ) + .await + } + + /// Reduce a natural-form field element modulo the active modulus. + /// + /// `arg` is a **double-width** (`2 · point_size` bytes, LE) zero-padded + /// dividend: the value to reduce occupies the low `point_size` bytes and the + /// high `point_size` bytes must be zero. The hardware reduction reads the + /// full `2 · point_size` window, so a tight `point_size` buffer would let it + /// read adjacent memory as the high half and produce garbage. `result` is + /// `point_size` bytes (LE). Requires a preceding + /// [`ecc_mont_const_calc`](Self::ecc_mont_const_calc) for the target + /// modulus on the same engine acquisition. + /// + /// # Returns + /// + /// - `Ok(())`: Reduced value was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mod_reduction( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + arg: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= point_size(curve) && arg.len() >= point_size(curve) * 2, + )?; + self.field_unary(ecc_mod_reduction_opcode(curve), result, arg) + .await + } + + /// Convert a natural-form field element into Montgomery form. + /// + /// `arg` is `point_size` bytes (LE); `result` is `montgomery_size` bytes. + /// + /// # Returns + /// + /// - `Ok(())`: Montgomery-form value was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mont_repr_in( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + arg: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= montgomery_size(curve) && arg.len() >= point_size(curve), + )?; + self.field_unary(ecc_mont_repr_in_opcode(curve), result, arg) + .await + } + + /// Convert a Montgomery-form field element back into natural form. + /// + /// `arg` is `montgomery_size` bytes; `result` is `point_size` bytes (LE). + /// + /// # Returns + /// + /// - `Ok(())`: Natural-form value was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mont_repr_out( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + arg: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= point_size(curve) && arg.len() >= montgomery_size(curve), + )?; + self.field_unary(ecc_mont_repr_out_opcode(curve), result, arg) + .await + } + + /// Compute the modular inverse of a Montgomery-form field element. + /// + /// `arg` and `result` are `montgomery_size` bytes. + /// + /// # Returns + /// + /// - `Ok(())`: Inverse was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mod_inverse( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + arg: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= montgomery_size(curve) && arg.len() >= montgomery_size(curve), + )?; + self.field_unary(ecc_mod_inverse_opcode(curve), result, arg) + .await + } + + /// Multiply two Montgomery-form field elements modulo the active modulus. + /// + /// `a`, `b` and `result` are `montgomery_size` bytes. + /// + /// # Returns + /// + /// - `Ok(())`: Product was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mod_mul( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + a: &DmaBuf, + b: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= montgomery_size(curve) + && a.len() >= montgomery_size(curve) + && b.len() >= montgomery_size(curve), + )?; + self.field_binary(ecc_mod_mul_opcode(curve), result, a, b) + .await + } + + /// Add two Montgomery-form field elements modulo the active modulus. + /// + /// `a`, `b` and `result` are `montgomery_size` bytes. + /// + /// # Returns + /// + /// - `Ok(())`: Sum was written to `result`. + /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware + /// rejected the command. + pub async fn ecc_mod_add( + &mut self, + curve: UpkaEccCurve, + result: &mut DmaBuf, + a: &DmaBuf, + b: &DmaBuf, + ) -> HsmResult<()> { + Self::ensure_cmd_input( + result.len() >= montgomery_size(curve) + && a.len() >= montgomery_size(curve) + && b.len() >= montgomery_size(curve), + )?; + self.field_binary(ecc_mod_add_opcode(curve), result, a, b) + .await + } + + /// Submit a unary field operation (`result = op(arg)`). + async fn field_unary( + &mut self, + opcode: u32, + result: &mut DmaBuf, + arg: &DmaBuf, + ) -> HsmResult<()> { + self.execute_cmd( + opcode, + result.as_mut_ptr() as u32, + arg.as_ptr() as u32, + 0, + 0, + ) + .await + } + + /// Submit a binary field operation (`result = op(a, b)`). + async fn field_binary( + &mut self, + opcode: u32, + result: &mut DmaBuf, + a: &DmaBuf, + b: &DmaBuf, + ) -> HsmResult<()> { + self.execute_cmd( + opcode, + result.as_mut_ptr() as u32, + a.as_ptr() as u32, + b.as_ptr() as u32, + 0, + ) + .await + } + /// Run an RSA private-key modular exponentiation. /// /// `key_type` selects both modulus size and key format (standard or CRT). diff --git a/fw/plat/uno/fw/drivers/upka/src/opcode.rs b/fw/plat/uno/fw/drivers/upka/src/opcode.rs index 3bd94bf0f..107ba0c24 100644 --- a/fw/plat/uno/fw/drivers/upka/src/opcode.rs +++ b/fw/plat/uno/fw/drivers/upka/src/opcode.rs @@ -232,6 +232,76 @@ pub(crate) fn ecc_point_mul_opcode(curve: UpkaEccCurve) -> u32 { } } +/// Return the modular-multiplication opcode for the selected curve. +/// +/// Multiplies two Montgomery-form field elements modulo the active modulus that +/// was established by the preceding `mont_const_calc`. +pub(crate) fn ecc_mod_mul_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MOD_MULTIPLICATION_256, + UpkaEccCurve::P384 => MOD_MULTIPLICATION_384, + UpkaEccCurve::P521 => MOD_MULTIPLICATION_521, + } +} + +/// Return the modular-addition opcode for the selected curve. +/// +/// Adds two Montgomery-form field elements modulo the active modulus. +pub(crate) fn ecc_mod_add_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MOD_ADDITION_256, + UpkaEccCurve::P384 => MOD_ADDITION_384, + UpkaEccCurve::P521 => MOD_ADDITION_521, + } +} + +/// Return the modular-inverse opcode for the selected curve. +/// +/// Computes the multiplicative inverse of a Montgomery-form field element modulo +/// the active modulus. +pub(crate) fn ecc_mod_inverse_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MOD_INVERSE_256, + UpkaEccCurve::P384 => MOD_INVERSE_384, + UpkaEccCurve::P521 => MOD_INVERSE_521, + } +} + +/// Return the modular-reduction opcode for the selected curve. +/// +/// Reduces a natural-form field element modulo the active modulus. +pub(crate) fn ecc_mod_reduction_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MOD_REDUCTION_256, + UpkaEccCurve::P384 => MOD_REDUCTION_384, + UpkaEccCurve::P521 => MOD_REDUCTION_521, + } +} + +/// Return the Montgomery-representation-in opcode for the selected curve. +/// +/// Converts a natural-form field element into Montgomery form for the active +/// modulus (`point_size` bytes in, `montgomery_size` bytes out). +pub(crate) fn ecc_mont_repr_in_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MONT_REPR_IN_256, + UpkaEccCurve::P384 => MONT_REPR_IN_384, + UpkaEccCurve::P521 => MONT_REPR_IN_521, + } +} + +/// Return the Montgomery-representation-out opcode for the selected curve. +/// +/// Converts a Montgomery-form field element back into natural form for the +/// active modulus (`montgomery_size` bytes in, `point_size` bytes out). +pub(crate) fn ecc_mont_repr_out_opcode(curve: UpkaEccCurve) -> u32 { + match curve { + UpkaEccCurve::P256 => MONT_REPR_OUT_256, + UpkaEccCurve::P384 => MONT_REPR_OUT_384, + UpkaEccCurve::P521 => MONT_REPR_OUT_521, + } +} + /// Return the ECC point-validation opcode for the selected curve. /// /// # Parameters @@ -283,6 +353,29 @@ pub(crate) fn point_size(curve: UpkaEccCurve) -> usize { } } +/// Return the Montgomery-form field-element size for the selected ECC curve. +/// +/// Montgomery intermediates carry extra guard words versus the natural +/// `point_size` representation. +/// +/// # Parameters +/// +/// - `curve`: ECC curve selector. +/// +/// # Returns +/// +/// - Montgomery representation size in bytes. +/// - P256: 36 +/// - P384: 52 +/// - P521: 72 +pub(crate) fn montgomery_size(curve: UpkaEccCurve) -> usize { + match curve { + UpkaEccCurve::P256 => 36, + UpkaEccCurve::P384 => 52, + UpkaEccCurve::P521 => 72, + } +} + /// Return the HSM wire-format coordinate size for the selected ECC curve. /// /// # Parameters diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index 3709476c1..81b57fc61 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -18,8 +18,8 @@ //! //! # Status //! -//! AES-256-CBC, HKDF, KBKDF, and per-engine RSA-2048 mod-exp (standard and CRT) -//! are implemented. Further tests (per-engine ECDSA / ECDH, and the RNG/DRBG +//! AES-256-CBC, HKDF, KBKDF, and per-engine RSA-2048 mod-exp (standard and CRT), +//! ECDH-P384, and ECDSA-P384 are implemented. Further tests (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. @@ -52,5 +52,8 @@ pub(crate) async fn run_pre_op(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> for engine in 0..pka::PKA_ENGINES { pka::run_ecdh_on_engine(pal, io, engine).await?; } + for engine in 0..pka::PKA_ENGINES { + pka::run_ecdsa_on_engine(pal, io, engine).await?; + } Ok(()) } diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs index 17b7fa5db..4f758f7f0 100644 --- a/fw/plat/uno/fw/pal/src/self_test/pka.rs +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -29,6 +29,10 @@ use azihsm_fw_uno_trace::tracing::error; use super::vectors::ECDH_384_KAT; use super::vectors::ECDH_P384_PRIME_LE; +use super::vectors::ECDSA_384_SIGN_KAT; +use super::vectors::ECDSA_P384_BASE_X_LE; +use super::vectors::ECDSA_P384_BASE_Y_LE; +use super::vectors::ECDSA_P384_ORDER_LE; use super::vectors::OAEP_KEK_SELF_TEST; use super::vectors::RSA_2K_CRT_KAT; use super::vectors::RSA_2K_MOD_EXP_KAT; @@ -44,6 +48,12 @@ const RSA_2K_LEN: usize = 256; /// NIST P-384 field element / shared-secret size in bytes. const ECDH_384_LEN: usize = 48; +/// NIST P-384 field-element size in bytes (point coordinate / scalar / digest). +const P384_FIELD_LEN: usize = 48; + +/// NIST P-384 Montgomery-form intermediate size in bytes. +const P384_MONT_LEN: usize = 52; + /// Runs the RSA-2048 mod-exp (private-key) known-answer test on PKA engine /// `engine`, followed by the OAEP-decode (SHA-256) KEK check. /// @@ -298,3 +308,157 @@ pub(super) async fn run_ecdh_on_engine( }) .await } + +/// Runs the ECDSA P-384 deterministic sign known-answer test on PKA engine +/// `engine`. +/// +/// Ported as-is from the reference firmware's `ecdsa_self_test_internal`: a +/// fixed-nonce ECDSA sign KAT built from the raw PKA Montgomery primitives +/// (point-multiply, modular reduction / inverse / multiply / add, and +/// Montgomery in/out conversions), reproducing the signature `(r, s)` for a +/// fixed private key `d`, nonce `k`, and message digest `e`, then comparing it +/// against the reference vectors. Running the full primitive sequence — rather +/// than the composite random-nonce `ecc_sign` — is required so the KAT is +/// deterministic and matches the FIPS submission exactly. +/// +/// The 12-step sequence (per engine) is: +/// 1. Montgomery-constant setup for the curve prime `p`. +/// 2. `R = k · G`; take the affine x-coordinate `x_R`. +/// 3. Montgomery-constant setup for the group order `n`. +/// 4. `r = x_R mod n` (must be non-zero). +/// 5. Convert `k`, `r`, `e`, `d` into Montgomery form (mod `n`). +/// 6. `k⁻¹ mod n`. +/// 7. `s = k⁻¹ · (e + r·d) mod n` (must be non-zero). +/// 8. Compare `(r, s)` against the expected signature. +/// +/// # Endianness +/// +/// All operands and vectors are little-endian (PKA-native) and fed verbatim; +/// the resulting `r` / `s` are compared against the little-endian expected +/// values with no reversal. +/// +/// Returns [`HsmError::SelfTestKatMismatch`] on a mismatch (or a degenerate +/// `r` / `s`), or any error surfaced by the PKA engine / allocator. +pub(super) async fn run_ecdsa_on_engine( + pal: &UnoHsmPal, + io: &UnoHsmIo, + engine: u8, +) -> HsmResult<()> { + let v = &ECDSA_384_SIGN_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // Curve prime and group order (little-endian). `mont_scratch` receives + // the Montgomery-constant output; it is reused for both the prime and + // the order setups since only the engine-side state is consumed. + let prime = scope.dma_alloc(P384_FIELD_LEN)?; + prime.copy_from_slice(ECDH_P384_PRIME_LE); + let order_n = scope.dma_alloc(P384_FIELD_LEN)?; + order_n.copy_from_slice(ECDSA_P384_ORDER_LE); + let mont_scratch = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; + + // Base point `G` as a contiguous `X ‖ Y` block (little-endian). + let base_xy = scope.dma_alloc(P384_FIELD_LEN * 2)?; + base_xy[..P384_FIELD_LEN].copy_from_slice(ECDSA_P384_BASE_X_LE); + base_xy[P384_FIELD_LEN..].copy_from_slice(ECDSA_P384_BASE_Y_LE); + + // Fixed nonce `k`, message digest `e`, private key `d` (little-endian). + let k = scope.dma_alloc(P384_FIELD_LEN)?; + k.copy_from_slice(v.k); + let digest = scope.dma_alloc(P384_FIELD_LEN)?; + digest.copy_from_slice(v.digest); + let priv_key = scope.dma_alloc(P384_FIELD_LEN)?; + priv_key.copy_from_slice(v.private_key); + + // Intermediate and output buffers. + let x_r = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; + // Modular reduction is a double-width primitive: the hardware reads a + // `2 · point_size` dividend. `x_r_wide` holds x_R in its low half with a + // zeroed high half so the reduction sees `x_R ‖ 0`. This mirrors the + // reference firmware, which reduces out of a zeroized scratch buffer. + let x_r_wide = scope.dma_alloc_zeroed(P384_FIELD_LEN * 2)?; + let r = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; + let s = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; + let k_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let r_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let e_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let d_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let k_mont_inverse = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let s_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let t_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let t_mont_dot_r = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + let s_mont_plus_t = scope.dma_alloc_zeroed(P384_MONT_LEN)?; + + // Pin the requested engine for the operation, release afterwards. + let mut eng = pal.pka.acquire_engine(engine).await?; + let outcome = async { + let curve = UpkaEccCurve::P384; + + // 1. Establish the prime modulus, then R = k · G (x-coordinate). + eng.ecc_mont_const_calc(curve, &mut *mont_scratch, &*prime) + .await?; + eng.ecc_point_mul(curve, &mut *x_r, &*base_xy, &*k).await?; + + // 2. Switch modulus to the group order n, then r = x_R mod n. Stage + // x_R into the zero-padded double-width reduction input first. + x_r_wide[..P384_FIELD_LEN].copy_from_slice(&*x_r); + eng.ecc_mont_const_calc(curve, &mut *mont_scratch, &*order_n) + .await?; + eng.ecc_mod_reduction(curve, &mut *r, &*x_r_wide).await?; + if r.iter().all(|&b| b == 0) { + return Err(HsmError::SelfTestKatMismatch); + } + + // 3. Convert k, r, e, d into Montgomery form (mod n). + eng.ecc_mont_repr_in(curve, &mut *k_mont, &*k).await?; + eng.ecc_mont_repr_in(curve, &mut *r_mont, &*r).await?; + eng.ecc_mont_repr_in(curve, &mut *e_mont, &*digest).await?; + eng.ecc_mont_repr_in(curve, &mut *d_mont, &*priv_key) + .await?; + + // 4. k⁻¹ mod n. + eng.ecc_mod_inverse(curve, &mut *k_mont_inverse, &*k_mont) + .await?; + + // 5. s = k⁻¹ · (e + r·d) mod n, computed as: + // s_mont = k⁻¹ · e + // t_mont = k⁻¹ · d + // t_mont_dot_r = t_mont · r + // s_mont_plus_t = s_mont + t_mont_dot_r + eng.ecc_mod_mul(curve, &mut *s_mont, &*k_mont_inverse, &*e_mont) + .await?; + eng.ecc_mod_mul(curve, &mut *t_mont, &*k_mont_inverse, &*d_mont) + .await?; + eng.ecc_mod_mul(curve, &mut *t_mont_dot_r, &*t_mont, &*r_mont) + .await?; + eng.ecc_mod_add(curve, &mut *s_mont_plus_t, &*s_mont, &*t_mont_dot_r) + .await?; + + // 6. Convert s back to natural form. + eng.ecc_mont_repr_out(curve, &mut *s, &*s_mont_plus_t) + .await?; + if s.iter().all(|&b| b == 0) { + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + } + .await; + let release = eng.release().await; + outcome?; + release?; + + // 7. Compare (r, s) against the expected signature (little-endian, no + // reversal). + if &r[..] != v.r || &s[..] != v.s { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "ECDSA P-384 sign KAT mismatch on engine" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index cfc478d92..3967bc175 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -513,3 +513,99 @@ pub(super) const ECDH_384_KAT: Ecdh384Kat = Ecdh384Kat { 0xe5, 0x33, 0xf1, ], }; + +/// NIST P-384 base point G, X coordinate, in PKA little-endian operand order. +/// +/// Byte-for-byte identical to the reference firmware's `BASE384.x[0..48]`. Used +/// by the ECDSA sign self-test point multiplication `k · G`. +pub(super) const ECDSA_P384_BASE_X_LE: &[u8] = &[ + 0xb7, 0x0a, 0x76, 0x72, 0x38, 0x5e, 0x54, 0x3a, 0x6c, 0x29, 0x55, 0xbf, 0x5d, 0xf2, 0x02, 0x55, + 0x38, 0x2a, 0x54, 0x82, 0xe0, 0x41, 0xf7, 0x59, 0x98, 0x9b, 0xa7, 0x8b, 0x62, 0x3b, 0x1d, 0x6e, + 0x74, 0xad, 0x20, 0xf3, 0x1e, 0xc7, 0xb1, 0x8e, 0x37, 0x05, 0x8b, 0xbe, 0x22, 0xca, 0x87, 0xaa, +]; + +/// NIST P-384 base point G, Y coordinate, in PKA little-endian operand order. +/// +/// Byte-for-byte identical to the reference firmware's `BASE384.y[0..48]`. Used +/// by the ECDSA sign self-test point multiplication `k · G`. +pub(super) const ECDSA_P384_BASE_Y_LE: &[u8] = &[ + 0x5f, 0x0e, 0xea, 0x90, 0x7c, 0x1d, 0x43, 0x7a, 0x9d, 0x81, 0x7e, 0x1d, 0xce, 0xb1, 0x60, 0x0a, + 0xc0, 0xb8, 0xf0, 0xb5, 0x13, 0x31, 0xda, 0xe9, 0x7c, 0x14, 0x9a, 0x28, 0xbd, 0x1d, 0xf4, 0xf8, + 0x29, 0xdc, 0x92, 0x92, 0xbf, 0x98, 0x9e, 0x5d, 0x6f, 0x2c, 0x26, 0x96, 0x4a, 0xde, 0x17, 0x36, +]; + +/// NIST P-384 group order n in PKA little-endian operand order. +/// +/// Byte-for-byte identical to the reference firmware's `ORDER384.n[0..48]`. The +/// ECDSA sign self-test establishes it as the active modulus (via a +/// Montgomery-constant setup) for the `mod n` steps. +pub(super) const ECDSA_P384_ORDER_LE: &[u8] = &[ + 0x73, 0x29, 0xc5, 0xcc, 0x6a, 0x19, 0xec, 0xec, 0x7a, 0xa7, 0xb0, 0x48, 0xb2, 0x0d, 0x1a, 0x58, + 0xdf, 0x2d, 0x37, 0xf4, 0x81, 0x4d, 0x63, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +]; + +/// ECDSA P-384 (SHA-256 component) deterministic sign known-answer test vector. +/// +/// Ported verbatim from the reference firmware's `KAT_ECC_TEST_384_TEST_VECTORS` +/// (NIST CAVP 186-3 ECDSA SigGen component test vectors, `[P-384,SHA-256]`, +/// first 48 bytes of each field; the reference arrays are padded to the P-521 +/// maximum length). The self-test replays the reference's raw-primitive sign +/// sequence with the **fixed** nonce `k` and compares the computed `(r, s)` +/// against these expected values. +/// +/// # Endianness +/// +/// All fields are **little-endian** — the PKA engine's native operand order — +/// and are fed to the driver verbatim. The computed `r` and `s` are emitted by +/// the engine little-endian and compared against `r`/`s` directly (no reversal). +pub(super) struct Ecdsa384Kat { + /// Message digest `e` (48 bytes, little-endian). + pub digest: &'static [u8], + + /// Fixed per-signature nonce `k` (48 bytes, little-endian). + pub k: &'static [u8], + + /// Signing private key `d` (48 bytes, little-endian). + pub private_key: &'static [u8], + + /// Expected signature component `r` (48 bytes, little-endian). + pub r: &'static [u8], + + /// Expected signature component `s` (48 bytes, little-endian). + pub s: &'static [u8], +} + +/// ECDSA P-384 deterministic sign known-answer test vector (see [`Ecdsa384Kat`]). +pub(super) const ECDSA_384_SIGN_KAT: Ecdsa384Kat = Ecdsa384Kat { + digest: &[ + 0xd1, 0x3e, 0x7f, 0x7d, 0x01, 0xcd, 0x18, 0x3e, 0x83, 0xc2, 0xfb, 0xe0, 0x00, 0xff, 0x9d, + 0x5f, 0x45, 0x99, 0xb2, 0x72, 0xd1, 0x88, 0xe2, 0x10, 0xda, 0x3f, 0x5d, 0x64, 0x5f, 0x0a, + 0xbd, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + ], + k: &[ + 0x43, 0x24, 0xa3, 0xf3, 0x24, 0x7e, 0x87, 0x04, 0xa8, 0xd4, 0xea, 0x36, 0x3d, 0xcd, 0x0f, + 0xb3, 0xcc, 0x57, 0xc7, 0x6c, 0xaf, 0xe7, 0xd1, 0x0d, 0x8c, 0x79, 0x9b, 0x29, 0xb5, 0x96, + 0x24, 0x93, 0xc0, 0xcd, 0x97, 0x86, 0xd8, 0xd0, 0x27, 0x78, 0x0b, 0x3d, 0x68, 0xc4, 0x25, + 0x5c, 0x0b, 0xc1, + ], + private_key: &[ + 0x6b, 0x9e, 0x7f, 0x6d, 0x47, 0x87, 0xc9, 0x83, 0x77, 0x5a, 0x85, 0x9b, 0xd9, 0xf0, 0x52, + 0x4b, 0x18, 0x30, 0x26, 0x16, 0x58, 0xf0, 0x89, 0x2a, 0xc4, 0x6c, 0x67, 0x74, 0x72, 0x20, + 0xf7, 0x84, 0x2c, 0x83, 0xe0, 0x61, 0x96, 0x56, 0xa6, 0x11, 0xc3, 0x92, 0x45, 0xa3, 0x74, + 0xbc, 0x02, 0xc6, + ], + r: &[ + 0x78, 0xe8, 0x40, 0xd4, 0x46, 0x79, 0x26, 0xc3, 0x2e, 0xaa, 0x88, 0x17, 0x85, 0x61, 0x8c, + 0x97, 0x59, 0x03, 0xab, 0xa0, 0x1d, 0x55, 0x54, 0x90, 0x60, 0xad, 0xc2, 0xeb, 0xd7, 0x7e, + 0x47, 0x48, 0x59, 0x78, 0x02, 0xcd, 0x38, 0x3f, 0x48, 0xd4, 0x86, 0x32, 0xf5, 0xda, 0x0c, + 0xb0, 0x1d, 0xb1, + ], + s: &[ + 0xf2, 0xcb, 0xf8, 0x7d, 0x02, 0xac, 0x48, 0x10, 0x67, 0x87, 0x78, 0x4b, 0xe7, 0x27, 0x8b, + 0xc9, 0xd7, 0x12, 0x65, 0x07, 0x5a, 0x06, 0xf5, 0x2f, 0x76, 0x3a, 0x68, 0x9c, 0x31, 0xe3, + 0xb6, 0xe2, 0xe8, 0x73, 0xe9, 0xfe, 0xa8, 0x12, 0x81, 0xe6, 0x4c, 0x60, 0xb0, 0xc5, 0x73, + 0x78, 0x00, 0x16, + ], +}; From 6cd5e1b98155631fb476822b36e2251ce8d4c029 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Fri, 10 Jul 2026 16:41:51 -0700 Subject: [PATCH 11/19] Add RNG preops self test --- fw/plat/uno/fw/drivers/rng/src/api.rs | 281 ++++++++++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/mod.rs | 12 +- 2 files changed, 289 insertions(+), 4 deletions(-) diff --git a/fw/plat/uno/fw/drivers/rng/src/api.rs b/fw/plat/uno/fw/drivers/rng/src/api.rs index 017ece003..46af0623f 100644 --- a/fw/plat/uno/fw/drivers/rng/src/api.rs +++ b/fw/plat/uno/fw/drivers/rng/src/api.rs @@ -17,13 +17,18 @@ use azihsm_fw_hsm_pal_traits::DmaBuf; use azihsm_fw_static_ref::StaticRef; +use azihsm_fw_uno_error::HsmError; use azihsm_fw_uno_error::HsmResult; use azihsm_fw_uno_reg_soc::rng::regs::RngRegs; use azihsm_fw_uno_reg_soc::rng::APT_CUTOFF; use azihsm_fw_uno_reg_soc::rng::CHISQ_CUTOFF; use azihsm_fw_uno_reg_soc::rng::CLK_DIV_MSB; use azihsm_fw_uno_reg_soc::rng::CTRL; +use azihsm_fw_uno_reg_soc::rng::FWIN_DATA; +use azihsm_fw_uno_reg_soc::rng::FWOUT_DATA; +use azihsm_fw_uno_reg_soc::rng::GENERATE_INTERVAL; use azihsm_fw_uno_reg_soc::rng::REPCNT_CUTOFF; +use azihsm_fw_uno_reg_soc::rng::RESEED_INTERVAL; use azihsm_fw_uno_reg_soc::rng::RNG_BASE; use azihsm_fw_uno_reg_soc::rng::RN_DATA; use azihsm_fw_uno_reg_soc::rng::STATUS; @@ -53,6 +58,113 @@ const REGS: StaticRef = unsafe { StaticRef::new(RNG_BASE as *const RngR /// and an imprecise bus fault on the `CTRL` MMIO write. const RNG_INIT_DELAY_NOPS: u32 = 100_000 * 450 / 2; +/// Upper bound on wait-loop poll iterations in [`RngDriver::self_test`]. +/// +/// The reference firmware bounds each DRBG wait with a ~4 µs `Tcon::tsc()` +/// counter that Uno has no equivalent for. This poll-iteration budget is the +/// stand-in: generous enough never to trip during a healthy DRBG run (which +/// completes in microseconds), but finite so a stuck engine fails the CAST at +/// the boot gate instead of hanging forever. +const RNG_SELF_TEST_MAX_SPINS: u32 = 1_000_000; + +/// DRBG generate interval (in generate cycles) used during the FW-mode KAT. +const DRBG_GENERATE_INTERVAL: u32 = 2; + +/// DRBG reseed interval (in generate cycles) used during the FW-mode KAT. +const DRBG_RESEED_INTERVAL: u32 = 2; + +/// DRBG FW-mode self-test input vector (seed material). +/// +/// Ported byte-for-byte from the reference firmware +/// (`drivers/crypto/rng/src/rng.rs::RNG_SELF_TEST_INPUT`). +const RNG_SELF_TEST_INPUT: [u32; 16] = [ + 0x2cb8_5c71, + 0xdef8_49bf, + 0x5346_88e3, + 0x03bf_f6bd, + 0x9923_dfd1, + 0x28e0_a0d7, + 0xf38f_606c, + 0xcd88_cb0b, + 0xd41f_21da, + 0x16b2_d32f, + 0x041b_8db2, + 0xb5b5_2b5b, + 0x1700_01ef, + 0x602d_910b, + 0x5a17_e20f, + 0xf9a0_b0b9, +]; + +/// Expected DRBG FW-mode generate output for [`RNG_SELF_TEST_INPUT`]. +/// +/// Ported byte-for-byte from the reference firmware +/// (`RNG_SELF_TEST_EXPECTED_OUTPUT`). +const RNG_SELF_TEST_EXPECTED_OUTPUT: [u32; 16] = [ + 0x2df5_26c1, + 0x4ed2_fea1, + 0xe03e_4e33, + 0x773b_820d, + 0x5363_125e, + 0x5731_b848, + 0xf922_7325, + 0x8364_e0f5, + 0xc0fd_533e, + 0x1572_b04f, + 0x678f_4cdc, + 0xf989_cd2b, + 0x580a_18c2, + 0xe9d9_8573, + 0x4789_01b9, + 0xf6aa_61ed, +]; + +/// DRBG FW-mode self-test reseed input vector. +/// +/// Ported byte-for-byte from the reference firmware +/// (`RNG_SELF_TEST_RESEED_INPUT`). +const RNG_SELF_TEST_RESEED_INPUT: [u32; 16] = [ + 0x9fc0_ef1b, + 0x69bd_5cee, + 0x0536_d040, + 0x84e3_24ac, + 0xe803_252f, + 0x51f6_cb45, + 0x9f42_5836, + 0x85a1_550b, + 0x1ebf_fe37, + 0xcca0_724b, + 0x77ec_8fcc, + 0x1cd9_9781, + 0x0022_6942, + 0x3add_2d04, + 0xb677_7a10, + 0xac8f_0743, +]; + +/// Expected DRBG FW-mode reseed output for [`RNG_SELF_TEST_RESEED_INPUT`]. +/// +/// Ported byte-for-byte from the reference firmware +/// (`RNG_SELF_TEST_EXPECTED_RESEED_OUTPUT`). +const RNG_SELF_TEST_EXPECTED_RESEED_OUTPUT: [u32; 16] = [ + 0xa6fc_aa14, + 0xd9cb_3539, + 0x9ba9_b84e, + 0x9864_e8ff, + 0x079b_82c3, + 0x32d3_18f6, + 0x8fac_5900, + 0x73fd_9211, + 0xe2bd_a914, + 0xaafe_a144, + 0x523b_af42, + 0x97f2_049e, + 0x3221_4767, + 0xa1fd_de1c, + 0x141d_a708, + 0x14b6_5a29, +]; + /// TRBG / DRBG calibration parameters. /// /// [`RngCalibration::DEFAULT`] provides the silicon-specific values @@ -190,4 +302,173 @@ impl RngDriver { } } } + + /// DRBG FW-mode known-answer self-test (pre-operational CAST). + /// + /// Ported as-is from the reference firmware + /// (`drivers/crypto/rng/src/rng.rs::self_test`). Drives the DRBG through + /// its firmware-mode back door: seeds the FW input FIFO with a fixed + /// vector, runs instantiate + generate, and compares the FW output FIFO + /// against the expected known answer; then exercises the reseed path with + /// a second fixed vector. The production DRBG registers (control word and + /// generate/reseed intervals) are saved on entry and restored before + /// return, so the RNG is left in its normal operating mode on both the + /// success and failure paths. + /// + /// # Returns + /// * `Ok(())` if both the generate and reseed outputs match their vectors. + /// + /// # Errors + /// * [`HsmError::SelfTestKatMismatch`] on any output mismatch, DRBG fault, + /// or wait-loop timeout. + /// + /// # Timeout + /// The reference bounds each wait with a ~4 µs `Tcon::tsc()` counter that + /// Uno lacks; here each wait loop is bounded by [`RNG_SELF_TEST_MAX_SPINS`] + /// poll iterations instead. Exhausting the budget fails the test (never a + /// hang), so a stuck DRBG surfaces a clean CAST failure at the boot gate. + pub fn self_test(&self) -> HsmResult<()> { + // Before switching to FW mode, ensure the DRBG is not actively reading + // entropy — FW mode cannot be entered while an entropy read is in + // flight. + Self::spin_while(|| REGS.status.is_set(STATUS::ENTROPY_FIFO_READ))?; + + // Save the production register state so it can be restored on exit. + let saved_generate_interval = REGS.generate_interval.get(); + let saved_reseed_interval = REGS.reseed_interval.get(); + let saved_ctrl = REGS.ctrl.get(); + + // Shorten the generate/reseed intervals so the KAT exercises a reseed. + REGS.generate_interval + .write(GENERATE_INTERVAL::VALUE.val(DRBG_GENERATE_INTERVAL)); + REGS.reseed_interval + .write(RESEED_INTERVAL::VALUE.val(DRBG_RESEED_INTERVAL)); + + // Enter FW mode and kick off instantiate + generate. + REGS.ctrl.modify( + CTRL::ENABLE::SET + + CTRL::FW_MODE::SET + + CTRL::DRBG_INSTANTIATE::SET + + CTRL::DRBG_GENERATE::SET + + CTRL::DRBG_UNINSTANTIATE::CLEAR, + ); + + // Restore the RNG to normal mode; run on every early-exit path. + let cleanup = || { + REGS.ctrl.modify( + CTRL::DRBG_GENERATE::CLEAR + CTRL::DRBG_INSTANTIATE::CLEAR + CTRL::FW_MODE::CLEAR, + ); + REGS.generate_interval.set(saved_generate_interval); + REGS.reseed_interval.set(saved_reseed_interval); + }; + + // Fill the input FIFO; once full the DRBG starts processing it. + for word in RNG_SELF_TEST_INPUT { + REGS.fwin_data.write(FWIN_DATA::DATA.val(word)); + } + + Self::wait_for_drbg_instantiate().inspect_err(|_e| cleanup())?; + Self::wait_for_drbg_generate().inspect_err(|_e| cleanup())?; + Self::compare_output(&RNG_SELF_TEST_EXPECTED_OUTPUT).inspect_err(|_e| cleanup())?; + + // Reseed path: wait for the DRBG to request a reseed, feed it the + // reseed vector, then wait for it to complete. + Self::wait_till_status_is_set_to_busy().inspect_err(|_e| cleanup())?; + Self::wait_till_reseed_is_set().inspect_err(|_e| cleanup())?; + + for word in RNG_SELF_TEST_RESEED_INPUT { + REGS.fwin_data.write(FWIN_DATA::DATA.val(word)); + } + + Self::wait_for_reseed_clear().inspect_err(|_e| cleanup())?; + Self::wait_for_busy_status_clear().inspect_err(|_e| cleanup())?; + + // Compare the reseed output (the reference does not discard here). + for word in RNG_SELF_TEST_EXPECTED_RESEED_OUTPUT { + if REGS.fwout_data.read(FWOUT_DATA::DATA) != word { + cleanup(); + return Err(HsmError::SelfTestKatMismatch); + } + } + + // Restore the RNG to its normal operating mode. + REGS.generate_interval.set(saved_generate_interval); + REGS.reseed_interval.set(saved_reseed_interval); + REGS.ctrl.set(saved_ctrl); + REGS.ctrl.modify( + CTRL::FW_MODE::CLEAR + CTRL::DRBG_INSTANTIATE::CLEAR + CTRL::DRBG_GENERATE::CLEAR, + ); + + Ok(()) + } + + /// Poll `busy` until it returns `false`, bounded by + /// [`RNG_SELF_TEST_MAX_SPINS`]. Returns [`HsmError::SelfTestKatMismatch`] + /// if the budget is exhausted (the Uno stand-in for the reference's + /// `Tcon::tsc()` timeout). + fn spin_while bool>(busy: F) -> HsmResult<()> { + let mut budget = RNG_SELF_TEST_MAX_SPINS; + while busy() { + budget -= 1; + if budget == 0 { + return Err(HsmError::SelfTestKatMismatch); + } + } + Ok(()) + } + + /// Wait for the DRBG instantiate operation to finish, failing early on a + /// DRBG fault. + fn wait_for_drbg_instantiate() -> HsmResult<()> { + let mut budget = RNG_SELF_TEST_MAX_SPINS; + while REGS.status.is_set(STATUS::DRBG_INST_BUSY) { + if REGS.status.is_set(STATUS::DRBG_FAULT_ERROR) { + return Err(HsmError::SelfTestKatMismatch); + } + budget -= 1; + if budget == 0 { + return Err(HsmError::SelfTestKatMismatch); + } + } + Ok(()) + } + + /// Wait for the DRBG generate operation to complete (`STATUS.BUSY` clear). + fn wait_for_drbg_generate() -> HsmResult<()> { + Self::spin_while(|| REGS.status.is_set(STATUS::BUSY)) + } + + /// Wait for `STATUS.BUSY` to be set (a reseed request is pending). + fn wait_till_status_is_set_to_busy() -> HsmResult<()> { + Self::spin_while(|| !REGS.status.is_set(STATUS::BUSY)) + } + + /// Wait for the DRBG reseed-busy flag to be set. + fn wait_till_reseed_is_set() -> HsmResult<()> { + Self::spin_while(|| !REGS.status.is_set(STATUS::DRBG_RESEED_BUSY)) + } + + /// Wait for the DRBG reseed-busy flag to clear. + fn wait_for_reseed_clear() -> HsmResult<()> { + Self::spin_while(|| REGS.status.is_set(STATUS::DRBG_RESEED_BUSY)) + } + + /// Wait for `STATUS.BUSY` to clear. + fn wait_for_busy_status_clear() -> HsmResult<()> { + Self::spin_while(|| REGS.status.is_set(STATUS::BUSY)) + } + + /// Read and discard the first 16 FW-output words, then compare the next 16 + /// against `expected`. Mirrors the reference `compare_output`. + fn compare_output(expected: &[u32; 16]) -> HsmResult<()> { + for _ in 0..expected.len() { + let _ = REGS.fwout_data.read(FWOUT_DATA::DATA); + } + for &word in expected { + if REGS.fwout_data.read(FWOUT_DATA::DATA) != word { + return Err(HsmError::SelfTestKatMismatch); + } + } + Ok(()) + } } diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index 81b57fc61..c93aa6651 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -18,10 +18,10 @@ //! //! # Status //! -//! AES-256-CBC, HKDF, KBKDF, and per-engine RSA-2048 mod-exp (standard and CRT), -//! ECDH-P384, and ECDSA-P384 are implemented. Further tests (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 +//! AES-256-CBC, HKDF, KBKDF, per-engine RSA-2048 mod-exp (standard and CRT), +//! ECDH-P384, ECDSA-P384, and the RNG/DRBG FW-mode KAT are implemented. Further +//! tests 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; @@ -55,5 +55,9 @@ pub(crate) async fn run_pre_op(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> for engine in 0..pka::PKA_ENGINES { pka::run_ecdsa_on_engine(pal, io, engine).await?; } + // The DRBG FW-mode KAT is state-destructive; it saves and restores the + // production DRBG registers internally. Run it last (matching the reference + // `preops_cast` ordering) and before any live RNG consumer exists. + pal.rng.self_test()?; Ok(()) } From 64df955fe76644bf8e50251f32fa1d5fa858f3ea Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Fri, 10 Jul 2026 17:18:34 -0700 Subject: [PATCH 12/19] Address review comments --- fw/plat/uno/fw/pal/src/alloc.rs | 17 ++++++++++++++++- fw/plat/uno/fw/pal/src/io.rs | 25 +++++++++++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/mod.rs | 2 +- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/fw/plat/uno/fw/pal/src/alloc.rs b/fw/plat/uno/fw/pal/src/alloc.rs index da7280961..a9cad2e83 100644 --- a/fw/plat/uno/fw/pal/src/alloc.rs +++ b/fw/plat/uno/fw/pal/src/alloc.rs @@ -48,7 +48,14 @@ pub(crate) const ADMIN_IO_INDEX: u16 = (IO_SLOTS - 2) as u16; /// 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. +/// shared bump heap. +/// +/// Unlike host and admin slots, this slot is **DMA-only**: it has +/// `SRAM_IO_BUF` (Dma) backing but no `DTCM_IO_BUF` (NonDma) entry — +/// `DTCM_IO_BUF` covers only the 33 host+admin slots (see +/// `dtcm_map.rdl`). NonDma allocations against this slot are rejected +/// by the bump allocator so an accidental one cannot address past the +/// `DTCM_IO_BUF` region into the crashdump/status area. pub(crate) const SELF_TEST_IO_INDEX: u16 = (IO_SLOTS - 1) as u16; // DTCM IO buffer region — per-IO NonDma scratch in upper DTCM. @@ -204,6 +211,14 @@ fn bump( size: usize, align: usize, ) -> HsmResult<(usize, &'static mut [u8])> { + // The self-test slot is DMA-only: it has SRAM_IO_BUF (Dma) backing but no + // DTCM_IO_BUF (NonDma) entry. Reject NonDma allocations rather than + // computing a DTCM address past the buffer region, which would overlap the + // crashdump/status area. + if heap == NONDMA && io_index == SELF_TEST_IO_INDEX { + return Err(HsmError::NotEnoughSpace); + } + let (base_ptr, cap) = heap_base_cap(io_index, heap); let w = wm(pal, io_index, heap); let mark = w.with(|v| *v).min(cap); diff --git a/fw/plat/uno/fw/pal/src/io.rs b/fw/plat/uno/fw/pal/src/io.rs index 967c85f11..d9472a9e1 100644 --- a/fw/plat/uno/fw/pal/src/io.rs +++ b/fw/plat/uno/fw/pal/src/io.rs @@ -34,6 +34,7 @@ use azihsm_fw_hsm_pal_traits::HsmResult; use azihsm_fw_hsm_pal_traits::HsmSqe; use azihsm_fw_static_ref::StaticRef; use azihsm_fw_uno_reg_soc::io_gsram::IO_GSRAM_BASE; +use azihsm_fw_uno_reg_soc::io_gsram::IO_META_COUNT; use azihsm_fw_uno_reg_soc::io_gsram::IO_META_CTLR; use azihsm_fw_uno_reg_soc::io_gsram::IO_META_QUEUE; use azihsm_fw_uno_reg_soc::io_gsram::IoCqEntry; @@ -105,6 +106,11 @@ impl UnoHsmIo { } /// Returns a reference to the IO_META entry for this slot. + /// + /// Only valid for host and admin slots — `IO_META` has + /// [`IO_META_COUNT`] entries (host `0..32` plus [`ADMIN_IO_INDEX`]). + /// The self-test slot ([`SELF_TEST_IO_INDEX`]) has no `IO_META` entry; + /// callers must guard against it (see [`pid`](HsmIo::pid)). #[inline] fn io_meta(&self) -> &IoMetaEntry { &IO_Q.io_meta[self.index as usize] @@ -118,17 +124,36 @@ impl HsmIo for UnoHsmIo { } /// Returns the partition ID (controller_id from IO_META). + /// + /// The self-test slot ([`SELF_TEST_IO_INDEX`]) has no `IO_META` entry and + /// no partition context, so this returns partition `0` for it rather than + /// indexing `IO_META` out of bounds. fn pid(&self) -> HsmPartId { + if self.index as u32 >= IO_META_COUNT { + return HsmPartId::from(0u8); + } HsmPartId::from(self.io_meta().ctlr.read(IO_META_CTLR::CONTROLLER_ID) as u8) } /// Returns the queue ID from IO_META. + /// + /// Returns `0` for the self-test slot ([`SELF_TEST_IO_INDEX`]), which has + /// no `IO_META` entry. fn queue_id(&self) -> u16 { + if self.index as u32 >= IO_META_COUNT { + return 0; + } self.io_meta().queue.read(IO_META_QUEUE::QUEUE_ID) as u16 } /// Returns the queue index from IO_META. + /// + /// Returns `0` for the self-test slot ([`SELF_TEST_IO_INDEX`]), which has + /// no `IO_META` entry. fn queue_idx(&self) -> u16 { + if self.index as u32 >= IO_META_COUNT { + return 0; + } self.io_meta().queue.read(IO_META_QUEUE::QUEUE_INDEX) as u16 } diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index c93aa6651..11225dc18 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -12,7 +12,7 @@ //! //! 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 +//! [`UnoHsmIo::self_test`]. 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). //! From edb2ed2c6525ae9bbe9a88487934b82a12f449a5 Mon Sep 17 00:00:00 2001 From: msft-nathwania Date: Fri, 10 Jul 2026 17:27:54 -0700 Subject: [PATCH 13/19] Fix extra character in rustdoc Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/pal/src/self_test/vectors.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 3967bc175..cd860ee99 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -267,7 +267,7 @@ pub(super) const RSA_2K_MOD_EXP_KAT: RsaModExpKat = RsaModExpKat { }; /// Expected KEK recovered by OAEP-decoding (SHA-256, empty label) the RSA-2048 -/// mod-exp plaintext block [`RSA_2K_MOD_EXP_KAT`]`.k`. +/// mod-exp plaintext block [`RSA_2K_MOD_EXP_KAT`].k. /// /// This is the tail of the reference firmware's RSA mod-exp self-test: after the /// PKA mod-exp produces the padded plaintext, the SHA engine OAEP-decodes it and From 827cf54d46c1a3a633f86ded0c1152d6295974ca Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Fri, 10 Jul 2026 17:40:24 -0700 Subject: [PATCH 14/19] Address review comments --- fw/plat/uno/fw/drivers/rng/src/api.rs | 11 +++++++---- fw/plat/uno/fw/drivers/upka/src/engine.rs | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/fw/plat/uno/fw/drivers/rng/src/api.rs b/fw/plat/uno/fw/drivers/rng/src/api.rs index 46af0623f..39d5470c8 100644 --- a/fw/plat/uno/fw/drivers/rng/src/api.rs +++ b/fw/plat/uno/fw/drivers/rng/src/api.rs @@ -310,10 +310,13 @@ impl RngDriver { /// its firmware-mode back door: seeds the FW input FIFO with a fixed /// vector, runs instantiate + generate, and compares the FW output FIFO /// against the expected known answer; then exercises the reseed path with - /// a second fixed vector. The production DRBG registers (control word and - /// generate/reseed intervals) are saved on entry and restored before - /// return, so the RNG is left in its normal operating mode on both the - /// success and failure paths. + /// a second fixed vector. The generate/reseed intervals are saved on entry + /// and restored on every path. On the success path the full saved control + /// word is additionally written back; an early-exit (failure) path instead + /// returns the control word to normal DRBG mode by clearing the + /// FW-mode/instantiate/generate bits it set (matching the reference + /// firmware, which heads to a self-test error state on failure). Either + /// way the RNG is left in its normal operating mode. /// /// # Returns /// * `Ok(())` if both the generate and reseed outputs match their vectors. diff --git a/fw/plat/uno/fw/drivers/upka/src/engine.rs b/fw/plat/uno/fw/drivers/upka/src/engine.rs index 8842bb2e2..62a07b686 100644 --- a/fw/plat/uno/fw/drivers/upka/src/engine.rs +++ b/fw/plat/uno/fw/drivers/upka/src/engine.rs @@ -630,9 +630,9 @@ impl UpkaEngine<'_, DEPTH, ENGINES> { /// /// - `key_type`: RSA key type (size + format selector). /// - `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. + /// `d ‖ n` blob at least `2 * mod_size` bytes long; 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. /// @@ -661,7 +661,10 @@ impl UpkaEngine<'_, DEPTH, ENGINES> { key.as_ptr() as u32 + param2_off as u32, ) } else { - (!key.is_empty(), 0) + // Standard keys are a single `d ‖ n` blob (2·mod_size) read from + // arg2; the buffer must be long enough for the hardware's full + // fetch to stay in bounds. + (key.len() >= 2 * mod_size, 0) }; Self::ensure_cmd_input(key_ok && input.len() == mod_size && output.len() == mod_size)?; From 234ea73d72a02cd1512e1b4f9130799e85661100 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Mon, 13 Jul 2026 12:17:27 -0700 Subject: [PATCH 15/19] Add comments --- fw/plat/uno/fw/pal/src/io.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fw/plat/uno/fw/pal/src/io.rs b/fw/plat/uno/fw/pal/src/io.rs index d9472a9e1..347be484d 100644 --- a/fw/plat/uno/fw/pal/src/io.rs +++ b/fw/plat/uno/fw/pal/src/io.rs @@ -98,6 +98,14 @@ impl UnoHsmIo { /// so `IO_META` is left untouched ([`pid`](HsmIo::pid) is unused by the /// self-test path). /// + /// # Restrictions + /// + /// This handle is for DMA allocation only. Its index (33) is outside the + /// host queue ranges (`IO_SQ_COUNT`/`IO_CQ_COUNT`/`IO_META_COUNT` are all + /// ≤ 33), so [`sqe`](HsmIo::sqe) and [`cqe`](HsmIo::cqe) have no backing + /// entry and must not be called on it; the host-IO dispatch that reads + /// them never runs for the self-test. + /// /// [`SELF_TEST_IO_INDEX`]: crate::alloc::SELF_TEST_IO_INDEX pub(crate) fn self_test() -> Self { Self { From f73e0143f2fefa7051caa9c7bd6ac134dc173f28 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Mon, 13 Jul 2026 12:33:49 -0700 Subject: [PATCH 16/19] Change error code as per review comments --- fw/plat/uno/fw/pal/src/alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fw/plat/uno/fw/pal/src/alloc.rs b/fw/plat/uno/fw/pal/src/alloc.rs index a9cad2e83..9f0a39068 100644 --- a/fw/plat/uno/fw/pal/src/alloc.rs +++ b/fw/plat/uno/fw/pal/src/alloc.rs @@ -216,7 +216,7 @@ fn bump( // computing a DTCM address past the buffer region, which would overlap the // crashdump/status area. if heap == NONDMA && io_index == SELF_TEST_IO_INDEX { - return Err(HsmError::NotEnoughSpace); + return Err(HsmError::InvalidArg); } let (base_ptr, cap) = heap_base_cap(io_index, heap); From 524694e16674a281882c3be95c45835bdf300d32 Mon Sep 17 00:00:00 2001 From: msft-nathwania Date: Mon, 13 Jul 2026 14:02:04 -0700 Subject: [PATCH 17/19] Address review comments Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/pal/src/io.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fw/plat/uno/fw/pal/src/io.rs b/fw/plat/uno/fw/pal/src/io.rs index 347be484d..058efb060 100644 --- a/fw/plat/uno/fw/pal/src/io.rs +++ b/fw/plat/uno/fw/pal/src/io.rs @@ -100,11 +100,10 @@ impl UnoHsmIo { /// /// # Restrictions /// - /// This handle is for DMA allocation only. Its index (33) is outside the - /// host queue ranges (`IO_SQ_COUNT`/`IO_CQ_COUNT`/`IO_META_COUNT` are all - /// ≤ 33), so [`sqe`](HsmIo::sqe) and [`cqe`](HsmIo::cqe) have no backing - /// entry and must not be called on it; the host-IO dispatch that reads - /// them never runs for the self-test. + /// This handle is for DMA allocation only. Its index ([`SELF_TEST_IO_INDEX`]) is outside the + /// host queue ranges (`IO_SQ_COUNT`/`IO_CQ_COUNT`/`IO_META_COUNT`), so + /// [`sqe`](HsmIo::sqe) and [`cqe`](HsmIo::cqe) have no backing entry and must not be + /// called on it; the host-IO dispatch that reads them never runs for the self-test. /// /// [`SELF_TEST_IO_INDEX`]: crate::alloc::SELF_TEST_IO_INDEX pub(crate) fn self_test() -> Self { From bdcaa1b144051f3d4f89e18f372a6d9c79f8d008 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Mon, 20 Jul 2026 16:02:53 -0700 Subject: [PATCH 18/19] Reuse ecc_sign_with_k driver API for ECDSA selftests --- fw/plat/uno/fw/drivers/upka/src/engine.rs | 242 -------------------- fw/plat/uno/fw/drivers/upka/src/opcode.rs | 93 -------- fw/plat/uno/fw/pal/src/crypto/ecc_det.rs | 64 +++++- fw/plat/uno/fw/pal/src/self_test/pka.rs | 136 ++--------- fw/plat/uno/fw/pal/src/self_test/vectors.rs | 31 --- 5 files changed, 77 insertions(+), 489 deletions(-) diff --git a/fw/plat/uno/fw/drivers/upka/src/engine.rs b/fw/plat/uno/fw/drivers/upka/src/engine.rs index 62a07b686..1878e39c7 100644 --- a/fw/plat/uno/fw/drivers/upka/src/engine.rs +++ b/fw/plat/uno/fw/drivers/upka/src/engine.rs @@ -379,248 +379,6 @@ impl UpkaEngine<'_, DEPTH, ENGINES> { .await } - /// Compute the Montgomery constant for `modulus` on this engine. - /// - /// Establishes the active modulus for the subsequent field operations on the - /// same engine acquisition (`execute_cmd` does not wipe engine state between - /// commands). `result` receives the natural-form scratch output. - /// - /// This is a low-level primitive intended for orchestrating a raw ECDSA - /// sign sequence (e.g. the FIPS self-test); production ECDSA/ECDH use the - /// composite [`ecc_sign`](Self::ecc_sign) / [`ecdh_derive`](Self::ecdh_derive) - /// helpers. - /// - /// # Returns - /// - /// - `Ok(())`: Montgomery constant established for `modulus`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mont_const_calc( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - modulus: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= point_size(curve) && modulus.len() >= point_size(curve), - )?; - self.field_unary(mont_const_calc_opcode(curve), result, modulus) - .await - } - - /// Multiply an affine point by a scalar (`result = scalar · point`). - /// - /// `point_xy` is the contiguous `X ‖ Y` affine point (each coordinate - /// `point_size` bytes, LE); `scalar` is `point_size` bytes (LE). `result` - /// receives the X coordinate of the product (`point_size` bytes, LE). - /// - /// # Returns - /// - /// - `Ok(())`: Product X coordinate was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_point_mul( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - point_xy: &DmaBuf, - scalar: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= point_size(curve) - && point_xy.len() >= point_size(curve) * 2 - && scalar.len() >= point_size(curve), - )?; - self.execute_cmd( - ecc_point_mul_opcode(curve), - result.as_mut_ptr() as u32, - point_xy.as_ptr() as u32, - scalar.as_ptr() as u32, - 0, - ) - .await - } - - /// Reduce a natural-form field element modulo the active modulus. - /// - /// `arg` is a **double-width** (`2 · point_size` bytes, LE) zero-padded - /// dividend: the value to reduce occupies the low `point_size` bytes and the - /// high `point_size` bytes must be zero. The hardware reduction reads the - /// full `2 · point_size` window, so a tight `point_size` buffer would let it - /// read adjacent memory as the high half and produce garbage. `result` is - /// `point_size` bytes (LE). Requires a preceding - /// [`ecc_mont_const_calc`](Self::ecc_mont_const_calc) for the target - /// modulus on the same engine acquisition. - /// - /// # Returns - /// - /// - `Ok(())`: Reduced value was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mod_reduction( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - arg: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= point_size(curve) && arg.len() >= point_size(curve) * 2, - )?; - self.field_unary(ecc_mod_reduction_opcode(curve), result, arg) - .await - } - - /// Convert a natural-form field element into Montgomery form. - /// - /// `arg` is `point_size` bytes (LE); `result` is `montgomery_size` bytes. - /// - /// # Returns - /// - /// - `Ok(())`: Montgomery-form value was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mont_repr_in( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - arg: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= montgomery_size(curve) && arg.len() >= point_size(curve), - )?; - self.field_unary(ecc_mont_repr_in_opcode(curve), result, arg) - .await - } - - /// Convert a Montgomery-form field element back into natural form. - /// - /// `arg` is `montgomery_size` bytes; `result` is `point_size` bytes (LE). - /// - /// # Returns - /// - /// - `Ok(())`: Natural-form value was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mont_repr_out( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - arg: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= point_size(curve) && arg.len() >= montgomery_size(curve), - )?; - self.field_unary(ecc_mont_repr_out_opcode(curve), result, arg) - .await - } - - /// Compute the modular inverse of a Montgomery-form field element. - /// - /// `arg` and `result` are `montgomery_size` bytes. - /// - /// # Returns - /// - /// - `Ok(())`: Inverse was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mod_inverse( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - arg: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= montgomery_size(curve) && arg.len() >= montgomery_size(curve), - )?; - self.field_unary(ecc_mod_inverse_opcode(curve), result, arg) - .await - } - - /// Multiply two Montgomery-form field elements modulo the active modulus. - /// - /// `a`, `b` and `result` are `montgomery_size` bytes. - /// - /// # Returns - /// - /// - `Ok(())`: Product was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mod_mul( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - a: &DmaBuf, - b: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= montgomery_size(curve) - && a.len() >= montgomery_size(curve) - && b.len() >= montgomery_size(curve), - )?; - self.field_binary(ecc_mod_mul_opcode(curve), result, a, b) - .await - } - - /// Add two Montgomery-form field elements modulo the active modulus. - /// - /// `a`, `b` and `result` are `montgomery_size` bytes. - /// - /// # Returns - /// - /// - `Ok(())`: Sum was written to `result`. - /// - `Err(UpkaError::CMD_ERROR)`: Buffer shape is invalid or hardware - /// rejected the command. - pub async fn ecc_mod_add( - &mut self, - curve: UpkaEccCurve, - result: &mut DmaBuf, - a: &DmaBuf, - b: &DmaBuf, - ) -> HsmResult<()> { - Self::ensure_cmd_input( - result.len() >= montgomery_size(curve) - && a.len() >= montgomery_size(curve) - && b.len() >= montgomery_size(curve), - )?; - self.field_binary(ecc_mod_add_opcode(curve), result, a, b) - .await - } - - /// Submit a unary field operation (`result = op(arg)`). - async fn field_unary( - &mut self, - opcode: u32, - result: &mut DmaBuf, - arg: &DmaBuf, - ) -> HsmResult<()> { - self.execute_cmd( - opcode, - result.as_mut_ptr() as u32, - arg.as_ptr() as u32, - 0, - 0, - ) - .await - } - - /// Submit a binary field operation (`result = op(a, b)`). - async fn field_binary( - &mut self, - opcode: u32, - result: &mut DmaBuf, - a: &DmaBuf, - b: &DmaBuf, - ) -> HsmResult<()> { - self.execute_cmd( - opcode, - result.as_mut_ptr() as u32, - a.as_ptr() as u32, - b.as_ptr() as u32, - 0, - ) - .await - } - /// Run an RSA private-key modular exponentiation. /// /// `key_type` selects both modulus size and key format (standard or CRT). diff --git a/fw/plat/uno/fw/drivers/upka/src/opcode.rs b/fw/plat/uno/fw/drivers/upka/src/opcode.rs index 107ba0c24..3bd94bf0f 100644 --- a/fw/plat/uno/fw/drivers/upka/src/opcode.rs +++ b/fw/plat/uno/fw/drivers/upka/src/opcode.rs @@ -232,76 +232,6 @@ pub(crate) fn ecc_point_mul_opcode(curve: UpkaEccCurve) -> u32 { } } -/// Return the modular-multiplication opcode for the selected curve. -/// -/// Multiplies two Montgomery-form field elements modulo the active modulus that -/// was established by the preceding `mont_const_calc`. -pub(crate) fn ecc_mod_mul_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MOD_MULTIPLICATION_256, - UpkaEccCurve::P384 => MOD_MULTIPLICATION_384, - UpkaEccCurve::P521 => MOD_MULTIPLICATION_521, - } -} - -/// Return the modular-addition opcode for the selected curve. -/// -/// Adds two Montgomery-form field elements modulo the active modulus. -pub(crate) fn ecc_mod_add_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MOD_ADDITION_256, - UpkaEccCurve::P384 => MOD_ADDITION_384, - UpkaEccCurve::P521 => MOD_ADDITION_521, - } -} - -/// Return the modular-inverse opcode for the selected curve. -/// -/// Computes the multiplicative inverse of a Montgomery-form field element modulo -/// the active modulus. -pub(crate) fn ecc_mod_inverse_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MOD_INVERSE_256, - UpkaEccCurve::P384 => MOD_INVERSE_384, - UpkaEccCurve::P521 => MOD_INVERSE_521, - } -} - -/// Return the modular-reduction opcode for the selected curve. -/// -/// Reduces a natural-form field element modulo the active modulus. -pub(crate) fn ecc_mod_reduction_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MOD_REDUCTION_256, - UpkaEccCurve::P384 => MOD_REDUCTION_384, - UpkaEccCurve::P521 => MOD_REDUCTION_521, - } -} - -/// Return the Montgomery-representation-in opcode for the selected curve. -/// -/// Converts a natural-form field element into Montgomery form for the active -/// modulus (`point_size` bytes in, `montgomery_size` bytes out). -pub(crate) fn ecc_mont_repr_in_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MONT_REPR_IN_256, - UpkaEccCurve::P384 => MONT_REPR_IN_384, - UpkaEccCurve::P521 => MONT_REPR_IN_521, - } -} - -/// Return the Montgomery-representation-out opcode for the selected curve. -/// -/// Converts a Montgomery-form field element back into natural form for the -/// active modulus (`montgomery_size` bytes in, `point_size` bytes out). -pub(crate) fn ecc_mont_repr_out_opcode(curve: UpkaEccCurve) -> u32 { - match curve { - UpkaEccCurve::P256 => MONT_REPR_OUT_256, - UpkaEccCurve::P384 => MONT_REPR_OUT_384, - UpkaEccCurve::P521 => MONT_REPR_OUT_521, - } -} - /// Return the ECC point-validation opcode for the selected curve. /// /// # Parameters @@ -353,29 +283,6 @@ pub(crate) fn point_size(curve: UpkaEccCurve) -> usize { } } -/// Return the Montgomery-form field-element size for the selected ECC curve. -/// -/// Montgomery intermediates carry extra guard words versus the natural -/// `point_size` representation. -/// -/// # Parameters -/// -/// - `curve`: ECC curve selector. -/// -/// # Returns -/// -/// - Montgomery representation size in bytes. -/// - P256: 36 -/// - P384: 52 -/// - P521: 72 -pub(crate) fn montgomery_size(curve: UpkaEccCurve) -> usize { - match curve { - UpkaEccCurve::P256 => 36, - UpkaEccCurve::P384 => 52, - UpkaEccCurve::P521 => 72, - } -} - /// Return the HSM wire-format coordinate size for the selected ECC curve. /// /// # Parameters diff --git a/fw/plat/uno/fw/pal/src/crypto/ecc_det.rs b/fw/plat/uno/fw/pal/src/crypto/ecc_det.rs index f47a67885..bc27f7ab5 100644 --- a/fw/plat/uno/fw/pal/src/crypto/ecc_det.rs +++ b/fw/plat/uno/fw/pal/src/crypto/ecc_det.rs @@ -261,6 +261,46 @@ impl UnoHsmPal { d: &DmaBuf, r: &mut DmaBuf, s: &mut DmaBuf, + ) -> HsmResult<()> { + self.sign_with_k_inner(io, curve, k, digest, d, r, s, None) + .await + } + + /// As [`Self::ecc_sign_with_k`], but pinned to a specific PKA `engine` + /// (0-based index) instead of any free engine. + /// + /// Used by the per-engine ECDSA self-test (CAST) to validate every UPKA + /// engine in turn; production callers use [`Self::ecc_sign_with_k`]. + #[allow(clippy::too_many_arguments)] + pub(crate) async fn ecc_sign_with_k_on_engine( + &self, + io: &impl HsmIo, + curve: UpkaEccCurve, + engine: u8, + k: &DmaBuf, + digest: &DmaBuf, + d: &DmaBuf, + r: &mut DmaBuf, + s: &mut DmaBuf, + ) -> HsmResult<()> { + self.sign_with_k_inner(io, curve, k, digest, d, r, s, Some(engine)) + .await + } + + /// Shared implementation of [`Self::ecc_sign_with_k`] / + /// [`Self::ecc_sign_with_k_on_engine`]: `engine` selects a specific PKA + /// engine (`Some`) or any free one (`None`). + #[allow(clippy::too_many_arguments)] + async fn sign_with_k_inner( + &self, + io: &impl HsmIo, + curve: UpkaEccCurve, + k: &DmaBuf, + digest: &DmaBuf, + d: &DmaBuf, + r: &mut DmaBuf, + s: &mut DmaBuf, + engine: Option, ) -> HsmResult<()> { // Implemented for P-384 only (the cert-chain PID leaf is signed with the // P-384 alias key). Other curves are rejected until their constants / @@ -365,11 +405,17 @@ impl UnoHsmPal { EccStep::new(EccStepOp::MontReprOut, a_s, a_s_plus_t, 0), ]; - // Drive the whole sequence on one held engine so the Montgomery - // constant set by each phase stays resident for the ops that follow. - let res = self - .pka - .with_engine(async |eng| { + // Drive the whole sequence on one held engine (a specific one for + // the per-engine self-test, otherwise any free engine) so the + // Montgomery constant set by each phase stays resident for the ops + // that follow. `release` performs the engine wipe, mirroring + // `with_engine` / `with_specific_engine`. + let res = async { + let mut eng = match engine { + Some(id) => self.pka.acquire_engine(id).await?, + None => self.pka.acquire_any().await?, + }; + let outcome = async { eng.ecc_run(curve, &prime_phase).await?; // Reduction is double-width: stage xR into the low half of the @@ -378,8 +424,14 @@ impl UnoHsmPal { xr_wide[..field].copy_from_slice(&xr[..field]); eng.ecc_run(curve, &order_phase).await - }) + } .await; + let release = eng.release().await; + outcome?; + release?; + Ok(()) + } + .await; // Reject a degenerate signature (`r == 0` or `s == 0`) after the full // sequence — RFC 6979 §3.2 requires advancing to the next candidate, diff --git a/fw/plat/uno/fw/pal/src/self_test/pka.rs b/fw/plat/uno/fw/pal/src/self_test/pka.rs index 4f758f7f0..6b217a47a 100644 --- a/fw/plat/uno/fw/pal/src/self_test/pka.rs +++ b/fw/plat/uno/fw/pal/src/self_test/pka.rs @@ -30,9 +30,6 @@ use azihsm_fw_uno_trace::tracing::error; use super::vectors::ECDH_384_KAT; use super::vectors::ECDH_P384_PRIME_LE; use super::vectors::ECDSA_384_SIGN_KAT; -use super::vectors::ECDSA_P384_BASE_X_LE; -use super::vectors::ECDSA_P384_BASE_Y_LE; -use super::vectors::ECDSA_P384_ORDER_LE; use super::vectors::OAEP_KEK_SELF_TEST; use super::vectors::RSA_2K_CRT_KAT; use super::vectors::RSA_2K_MOD_EXP_KAT; @@ -51,9 +48,6 @@ const ECDH_384_LEN: usize = 48; /// NIST P-384 field-element size in bytes (point coordinate / scalar / digest). const P384_FIELD_LEN: usize = 48; -/// NIST P-384 Montgomery-form intermediate size in bytes. -const P384_MONT_LEN: usize = 52; - /// Runs the RSA-2048 mod-exp (private-key) known-answer test on PKA engine /// `engine`, followed by the OAEP-decode (SHA-256) KEK check. /// @@ -313,23 +307,14 @@ pub(super) async fn run_ecdh_on_engine( /// `engine`. /// /// Ported as-is from the reference firmware's `ecdsa_self_test_internal`: a -/// fixed-nonce ECDSA sign KAT built from the raw PKA Montgomery primitives -/// (point-multiply, modular reduction / inverse / multiply / add, and -/// Montgomery in/out conversions), reproducing the signature `(r, s)` for a -/// fixed private key `d`, nonce `k`, and message digest `e`, then comparing it -/// against the reference vectors. Running the full primitive sequence — rather -/// than the composite random-nonce `ecc_sign` — is required so the KAT is -/// deterministic and matches the FIPS submission exactly. -/// -/// The 12-step sequence (per engine) is: -/// 1. Montgomery-constant setup for the curve prime `p`. -/// 2. `R = k · G`; take the affine x-coordinate `x_R`. -/// 3. Montgomery-constant setup for the group order `n`. -/// 4. `r = x_R mod n` (must be non-zero). -/// 5. Convert `k`, `r`, `e`, `d` into Montgomery form (mod `n`). -/// 6. `k⁻¹ mod n`. -/// 7. `s = k⁻¹ · (e + r·d) mod n` (must be non-zero). -/// 8. Compare `(r, s)` against the expected signature. +/// fixed-nonce ECDSA sign KAT that reproduces the signature `(r, s)` for a +/// fixed private key `d`, nonce `k`, and message digest `e`, then compares it +/// against the reference vectors. The signature is produced by the shared +/// driver primitive [`UnoHsmPal::ecc_sign_with_k_on_engine`], pinned to +/// `engine`, so the self-test and the production PID-cert signer exercise the +/// identical raw-primitive Montgomery sequence. A fixed nonce (rather than the +/// random-nonce `ecc_sign`) is required so the KAT is deterministic and matches +/// the FIPS submission exactly. /// /// # Endianness /// @@ -337,8 +322,8 @@ pub(super) async fn run_ecdh_on_engine( /// the resulting `r` / `s` are compared against the little-endian expected /// values with no reversal. /// -/// Returns [`HsmError::SelfTestKatMismatch`] on a mismatch (or a degenerate -/// `r` / `s`), or any error surfaced by the PKA engine / allocator. +/// Returns [`HsmError::SelfTestKatMismatch`] on a mismatch, or any error +/// surfaced by the PKA engine / allocator. pub(super) async fn run_ecdsa_on_engine( pal: &UnoHsmPal, io: &UnoHsmIo, @@ -347,108 +332,25 @@ pub(super) async fn run_ecdsa_on_engine( let v = &ECDSA_384_SIGN_KAT; pal.alloc_scoped_async(io, async |scope| { - // Curve prime and group order (little-endian). `mont_scratch` receives - // the Montgomery-constant output; it is reused for both the prime and - // the order setups since only the engine-side state is consumed. - let prime = scope.dma_alloc(P384_FIELD_LEN)?; - prime.copy_from_slice(ECDH_P384_PRIME_LE); - let order_n = scope.dma_alloc(P384_FIELD_LEN)?; - order_n.copy_from_slice(ECDSA_P384_ORDER_LE); - let mont_scratch = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; - - // Base point `G` as a contiguous `X ‖ Y` block (little-endian). - let base_xy = scope.dma_alloc(P384_FIELD_LEN * 2)?; - base_xy[..P384_FIELD_LEN].copy_from_slice(ECDSA_P384_BASE_X_LE); - base_xy[P384_FIELD_LEN..].copy_from_slice(ECDSA_P384_BASE_Y_LE); - - // Fixed nonce `k`, message digest `e`, private key `d` (little-endian). + // Fixed nonce `k`, message digest `e`, and private key `d` + // (little-endian) from the KAT vector; `r` / `s` receive the signature. let k = scope.dma_alloc(P384_FIELD_LEN)?; k.copy_from_slice(v.k); let digest = scope.dma_alloc(P384_FIELD_LEN)?; digest.copy_from_slice(v.digest); let priv_key = scope.dma_alloc(P384_FIELD_LEN)?; priv_key.copy_from_slice(v.private_key); - - // Intermediate and output buffers. - let x_r = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; - // Modular reduction is a double-width primitive: the hardware reads a - // `2 · point_size` dividend. `x_r_wide` holds x_R in its low half with a - // zeroed high half so the reduction sees `x_R ‖ 0`. This mirrors the - // reference firmware, which reduces out of a zeroized scratch buffer. - let x_r_wide = scope.dma_alloc_zeroed(P384_FIELD_LEN * 2)?; let r = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; let s = scope.dma_alloc_zeroed(P384_FIELD_LEN)?; - let k_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let r_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let e_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let d_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let k_mont_inverse = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let s_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let t_mont = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let t_mont_dot_r = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - let s_mont_plus_t = scope.dma_alloc_zeroed(P384_MONT_LEN)?; - // Pin the requested engine for the operation, release afterwards. - let mut eng = pal.pka.acquire_engine(engine).await?; - let outcome = async { - let curve = UpkaEccCurve::P384; - - // 1. Establish the prime modulus, then R = k · G (x-coordinate). - eng.ecc_mont_const_calc(curve, &mut *mont_scratch, &*prime) - .await?; - eng.ecc_point_mul(curve, &mut *x_r, &*base_xy, &*k).await?; - - // 2. Switch modulus to the group order n, then r = x_R mod n. Stage - // x_R into the zero-padded double-width reduction input first. - x_r_wide[..P384_FIELD_LEN].copy_from_slice(&*x_r); - eng.ecc_mont_const_calc(curve, &mut *mont_scratch, &*order_n) - .await?; - eng.ecc_mod_reduction(curve, &mut *r, &*x_r_wide).await?; - if r.iter().all(|&b| b == 0) { - return Err(HsmError::SelfTestKatMismatch); - } - - // 3. Convert k, r, e, d into Montgomery form (mod n). - eng.ecc_mont_repr_in(curve, &mut *k_mont, &*k).await?; - eng.ecc_mont_repr_in(curve, &mut *r_mont, &*r).await?; - eng.ecc_mont_repr_in(curve, &mut *e_mont, &*digest).await?; - eng.ecc_mont_repr_in(curve, &mut *d_mont, &*priv_key) - .await?; - - // 4. k⁻¹ mod n. - eng.ecc_mod_inverse(curve, &mut *k_mont_inverse, &*k_mont) - .await?; - - // 5. s = k⁻¹ · (e + r·d) mod n, computed as: - // s_mont = k⁻¹ · e - // t_mont = k⁻¹ · d - // t_mont_dot_r = t_mont · r - // s_mont_plus_t = s_mont + t_mont_dot_r - eng.ecc_mod_mul(curve, &mut *s_mont, &*k_mont_inverse, &*e_mont) - .await?; - eng.ecc_mod_mul(curve, &mut *t_mont, &*k_mont_inverse, &*d_mont) - .await?; - eng.ecc_mod_mul(curve, &mut *t_mont_dot_r, &*t_mont, &*r_mont) - .await?; - eng.ecc_mod_add(curve, &mut *s_mont_plus_t, &*s_mont, &*t_mont_dot_r) - .await?; - - // 6. Convert s back to natural form. - eng.ecc_mont_repr_out(curve, &mut *s, &*s_mont_plus_t) - .await?; - if s.iter().all(|&b| b == 0) { - return Err(HsmError::SelfTestKatMismatch); - } - - Ok::<(), HsmError>(()) - } - .await; - let release = eng.release().await; - outcome?; - release?; + // Deterministic fixed-nonce sign on the pinned engine, reusing the + // shared driver primitive so the self-test and the production PID-cert + // signer exercise the identical Montgomery sequence. + pal.ecc_sign_with_k_on_engine(io, UpkaEccCurve::P384, engine, k, digest, priv_key, r, s) + .await?; - // 7. Compare (r, s) against the expected signature (little-endian, no - // reversal). + // Compare (r, s) against the expected signature (little-endian, no + // reversal). if &r[..] != v.r || &s[..] != v.s { error!( "selftest", diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index cd860ee99..8d37344c8 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -514,37 +514,6 @@ pub(super) const ECDH_384_KAT: Ecdh384Kat = Ecdh384Kat { ], }; -/// NIST P-384 base point G, X coordinate, in PKA little-endian operand order. -/// -/// Byte-for-byte identical to the reference firmware's `BASE384.x[0..48]`. Used -/// by the ECDSA sign self-test point multiplication `k · G`. -pub(super) const ECDSA_P384_BASE_X_LE: &[u8] = &[ - 0xb7, 0x0a, 0x76, 0x72, 0x38, 0x5e, 0x54, 0x3a, 0x6c, 0x29, 0x55, 0xbf, 0x5d, 0xf2, 0x02, 0x55, - 0x38, 0x2a, 0x54, 0x82, 0xe0, 0x41, 0xf7, 0x59, 0x98, 0x9b, 0xa7, 0x8b, 0x62, 0x3b, 0x1d, 0x6e, - 0x74, 0xad, 0x20, 0xf3, 0x1e, 0xc7, 0xb1, 0x8e, 0x37, 0x05, 0x8b, 0xbe, 0x22, 0xca, 0x87, 0xaa, -]; - -/// NIST P-384 base point G, Y coordinate, in PKA little-endian operand order. -/// -/// Byte-for-byte identical to the reference firmware's `BASE384.y[0..48]`. Used -/// by the ECDSA sign self-test point multiplication `k · G`. -pub(super) const ECDSA_P384_BASE_Y_LE: &[u8] = &[ - 0x5f, 0x0e, 0xea, 0x90, 0x7c, 0x1d, 0x43, 0x7a, 0x9d, 0x81, 0x7e, 0x1d, 0xce, 0xb1, 0x60, 0x0a, - 0xc0, 0xb8, 0xf0, 0xb5, 0x13, 0x31, 0xda, 0xe9, 0x7c, 0x14, 0x9a, 0x28, 0xbd, 0x1d, 0xf4, 0xf8, - 0x29, 0xdc, 0x92, 0x92, 0xbf, 0x98, 0x9e, 0x5d, 0x6f, 0x2c, 0x26, 0x96, 0x4a, 0xde, 0x17, 0x36, -]; - -/// NIST P-384 group order n in PKA little-endian operand order. -/// -/// Byte-for-byte identical to the reference firmware's `ORDER384.n[0..48]`. The -/// ECDSA sign self-test establishes it as the active modulus (via a -/// Montgomery-constant setup) for the `mod n` steps. -pub(super) const ECDSA_P384_ORDER_LE: &[u8] = &[ - 0x73, 0x29, 0xc5, 0xcc, 0x6a, 0x19, 0xec, 0xec, 0x7a, 0xa7, 0xb0, 0x48, 0xb2, 0x0d, 0x1a, 0x58, - 0xdf, 0x2d, 0x37, 0xf4, 0x81, 0x4d, 0x63, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, -]; - /// ECDSA P-384 (SHA-256 component) deterministic sign known-answer test vector. /// /// Ported verbatim from the reference firmware's `KAT_ECC_TEST_384_TEST_VECTORS` From ff7a0a41fd57305bc9d0e92dc182e55e35f80b94 Mon Sep 17 00:00:00 2001 From: Abhijit Nathwani Date: Fri, 31 Jul 2026 16:39:46 -0700 Subject: [PATCH 19/19] Add AES-KWP and AES-ECB KATs --- fw/plat/uno/fw/app/src/main.rs | 4 ++ fw/plat/uno/fw/pal/src/self_test/aes_ecb.rs | 57 ++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/aes_kwp.rs | 61 +++++++++++++++++ fw/plat/uno/fw/pal/src/self_test/mod.rs | 12 ++-- fw/plat/uno/fw/pal/src/self_test/vectors.rs | 75 +++++++++++++++++++++ 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 fw/plat/uno/fw/pal/src/self_test/aes_ecb.rs create mode 100644 fw/plat/uno/fw/pal/src/self_test/aes_kwp.rs diff --git a/fw/plat/uno/fw/app/src/main.rs b/fw/plat/uno/fw/app/src/main.rs index 6327f2fc5..2858c7bb3 100644 --- a/fw/plat/uno/fw/app/src/main.rs +++ b/fw/plat/uno/fw/app/src/main.rs @@ -37,6 +37,10 @@ #![no_std] #![no_main] +// The `handle_io` Embassy task future is a deeply nested async state machine; +// with `trace-uart` enabled its layout computation exceeds rustc's default +// recursion limit of 128. Raise it to give headroom. +#![recursion_limit = "256"] mod trampoline; diff --git a/fw/plat/uno/fw/pal/src/self_test/aes_ecb.rs b/fw/plat/uno/fw/pal/src/self_test/aes_ecb.rs new file mode 100644 index 000000000..bda7f333b --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/aes_ecb.rs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! AES-256-ECB cryptographic algorithm self-test (CAST). +//! +//! Runs a fixed NIST SP 800-38A known-answer vector through the HSM AES +//! engine's ECB-decrypt path and compares the recovered plaintext against the +//! expected value. 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_ECB_256_KAT; +use crate::UnoHsmIo; +use crate::UnoHsmPal; + +/// Runs the AES-256-ECB known-answer test (decrypt direction) against the HSM +/// AES engine. +/// +/// Decrypts the KAT ciphertext and verifies the recovered plaintext. Returns +/// [`HsmError::SelfTestKatMismatch`] on any mismatch (or any error surfaced by +/// the AES engine / allocator). +pub(super) async fn run_aes_ecb(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> { + let v = &AES_ECB_256_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // Stage the key and ciphertext into DMA-visible memory. + let key = scope.dma_alloc(v.key.len())?; + key.copy_from_slice(v.key); + 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_ecb_enc_dec(io, AesOp::Decrypt, &*key, &*ct, &mut *pt_out) + .await?; + + // KAT vectors are public, fixed test data — a plain slice comparison is + // correct; no constant-time compare is needed. + if &pt_out[..] != v.plaintext { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "AES-ECB decrypt KAT mismatch" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/aes_kwp.rs b/fw/plat/uno/fw/pal/src/self_test/aes_kwp.rs new file mode 100644 index 000000000..928df8222 --- /dev/null +++ b/fw/plat/uno/fw/pal/src/self_test/aes_kwp.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! AES Key Wrap with Padding (RFC 5649) cryptographic algorithm self-test +//! (CAST). +//! +//! Runs a fixed NIST CAVP known-answer vector through the HSM AES engine's +//! KWP-AD (authenticated decryption / unwrap) path and compares the recovered +//! plaintext against the expected value. This validates the same engine path +//! the production RSA-unwrap flow depends on. 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::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_KWP_256_KAT; +use crate::UnoHsmIo; +use crate::UnoHsmPal; + +/// Runs the AES-256 KWP (RFC 5649) known-answer test against the HSM AES +/// engine. +/// +/// Unwraps the KAT `wrapped` blob under the KAT `key` and verifies both the +/// recovered length (the Message Length Indicator) and the recovered plaintext +/// bytes. Returns [`HsmError::SelfTestKatMismatch`] on any mismatch (or any +/// error surfaced by the AES engine / allocator). +pub(super) async fn run_aes_kwp(pal: &UnoHsmPal, io: &UnoHsmIo) -> HsmResult<()> { + let v = &AES_KWP_256_KAT; + + pal.alloc_scoped_async(io, async |scope| { + // Stage the KEK and wrapped blob into DMA-visible memory. + let key = scope.dma_alloc(v.key.len())?; + key.copy_from_slice(v.key); + let input = scope.dma_alloc(v.wrapped.len())?; + input.copy_from_slice(v.wrapped); + + // Output must be at least `wrapped.len() - 8` (the AIV semiblock is + // stripped). For this vector that equals the plaintext length. + let out = scope.dma_alloc_zeroed(v.wrapped.len() - 8)?; + let mli = pal.aes_kwp_unwrap(io, &*key, &*input, &mut *out).await?; + + // KAT vectors are public, fixed test data — a plain slice comparison is + // correct; no constant-time compare is needed. + if mli != v.plaintext.len() || &out[..mli] != v.plaintext { + error!( + "selftest", + HsmError::SelfTestKatMismatch, + "AES-KWP unwrap KAT mismatch" + ); + return Err(HsmError::SelfTestKatMismatch); + } + + Ok::<(), HsmError>(()) + }) + .await +} diff --git a/fw/plat/uno/fw/pal/src/self_test/mod.rs b/fw/plat/uno/fw/pal/src/self_test/mod.rs index 11225dc18..75ef4fe40 100644 --- a/fw/plat/uno/fw/pal/src/self_test/mod.rs +++ b/fw/plat/uno/fw/pal/src/self_test/mod.rs @@ -18,13 +18,15 @@ //! //! # Status //! -//! AES-256-CBC, HKDF, KBKDF, per-engine RSA-2048 mod-exp (standard and CRT), -//! ECDH-P384, ECDSA-P384, and the RNG/DRBG FW-mode KAT are implemented. Further -//! tests are appended to [`run_pre_op`] as they are added — each is a direct -//! call, with per-PKA-engine tests wrapped in a +//! AES-256-CBC, AES-256-ECB, AES-256-KWP, HKDF, KBKDF, per-engine RSA-2048 +//! mod-exp (standard and CRT), ECDH-P384, ECDSA-P384, and the RNG/DRBG FW-mode +//! KAT are implemented. Further tests 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 aes_ecb; +mod aes_kwp; mod kdf; mod pka; mod vectors; @@ -41,6 +43,8 @@ use crate::UnoHsmPal; /// 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?; + aes_ecb::run_aes_ecb(pal, io).await?; + aes_kwp::run_aes_kwp(pal, io).await?; kdf::run_hkdf(pal, io).await?; kdf::run_kbkdf(pal, io).await?; for engine in 0..pka::PKA_ENGINES { diff --git a/fw/plat/uno/fw/pal/src/self_test/vectors.rs b/fw/plat/uno/fw/pal/src/self_test/vectors.rs index 8d37344c8..d324fd4c7 100644 --- a/fw/plat/uno/fw/pal/src/self_test/vectors.rs +++ b/fw/plat/uno/fw/pal/src/self_test/vectors.rs @@ -60,6 +60,81 @@ pub(super) const AES_CBC_256_KAT: AesCbcKat = AesCbcKat { ], }; +/// AES Key Wrap with Padding (RFC 5649) known-answer vector. +/// +/// Holds a KEK, the wrapped key data, and the expected recovered plaintext so +/// the self-test can exercise the KWP-AD (authenticated decryption / unwrap) +/// path that the production RSA-unwrap flow relies on. +pub(super) struct AesKwpKat { + /// Key-encryption key (16 / 24 / 32 bytes). + pub key: &'static [u8], + + /// Wrapped key data (AIV ‖ padded plaintext; ≥ 16 bytes, multiple of 8). + pub wrapped: &'static [u8], + + /// Expected recovered plaintext (the Message Length Indicator bytes). + pub plaintext: &'static [u8], +} + +/// AES-256 Key Wrap with Padding known-answer test vector (KWP-AD). +/// +/// Source: NIST CAVP `kwtestvectors` (`KWP_AD_256`, PLAINTEXT LENGTH = 64, +/// COUNT = 0). Unwrapping the 16-byte `wrapped` blob under `key` must recover +/// the 8-byte `plaintext`. +pub(super) const AES_KWP_256_KAT: AesKwpKat = AesKwpKat { + key: &[ + 0x35, 0x17, 0xf0, 0xef, 0xa7, 0xf0, 0xc4, 0xd7, 0x4f, 0x91, 0xaf, 0x83, 0xec, 0xe5, 0xe7, + 0x50, 0x3b, 0xcc, 0x5a, 0xb8, 0x29, 0x07, 0xa6, 0xe4, 0xb7, 0xed, 0x34, 0xd8, 0x7b, 0x69, + 0xab, 0x1d, + ], + wrapped: &[ + 0x0b, 0x06, 0xa9, 0xb6, 0x35, 0xd5, 0x0c, 0xda, 0x9d, 0x42, 0x10, 0xcb, 0x3a, 0x71, 0xf9, + 0x90, + ], + plaintext: &[0x89, 0x7e, 0x04, 0x56, 0xb2, 0x89, 0xad, 0x31], +}; + +/// AES-ECB known-answer vector. +/// +/// Holds a ciphertext/plaintext pair so the self-test can exercise the +/// single-block ECB-decrypt path (used by software AES unwrap paths). +pub(super) struct AesEcbKat { + /// Cipher key (16 / 24 / 32 bytes). + pub key: &'static [u8], + + /// Ciphertext (block multiple). + pub ciphertext: &'static [u8], + + /// Expected plaintext for `ciphertext` under `key`. + pub plaintext: &'static [u8], +} + +/// AES-256-ECB known-answer test vector (decrypt direction). +/// +/// Source: NIST SP 800-38A, F.1.6 (ECB-AES256.Decrypt) — all four blocks. +/// Shares the AES-256 key with [`AES_CBC_256_KAT`]. +pub(super) const AES_ECB_256_KAT: AesEcbKat = AesEcbKat { + key: &[ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, + 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, + 0xdf, 0xf4, + ], + ciphertext: &[ + 0xf3, 0xee, 0xd1, 0xbd, 0xb5, 0xd2, 0xa0, 0x3c, 0x06, 0x4b, 0x5a, 0x7e, 0x3d, 0xb1, 0x81, + 0xf8, 0x59, 0x1c, 0xcb, 0x10, 0xd4, 0x10, 0xed, 0x26, 0xdc, 0x5b, 0xa7, 0x4a, 0x31, 0x36, + 0x28, 0x70, 0xb6, 0xed, 0x21, 0xb9, 0x9c, 0xa6, 0xf4, 0xf9, 0xf1, 0x53, 0xe7, 0xb1, 0xbe, + 0xaf, 0xed, 0x1d, 0x23, 0x30, 0x4b, 0x7a, 0x39, 0xf9, 0xf3, 0xff, 0x06, 0x7d, 0x8d, 0x8f, + 0x9e, 0x24, 0xec, 0xc7, + ], + plaintext: &[ + 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, + 0x2a, 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, + 0x8e, 0x51, 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, + 0x0a, 0x52, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, + 0xe6, 0x6c, 0x37, 0x10, + ], +}; + /// HKDF (RFC 5869, HMAC-SHA-256) known-answer vector. /// /// Exercises HKDF-Extract (`salt` + `ikm` → PRK) followed by HKDF-Expand