diff --git a/ddi/tbor/types/src/hash.rs b/ddi/tbor/types/src/hash.rs new file mode 100644 index 000000000..48f7a83cf --- /dev/null +++ b/ddi/tbor/types/src/hash.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Host-side wrapper for the TBOR `Hash` command. +//! +//! `Hash` is an **in-session** command (Crypto-Officer or +//! Crypto-User) that computes a SHA-256 / 384 / 512 digest of a +//! host-supplied message and returns it. It carries no key and touches no +//! partition state — a pure hashing utility. +//! +//! `algo` is a raw 1-byte discriminant (the firmware types it as the +//! `HashAlgo` open-enum; this host crate is firewalled from the firmware +//! PAL types). + +use alloc::vec::Vec; + +use crate::tbor; + +/// TBOR opcode for `Hash`. +pub const TBOR_OP_HASH: u8 = 0x1B; + +/// Maximum message length (bytes) accepted by `Hash`. +pub const HASH_MSG_MAX_LEN: usize = 2048; +/// Maximum digest length (bytes) — the SHA-512 digest. +pub const HASH_DIGEST_MAX_LEN: usize = 64; + +/// `HashAlgo` discriminant for SHA-256 (32-byte digest). +pub const HASH_ALGO_SHA256: u8 = 1; +/// `HashAlgo` discriminant for SHA-384 (48-byte digest). +pub const HASH_ALGO_SHA384: u8 = 2; +/// `HashAlgo` discriminant for SHA-512 (64-byte digest). +pub const HASH_ALGO_SHA512: u8 = 3; + +/// Host-facing TBOR `Hash` request. +#[tbor(opcode = TBOR_OP_HASH, session_ctrl = in_session)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TborHashReq { + /// Session id this request is bound to. + #[tbor(session_id)] + pub session_id: u16, + + /// Digest algorithm, 1-byte `HashAlgo` (see `HASH_ALGO_*`). + pub algo: u8, + + /// The message to hash, up to `HASH_MSG_MAX_LEN` bytes. + #[tbor(max_len = 2048)] + pub msg: Vec, +} + +/// Host-facing TBOR `Hash` response. +#[tbor(response)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TborHashResp { + /// The natural (big-endian) digest, exactly the algorithm's length + /// (32 / 48 / 64 B for SHA-256 / 384 / 512). + #[tbor(max_len = 64)] + pub digest: Vec, +} + +#[cfg(test)] +mod tests { + use azihsm_ddi_tbor_types::TborOpReq; + + use super::*; + + #[test] + fn request_encodes_fields() { + let req = TborHashReq { + session_id: 9, + algo: HASH_ALGO_SHA384, + msg: alloc::vec![0x61u8; 64], + }; + let mut buf = [0u8; 512]; + let frame = req.encode_request(&mut buf).expect("encode"); + assert!( + frame.contains(&HASH_ALGO_SHA384), + "encoded frame must carry the sha-mode discriminant", + ); + } +} diff --git a/ddi/tbor/types/src/lib.rs b/ddi/tbor/types/src/lib.rs index 2b3bd2ad9..f656b3442 100644 --- a/ddi/tbor/types/src/lib.rs +++ b/ddi/tbor/types/src/lib.rs @@ -81,6 +81,7 @@ impl From for u8 { mod api_rev; mod evidence; mod get_unwrapping_key; +mod hash; mod key_report; mod part_final; mod part_info; @@ -103,6 +104,7 @@ mod unwrap_key; pub use api_rev::*; pub use evidence::*; pub use get_unwrapping_key::*; +pub use hash::*; pub use key_report::*; pub use part_final::*; pub use part_info::*; diff --git a/ddi/tbor/types/tests/commands/hash.rs b/ddi/tbor/types/tests/commands/hash.rs new file mode 100644 index 000000000..1e210ea4d --- /dev/null +++ b/ddi/tbor/types/tests/commands/hash.rs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Integration tests for the TBOR `Hash` command. +//! +//! `Hash` computes a SHA-256 / 384 / 512 digest of a host-supplied +//! message. These tests hash several messages on-device and verify the +//! result byte-for-byte against the digest computed on the host with +//! `azihsm_crypto` (natural big-endian output), for every algorithm. + +#![cfg(feature = "emu")] + +use azihsm_crypto::HashAlgo as CryptoHashAlgo; +use azihsm_crypto::Hasher; +use azihsm_ddi_tbor_types::TborHashReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::HASH_ALGO_SHA256; +use azihsm_ddi_tbor_types::HASH_ALGO_SHA384; +use azihsm_ddi_tbor_types::HASH_ALGO_SHA512; + +use crate::commands::sd_sealing_key_gen::finalized_co_session; +use crate::harness::TestCtx; + +/// Hash `msg` on-device with `algo`, returning the digest. +fn device_digest(ctx: &TestCtx, session_id: u16, algo: u8, msg: Vec) -> Vec { + ctx.tbor(&TborHashReq { + session_id, + algo, + msg, + }) + .expect("Hash") + .digest +} + +/// Compute the expected digest on the host with `azihsm_crypto`. +fn host_digest(algo: u8, msg: &[u8]) -> Vec { + let mut crypto_algo = match algo { + HASH_ALGO_SHA256 => CryptoHashAlgo::sha256(), + HASH_ALGO_SHA384 => CryptoHashAlgo::sha384(), + HASH_ALGO_SHA512 => CryptoHashAlgo::sha512(), + _ => unreachable!("unknown hash algo"), + }; + Hasher::hash_vec(&mut crypto_algo, msg).expect("host hash") +} + +/// Expected digest length for a mode. +fn digest_len(algo: u8) -> usize { + match algo { + HASH_ALGO_SHA256 => 32, + HASH_ALGO_SHA384 => 48, + HASH_ALGO_SHA512 => 64, + _ => unreachable!(), + } +} + +#[test] +fn hash_matches_host_all_algos_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + + // A few messages: short, empty, and a longer non-trivial buffer. + let short = b"abc".to_vec(); + let empty: Vec = Vec::new(); + let long: Vec = (0..1000u32).map(|i| (i % 251) as u8).collect(); + + for msg in [short, empty, long] { + for mode in [HASH_ALGO_SHA256, HASH_ALGO_SHA384, HASH_ALGO_SHA512] { + let dev = device_digest(&ctx, session.session_id, mode, msg.clone()); + assert_eq!( + dev.len(), + digest_len(mode), + "digest length must match the algorithm (mode {mode}, msg {} B)", + msg.len(), + ); + assert_eq!( + dev, + host_digest(mode, &msg), + "device digest must match host SHA (mode {mode}, msg {} B)", + msg.len(), + ); + } + } +} + +#[test] +fn hash_unknown_algo_rejected_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + + // Mode discriminant `0` is not one of SHA-256 / 384 / 512. + ctx.expect_fw_reject( + &TborHashReq { + session_id: session.session_id, + algo: 0, + msg: b"abc".to_vec(), + }, + TborStatus::InvalidArg, + ); +} diff --git a/ddi/tbor/types/tests/commands/mod.rs b/ddi/tbor/types/tests/commands/mod.rs index d6c0fdf8f..326e12e3e 100644 --- a/ddi/tbor/types/tests/commands/mod.rs +++ b/ddi/tbor/types/tests/commands/mod.rs @@ -10,6 +10,7 @@ pub mod default_psk_gate; pub mod forward_compat; pub mod fw_error_decode; pub mod get_unwrapping_key; +pub mod hash; pub mod key_report; pub mod open_session; pub mod part_final; diff --git a/docs/tbor-ddi/README.md b/docs/tbor-ddi/README.md index 90e7bb73d..f772b7a33 100644 --- a/docs/tbor-ddi/README.md +++ b/docs/tbor-ddi/README.md @@ -67,6 +67,7 @@ single `none` TOC placeholder and no typed body fields. | `0x10` | `KeyReport` | InSession | [`commands/key_report.md`](./commands/key_report.md) | | `0x13` | `GetUnwrappingKey` | InSession | [`commands/get_unwrapping_key.md`](./commands/get_unwrapping_key.md) | | `0x14` | `UnwrapKey` | InSession | [`commands/unwrap_key.md`](./commands/unwrap_key.md) | +| `0x1B` | `Hash` | InSession | [`commands/hash.md`](./commands/hash.md) | ## Default-PSK gate diff --git a/docs/tbor-ddi/commands/hash.md b/docs/tbor-ddi/commands/hash.md new file mode 100644 index 000000000..0fe14eb68 --- /dev/null +++ b/docs/tbor-ddi/commands/hash.md @@ -0,0 +1,63 @@ + + +# Hash (Opcode 0x1B) + +**Handler:** `fw/core/lib/src/ddi/tbor/hash.rs` +**Session:** InSession + +## Description + +Computes a cryptographic hash (SHA-256 / 384 / 512) of a host-supplied +message and returns the digest. A pure hashing utility — it carries no +key, no scope, and touches no partition state. This is the TBOR analogue +of MBOR `ShaDigest`. + +The handler uses the reserve-then-fill pattern: the response frame is +encoded with the digest slot reserved, then the PAL hashes straight into +it — no intermediate buffer, no copy. The digest is emitted in natural +(big-endian) byte order. + +Available to **both Crypto-Officer and Crypto-User** sessions. + +## Request + +### TOC entries + +| Offset | Field | Type | Description | +|---|---|---|---| +| 4 | `session_id` | `session_id` (inline) | Session this request is bound to; cross-checked against the SQE-carried session id. | +| — | `algo` | `u8` (inline) | Digest algorithm ([`HashAlgo`]): `1` = SHA-256, `2` = SHA-384, `3` = SHA-512. | +| — | `msg` | `buffer` (≤ 2048 B) | The message to hash. | + +### Data section + +Carries the message bytes. + +## Response + +### TOC entries + +| Offset | Field | Type | Description | +|---|---|---|---| +| 8 | `digest` | `buffer` (32 / 48 / 64 B) | The natural (big-endian) digest, exactly the algorithm's length. | + +### Data section + +Carries the digest. + +## Errors + +| Error | Cause | +|---|---| +| `SessionNotFound` | `session_id` does not refer to an `Active` slot | +| `InvalidArg` | Unknown `algo` | +| `DefaultPskMustRotate` | The calling role's PSK is still the compiled-in default (dispatcher, pre-handler) | +| `DdiDecodeFailed` | Malformed request body (e.g. `msg` exceeds 2048 B) | + +## See also + +- Wire encoding: [TBOR specification](../../../fw/core/ddi/tbor/docs/spec.md) +- Wire schema: `fw/core/ddi/tbor/types/src/hash.rs` diff --git a/fw/core/ddi/tbor/types/src/hash.rs b/fw/core/ddi/tbor/types/src/hash.rs new file mode 100644 index 000000000..8f43cf5a1 --- /dev/null +++ b/fw/core/ddi/tbor/types/src/hash.rs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `Hash` wire schema. +//! +//! `Hash` is an in-session command that computes a cryptographic hash +//! (SHA-256 / 384 / 512) of a host-supplied message and returns the +//! digest. It carries no key and touches no partition state — it is a +//! pure hashing utility, the TBOR analogue of MBOR `ShaDigest`. +//! +//! Inputs: +//! +//! * `session_id` — TOC-carried session id; cross-checked by the dispatcher. +//! * `algo` — the [`HashAlgo`] selecting the digest algorithm. +//! * `msg` — the message to hash, up to [`HASH_MSG_MAX_LEN`] bytes. +//! +//! Outputs: +//! +//! * `digest` — the natural (big-endian) digest, exactly the algorithm's +//! length (32 / 48 / 64 B for SHA-256 / 384 / 512). + +use azihsm_fw_ddi_tbor_api::tbor; + +use crate::key_props::HashAlgo; + +/// TBOR opcode for `Hash`. +pub const TBOR_OP_HASH: u8 = 0x1B; + +/// Maximum message length (bytes) accepted by `Hash`. Pinned into +/// the `#[tbor(buffer, max_len = 2048)]` literal on +/// [`TborHashReq::msg`]. +pub const HASH_MSG_MAX_LEN: usize = 2048; + +/// Maximum digest length (bytes) — the SHA-512 digest. Pinned into the +/// `#[tbor(buffer, max_len = 64)]` literal on +/// [`TborHashResp::digest`]. +pub const HASH_DIGEST_MAX_LEN: usize = 64; + +/// `Hash` request schema. +#[tbor(opcode = 0x1B)] +pub struct TborHashReq<'a> { + /// CO/CU session id this request is bound to. + #[tbor(session_id)] + pub session_id: SessionId, + + /// Digest algorithm, 1-byte [`HashAlgo`]. + #[tbor(U8)] + pub algo: HashAlgo, + + /// The message to hash, up to [`HASH_MSG_MAX_LEN`] bytes. + #[tbor(buffer, max_len = 2048)] + pub msg: &'a [u8], +} + +/// `Hash` response schema. +/// +/// `digest` is `#[tbor(mutable)]` so the handler can reserve the slot and +/// have the PAL write the digest straight into it (`decode_mut`) — no +/// scratch buffer, no copy. +#[tbor(response)] +pub struct TborHashResp<'a> { + /// The natural (big-endian) digest, exactly the algorithm's length + /// (32 / 48 / 64 B for SHA-256 / 384 / 512). + #[tbor(buffer, max_len = 64, mutable)] + pub digest: &'a [u8], +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use azihsm_fw_ddi_tbor_api::SessionId; + + use super::*; + + #[test] + fn request_round_trips_fields() { + let mut buf = [0u8; 512]; + let msg = [0x61u8; 64]; + let frame = TborHashReq::encode(&mut buf) + .unwrap() + .session_id(SessionId(9)) + .unwrap() + .algo(HashAlgo::Sha384) + .unwrap() + .msg(&msg) + .unwrap() + .finish(); + assert_eq!(frame.algo(), HashAlgo::Sha384); + assert_eq!(frame.msg(), &msg[..]); + } + + #[test] + fn response_round_trips_digest() { + let mut buf = [0u8; 256]; + let digest = [0x5Au8; HASH_DIGEST_MAX_LEN]; + let frame = TborHashResp::encode(&mut buf, 0, false) + .unwrap() + .digest(&digest) + .unwrap() + .finish(); + assert_eq!(frame.digest(), &digest[..]); + } + + #[test] + fn lengths_match_pinned_values() { + assert_eq!(HASH_MSG_MAX_LEN, 2048); + assert_eq!(HASH_DIGEST_MAX_LEN, 64); + } +} diff --git a/fw/core/ddi/tbor/types/src/lib.rs b/fw/core/ddi/tbor/types/src/lib.rs index 907846571..25579751e 100644 --- a/fw/core/ddi/tbor/types/src/lib.rs +++ b/fw/core/ddi/tbor/types/src/lib.rs @@ -37,6 +37,7 @@ pub mod tbor_int { pub mod api_rev; pub mod evidence; pub mod get_unwrapping_key; +pub mod hash; pub mod key_props; pub mod key_report; pub mod part_final; @@ -59,6 +60,7 @@ pub mod unwrap_key; pub use api_rev::*; pub use evidence::*; pub use get_unwrapping_key::*; +pub use hash::*; pub use key_props::*; pub use key_report::*; pub use part_final::*; diff --git a/fw/core/lib/src/ddi/tbor/hash.rs b/fw/core/lib/src/ddi/tbor/hash.rs new file mode 100644 index 000000000..e1f6ce236 --- /dev/null +++ b/fw/core/lib/src/ddi/tbor/hash.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `Hash` command handler. +//! +//! Within an open session, compute a SHA-256 / 384 / 512 digest of a +//! host-supplied message and return it. A pure hashing utility — no key, +//! no scope, no partition state — the TBOR analogue of MBOR `ShaDigest`. +//! +//! Uses the reserve-then-fill pattern: the response frame is encoded with +//! the digest slot reserved, then the PAL hashes straight into it — no +//! intermediate buffer, no copy. Available to both Crypto-Officer and +//! Crypto-User sessions. + +use azihsm_fw_ddi_tbor_types::HashAlgo; +use azihsm_fw_ddi_tbor_types::TborHashReq; +use azihsm_fw_ddi_tbor_types::TborHashResp; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmHashAlgo; +use azihsm_fw_hsm_pal_traits::HsmIo; +use azihsm_fw_hsm_pal_traits::HsmPal; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmSessId; + +use super::validate_active_session; + +/// Map the wire [`HashAlgo`] onto the firmware hash algorithm. +fn hsm_hash_algo(algo: HashAlgo) -> HsmResult { + match algo { + HashAlgo::Sha256 => Ok(HsmHashAlgo::Sha256), + HashAlgo::Sha384 => Ok(HsmHashAlgo::Sha384), + HashAlgo::Sha512 => Ok(HsmHashAlgo::Sha512), + _ => Err(HsmError::InvalidArg), + } +} + +/// Handle a TBOR `Hash` request. +/// +/// No partition lock or undo log is required: the command reads no mutable +/// partition state and persists nothing — it hashes the message and +/// returns the digest. +pub(crate) async fn handle<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + req_buf: &DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let req = TborHashReq::decode(req_buf)?; + let sess_id = HsmSessId::from(u16::from(req.session_id())); + validate_active_session(pal, io, sess_id)?; + + let algo = hsm_hash_algo(req.algo())?; + let digest_len = algo.digest_len(); + let msg = req.msg(); + + // Build the response with the digest slot reserved (sized exactly to the + // algorithm's digest length), then have the PAL hash straight into it. + let resp = pal.dma_alloc_var(io, |buf| { + let frame = TborHashResp::encode(buf, 0, false)? + .digest_reserve(digest_len)? + .finish(); + Ok(frame.as_bytes().len()) + })?; + + // `decode_mut` hands out a `&mut` view into the reserved slot; the view + // is scoped so its borrow of `resp` ends before `resp` is returned. The + // digest is emitted in natural big-endian order. + { + let out = TborHashResp::decode_mut(resp)?; + pal.hash(io, algo, msg, out.digest, true).await?; + } + + Ok(resp) +} diff --git a/fw/core/lib/src/ddi/tbor/mod.rs b/fw/core/lib/src/ddi/tbor/mod.rs index b9667c5d8..ba8049f82 100644 --- a/fw/core/lib/src/ddi/tbor/mod.rs +++ b/fw/core/lib/src/ddi/tbor/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod api_rev; pub(crate) mod from_pal; pub(crate) mod get_unwrapping_key; +pub(crate) mod hash; pub(crate) mod key_report; pub(crate) mod part_final; pub mod part_info; @@ -146,6 +147,15 @@ pub(crate) mod opcode { /// return it masked under the requested scope's masking key (plus the /// re-derived public key for RSA / ECC). See [`super::unwrap_key`]. pub(crate) const UNWRAP_KEY: u8 = 0x14; + + /// `Hash` — compute a SHA-256 / 384 / 512 digest of a + /// host-supplied message. A pure hashing utility with no key or + /// partition state. See [`super::hash`]. + /// + /// `0x15..=0x1A` are reserved by the sibling AES / ECC / RSA crypto + /// commands (separate branches), so `Hash` takes the next free + /// opcode, `0x1B`. + pub(crate) const HASH: u8 = 0x1B; } /// Validate that `sess_id` belongs to an active Crypto-Officer session. @@ -321,6 +331,7 @@ pub(crate) async fn dispatch<'p, P: HsmPal>( opcode::KEY_REPORT => key_report::handle(pal, io, req_buf).await, opcode::GET_UNWRAPPING_KEY => get_unwrapping_key::handle(pal, io, req_buf).await, opcode::UNWRAP_KEY => unwrap_key::handle(pal, io, req_buf, undo).await, + opcode::HASH => hash::handle(pal, io, req_buf).await, _ => Err(HsmError::UnsupportedCmd), } } @@ -346,6 +357,7 @@ fn is_known_opcode(opcode: u8) -> bool { | opcode::KEY_REPORT | opcode::GET_UNWRAPPING_KEY | opcode::UNWRAP_KEY + | opcode::HASH ) } @@ -377,7 +389,8 @@ fn is_in_session(opcode: u8) -> bool { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::KEY_REPORT | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => true, + | opcode::UNWRAP_KEY + | opcode::HASH => true, // Default-deny: any future opcode is treated as in-session // until classified, so the default-PSK gate applies to it. _ => true, @@ -417,7 +430,8 @@ fn needs_session_id_cross_check(opcode: u8) -> bool { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::KEY_REPORT | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => true, + | opcode::UNWRAP_KEY + | opcode::HASH => true, _ => true, } } diff --git a/fw/core/lib/src/op.rs b/fw/core/lib/src/op.rs index be4dcaf7f..64875c313 100644 --- a/fw/core/lib/src/op.rs +++ b/fw/core/lib/src/op.rs @@ -266,7 +266,8 @@ impl SessionCtrl { | opcode::SD_RESEAL_REMOTE_BACKUP | opcode::KEY_REPORT | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => Self::InSession, + | opcode::UNWRAP_KEY + | opcode::HASH => Self::InSession, opcode::SESSION_CLOSE => Self::Close, _ => Self::NoSession, }