blob: d82f6dec1c9d521b6fc24e1f65b44d775be7b3f9 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! Software RFC-2104 HMAC over the HACE SHA-2 hasher.
//!
//! Per `plans/goal.md` §2.1 / §3 item 2, HMAC is **not** the engine-native
//! `aspeed_hash_*_hmac` path: it is implemented in software as
//! `H((K0 ^ opad) ‖ H((K0 ^ ipad) ‖ msg))` on top of the already
//! parity-verified [`HaceDigest`] digest path. The key-reduction threshold is
//! the RFC-2104-correct, algorithm-dependent `key_len > block_size` (64 for
//! SHA-256, 128 for SHA-384/512) — *not* `aspeed-rust`'s flat `>128`.
//!
//! One-shot, like the authoritative model (goal.md §1.7: "HMAC — NOT
//! streaming"). Each sub-hash runs through the verified public [`HaceDigest`]
//! path (`HaceDevice` → `from_device` → `DigestInit`).
//!
//! Key reduction always copies the raw key bytes through a `.ram_nc` staging
//! buffer before DMA, ensuring the HACE engine reads from non-cacheable SRAM
//! regardless of where the caller allocated the `HmacKey` (mirrors
//! aspeed-rust's `hash_key` strategy).
//!
//! Correctness authority: published RFC-4231 known-answer vectors (§2.1).
use super::device::HaceDevice;
use super::digest::HaceDigest;
use super::error::HaceError;
use core::marker::PhantomData;
use openprot_hal_blocking::digest::scoped::{DigestInit, DigestOp};
use openprot_hal_blocking::digest::{Digest, Sha2_256, Sha2_384, Sha2_512};
use openprot_hal_blocking::mac::scoped::{MacInit, MacOp};
use openprot_hal_blocking::mac::{
ErrorType as MacErrorType, HmacSha2_256, HmacSha2_384, HmacSha2_512, KeyHandle,
};
/// Maximum HMAC key length accepted by [`HmacKey`]. Comfortably covers the
/// RFC-4231 vectors (longest key 131 B); longer keys are reduced via `H(K)`
/// anyway when `len > block_size`.
pub const HMAC_KEY_CAP: usize = 256;
/// Largest HMAC message accepted (one-shot, like the authoritative model).
pub const HMAC_MSG_CAP: usize = 1024;
// ----- DMA-safe key staging buffer ------------------------------------
//
// The HACE engine performs DMA from the SG-list source addresses. When a
// key arrives from an arbitrary call site (e.g. from a `HmacKey` on the
// caller's stack, which is in cacheable SRAM), the DMA engine may observe
// stale zeros from physical RAM instead of the CPU-cached key bytes.
// Aspeed-rust fixes this in `hash_key()` by always copying the key bytes
// into `ctx.buffer` (`.ram_nc`) before hashing. We do the same here: any
// key that requires reduction (len > block_size) is first CPU-copied into
// this `.ram_nc` staging buffer, then `one_shot!` reads it from there.
struct HmacKeyNc(core::cell::UnsafeCell<[u8; HMAC_KEY_CAP]>);
// SAFETY: single-threaded HACE driver; key-reduction calls are serialised by
// the single-instance/non-reentrant `HaceDevice` contract.
unsafe impl Sync for HmacKeyNc {}
#[unsafe(link_section = ".ram_nc")]
static HMAC_KEY_NC: HmacKeyNc = HmacKeyNc(core::cell::UnsafeCell::new([0u8; HMAC_KEY_CAP]));
/// Run one full digest of `$input` via the verified public path, yielding the
/// algorithm's `Digest`. Used for all three HMAC sub-hashes so HMAC rides
/// exactly the digest path the KAT suite covers.
macro_rules! one_shot {
($inner:ty, $algo:expr, $pb:expr, $input:expr) => {{
// SAFETY: the whole HACE driver is single-threaded / non-reentrant;
// the HMAC controller upholds that contract for its sub-hashes.
let dev = unsafe { HaceDevice::new_global(|_| core::hint::spin_loop()) };
let mut dev = dev.with_timeout_polls($pb);
// SAFETY: same single-threaded exclusivity contract.
let mut dd = unsafe { HaceDigest::<$inner>::from_device(&mut dev) };
let mut op = dd.init($algo)?;
op.update($input)?;
op.finalize()?
}};
}
/// Owned, bounded HMAC key handle. Variable length up to [`HMAC_KEY_CAP`].
#[derive(Clone)]
pub struct HmacKey {
bytes: [u8; HMAC_KEY_CAP],
len: usize,
}
impl HmacKey {
/// Build a key from a byte slice.
///
/// Returns [`HaceError::InvalidInput`] if `key.len() > HMAC_KEY_CAP`.
pub fn from_slice(key: &[u8]) -> Result<Self, HaceError> {
if key.len() > HMAC_KEY_CAP {
return Err(HaceError::InvalidInput);
}
let mut bytes = [0u8; HMAC_KEY_CAP];
if let Some(dst) = bytes.get_mut(..key.len()) {
dst.copy_from_slice(key);
}
Ok(Self {
bytes,
len: key.len(),
})
}
#[inline]
fn as_slice(&self) -> &[u8] {
self.bytes.get(..self.len).unwrap_or(&[])
}
}
impl KeyHandle for HmacKey {}
/// HMAC controller. Holds only the poll budget; each sub-hash binds the
/// singleton HACE through the standard [`HaceDevice`] path.
pub struct HaceHmac {
poll_budget: u32,
}
impl HaceHmac {
pub(crate) fn new(poll_budget: u32) -> Self {
Self { poll_budget }
}
/// Construct an HMAC controller from a [`super::device::HaceDevice`].
///
/// # Safety
/// Caller must ensure no concurrent or reentrant HACE access for the
/// lifetime of HMAC operations created from this controller.
pub unsafe fn from_device<Y: FnMut(u32)>(device: &mut super::device::HaceDevice<Y>) -> Self {
Self::new(device.poll_budget)
}
}
impl MacErrorType for HaceHmac {
type Error = HaceError;
}
/// In-flight HMAC operation: retained `K0^ipad` / `K0^opad` blocks plus the
/// buffered message. The HMAC is computed entirely at `finalize`.
pub struct HaceHmacCtx<Inner> {
ipad: [u8; 128],
opad: [u8; 128],
block: usize,
msg: [u8; HMAC_MSG_CAP],
msg_len: usize,
poll_budget: u32,
_inner: PhantomData<Inner>,
}
impl<Inner> MacErrorType for HaceHmacCtx<Inner> {
type Error = HaceError;
}
macro_rules! hmac_variant {
($mac:ty, $inner:ty, $algo:expr, $b:expr, $nw:expr) => {
impl MacInit<$mac> for HaceHmac {
type Key = HmacKey;
type OpContext<'a>
= HaceHmacCtx<$inner>
where
Self: 'a;
fn init<'a>(
&'a mut self,
_algo: $mac,
key: HmacKey,
) -> Result<Self::OpContext<'a>, HaceError> {
let pb = self.poll_budget;
let k = key.as_slice();
// K0: key reduced to <= block_size, then zero-padded to block.
// RFC-2104-correct threshold: reduce only when len > block_size.
let mut k0 = [0u8; 128];
if k.len() > $b {
// Copy the raw key into the .ram_nc staging buffer so that
// the HACE DMA engine reads from non-cacheable SRAM.
// Passing a stack pointer directly as the DMA source can
// produce wrong results when the CPU cache has not been
// written back (observed: SHA-512 key reduction reads zeros
// from physical RAM while correct bytes are cache-resident).
// This mirrors aspeed-rust's `hash_key()` which always does
// `ctx.buffer[..key_len].copy_from_slice(key_bytes)` first.
let key_nc: &[u8] = unsafe {
let buf = &mut *HMAC_KEY_NC.0.get();
if let Some(dst) = buf.get_mut(..k.len()) {
dst.copy_from_slice(k);
}
buf.get(..k.len()).unwrap_or(&[])
};
let kh = one_shot!($inner, $algo, pb, key_nc);
let hb = kh.as_bytes();
if let Some(dst) = k0.get_mut(..hb.len()) {
dst.copy_from_slice(hb);
}
} else {
if let Some(dst) = k0.get_mut(..k.len()) {
dst.copy_from_slice(k);
}
}
let mut ipad = [0u8; 128];
let mut opad = [0u8; 128];
ipad[..$b].copy_from_slice(&k0[..$b]);
opad[..$b].copy_from_slice(&k0[..$b]);
ipad[..$b].iter_mut().for_each(|b| *b ^= 0x36);
opad[..$b].iter_mut().for_each(|b| *b ^= 0x5c);
Ok(HaceHmacCtx {
ipad,
opad,
block: $b,
msg: [0u8; HMAC_MSG_CAP],
msg_len: 0,
poll_budget: pb,
_inner: PhantomData,
})
}
}
impl MacOp for HaceHmacCtx<$inner> {
type Output = Digest<$nw>;
fn update(&mut self, input: &[u8]) -> Result<(), HaceError> {
let end = self
.msg_len
.checked_add(input.len())
.ok_or(HaceError::InvalidInput)?;
if end > HMAC_MSG_CAP {
return Err(HaceError::InvalidInput);
}
let dst = self
.msg
.get_mut(self.msg_len..end)
.ok_or(HaceError::InvalidInput)?;
// Element-wise copy: `dst` is `end - self.msg_len == input.len()`
// long, but the optimizer cannot prove that through the range
// slice, so `copy_from_slice` would retain its length-mismatch
// panic branch. The zip-copy is provably panic-free.
for (d, s) in dst.iter_mut().zip(input.iter()) {
*d = *s;
}
self.msg_len = end;
Ok(())
}
fn finalize(self) -> Result<Self::Output, HaceError> {
let b = self.block;
let pb = self.poll_budget;
// inner = H(K0^ipad ‖ msg)
//
// Feed ipad and msg via two update() calls — no contiguous scratch
// buffer needed on the stack. update() copies each slice through
// ctx.buffer (.ram_nc) before DMA, so stack-resident sources
// (ipad, msg) are DMA-safe (D1/D2 guarantee).
let inner = {
// SAFETY: single-threaded HMAC controller upholds non-reentrant contract.
let dev = unsafe { HaceDevice::new_global(|_| core::hint::spin_loop()) };
let mut dev = dev.with_timeout_polls(pb);
// SAFETY: same single-threaded exclusivity contract.
let mut dd = unsafe { HaceDigest::<$inner>::from_device(&mut dev) };
let mut op = dd.init($algo)?;
op.update(self.ipad.get(..b).unwrap_or(&[]))?;
op.update(self.msg.get(..self.msg_len).unwrap_or(&[]))?;
op.finalize()?
};
let inner_bytes = inner.as_bytes();
// outer = H(K0^opad ‖ inner)
// SAFETY: same single-threaded exclusivity contract.
let dev = unsafe { HaceDevice::new_global(|_| core::hint::spin_loop()) };
let mut dev = dev.with_timeout_polls(pb);
// SAFETY: same contract.
let mut dd = unsafe { HaceDigest::<$inner>::from_device(&mut dev) };
let mut op = dd.init($algo)?;
op.update(self.opad.get(..b).unwrap_or(&[]))?;
op.update(inner_bytes)?;
op.finalize()
}
}
};
}
// Block sizes / output words: SHA-256 -> 64 B block, 8 words (32 B);
// SHA-384 -> 128 B, 12 words (48 B); SHA-512 -> 128 B, 16 words (64 B).
hmac_variant!(HmacSha2_256, Sha2_256, Sha2_256, 64, 8);
hmac_variant!(HmacSha2_384, Sha2_384, Sha2_384, 128, 12);
hmac_variant!(HmacSha2_512, Sha2_512, Sha2_512, 128, 16);