Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions ddi/tbor/types/src/hash.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

/// 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<u8>,
}

#[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",
);
}
}
2 changes: 2 additions & 0 deletions ddi/tbor/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl From<SessionControlKind> for u8 {
mod api_rev;
mod evidence;
mod get_unwrapping_key;
mod hash;
mod key_report;
mod part_final;
mod part_info;
Expand All @@ -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::*;
Expand Down
99 changes: 99 additions & 0 deletions ddi/tbor/types/tests/commands/hash.rs
Original file line number Diff line number Diff line change
@@ -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<u8>) -> Vec<u8> {
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<u8> {
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<u8> = Vec::new();
let long: Vec<u8> = (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,
);
}
1 change: 1 addition & 0 deletions ddi/tbor/types/tests/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions docs/tbor-ddi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
63 changes: 63 additions & 0 deletions docs/tbor-ddi/commands/hash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!--
Copyright (c) Microsoft Corporation.
Licensed under the MIT License.
-->

# 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`
110 changes: 110 additions & 0 deletions fw/core/ddi/tbor/types/src/hash.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading