Skip to content
Merged
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
85 changes: 85 additions & 0 deletions ddi/tbor/types/src/hmac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Host-side wrapper for the TBOR `Hmac` command.
//!
//! `Hmac` is an **in-session** command (Crypto-Officer or Crypto-User)
//! that computes an HMAC tag over `msg` using a caller-held **masked**
//! HMAC key (the `masked_key` returned by
//! [`HmacGenerateKey`](crate::hmac_generate_key) or imported via unwrap).
//! The device unmasks the key on-device, computes the MAC, and returns the
//! tag; nothing is persisted.

use alloc::vec::Vec;

pub use crate::hmac_generate_key::MASKED_HMAC_KEY_MAX_LEN;
pub use crate::hmac_generate_key::MASKED_HMAC_KEY_MIN_LEN;
use crate::tbor;

/// TBOR opcode for `Hmac`.
pub const TBOR_OP_HMAC: u8 = 0x12;

/// Maximum message length (bytes) accepted by `Hmac`.
///
/// `1024`, matching the MBOR `Hmac` command's message bound
/// (`DdiHmacReq::msg`), which is itself sized to the AES-CBC
/// single-shot message cap (`MAX_MSG_SIZE`) so the two symmetric
/// primitives share one host-visible limit. Larger inputs are hashed
/// host-side or chunked by the caller.
pub const HMAC_MSG_MAX_LEN: usize = 1024;
Comment thread
vsonims marked this conversation as resolved.

/// Maximum HMAC tag length (bytes): the SHA-512 digest.
pub const HMAC_TAG_MAX_LEN: usize = 64;

/// Host-facing TBOR `Hmac` request.
#[tbor(opcode = TBOR_OP_HMAC, session_ctrl = in_session)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TborHmacReq {
/// Session id this request is bound to. Cross-checked against the
/// SQE-carried session id by the dispatcher.
#[tbor(session_id)]
pub session_id: u16,

/// The masked HMAC key (from `HmacGenerateKey` / unwrap), an
/// AEAD-GCM-256 envelope of 164..=260 B.
#[tbor(min_len = 164, max_len = 260)]
pub masked_key: Vec<u8>,

/// The message to MAC, up to [`HMAC_MSG_MAX_LEN`] (1024) bytes.
#[tbor(max_len = 1024)]
pub msg: Vec<u8>,
}

/// Host-facing TBOR `Hmac` response.
#[tbor(response)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TborHmacResp {
/// The HMAC tag over `msg`: 32 / 48 / 64 B for SHA-256 / 384 / 512.
#[tbor(max_len = 64)]
pub tag: Vec<u8>,
}

#[cfg(test)]
mod tests {
use azihsm_ddi_tbor_types::TborOpReq;

use super::*;

#[test]
fn request_encodes_masked_key_and_msg() {
let req = TborHmacReq {
session_id: 7,
masked_key: alloc::vec![0x11u8; MASKED_HMAC_KEY_MIN_LEN],
msg: alloc::vec![0x22u8; 40],
};

let mut buf = [0u8; 1536];
let frame = req.encode_request(&mut buf).expect("encode");

// The message bytes must appear in the encoded frame.
assert!(
frame.windows(4).any(|w| w == [0x22u8; 4]),
"encoded frame must carry the message bytes",
);
}
}
102 changes: 102 additions & 0 deletions ddi/tbor/types/src/hmac_generate_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Host-side wrapper for the TBOR `HmacGenerateKey` command.
//!
//! `HmacGenerateKey` is an **in-session** command (Crypto-Officer or
//! Crypto-User) that generates a fresh random HMAC key of the requested
//! SHA variant and returns it as a **masked** blob. The key is not stored
//! on-device; the caller holds the masked blob and passes it back to
//! [`Hmac`](crate::hmac) to compute a MAC (unmask-on-use).
//!
//! The request carries the requested key `scope` (lifecycle / visibility
//! domain) and `hash_algo` (SHA variant) as raw 1-byte discriminants — the
//! firmware types them as the `KeyScope` / `HashAlgo` open-enums, but this
//! host crate is firewalled from the firmware PAL types so it carries the
//! same bytes as raw `u8`.

use alloc::vec::Vec;

use crate::tbor;

/// TBOR opcode for `HmacGenerateKey`.
pub const TBOR_OP_HMAC_GENERATE_KEY: u8 = 0x11;

/// Minimum masked HMAC-key envelope length (SHA-256, 32-byte key): an
/// AEAD-GCM-256 masked-key envelope `header(8) ‖ iv(12) ‖ aad(96) ‖
/// pt(32) ‖ tag(16)`.
pub const MASKED_HMAC_KEY_MIN_LEN: usize = 8 + 12 + 96 + 32 + 16;

/// Maximum masked HMAC-key envelope length (128-byte key): an AEAD-GCM-256
/// masked-key envelope `header(8) ‖ iv(12) ‖ aad(96) ‖ pt(128) ‖ tag(16)`.
pub const MASKED_HMAC_KEY_MAX_LEN: usize = 8 + 12 + 96 + 128 + 16;

/// `HashAlgo` discriminant for HMAC-SHA-256 (mirror of the firmware
/// `HsmHashAlgo` / TBOR `HashAlgo` value).
pub const HMAC_HASH_SHA256: u8 = 1;
/// `HashAlgo` discriminant for HMAC-SHA-384.
pub const HMAC_HASH_SHA384: u8 = 2;
/// `HashAlgo` discriminant for HMAC-SHA-512.
pub const HMAC_HASH_SHA512: u8 = 3;

/// Host-facing TBOR `HmacGenerateKey` request.
#[tbor(opcode = TBOR_OP_HMAC_GENERATE_KEY, session_ctrl = in_session)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct TborHmacGenerateKeyReq {
/// Session id this request is bound to. Cross-checked against the
/// SQE-carried session id by the dispatcher.
#[tbor(session_id)]
pub session_id: u16,

/// Requested key scope as the 1-byte `KeyScope` discriminant.
pub scope: u8,

/// Hash algorithm as the 1-byte `HashAlgo` discriminant (see
/// [`HMAC_HASH_SHA256`] / [`HMAC_HASH_SHA384`] / [`HMAC_HASH_SHA512`]).
/// Selects the HMAC SHA variant and the valid `key_length` range.
pub hash_algo: u8,
Comment thread
vsonims marked this conversation as resolved.

/// Requested key length in bytes. HMAC keys are variable-length; the
/// value must be in the SHA variant's range (SHA-256: 32–64,
/// SHA-384: 48–128, SHA-512: 64–128), else the device returns
/// `InvalidKeyLength`.
pub key_length: u8,
}

/// Host-facing TBOR `HmacGenerateKey` response.
#[tbor(response)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TborHmacGenerateKeyResp {
/// The freshly generated HMAC key, masked (AEAD-GCM-256) under the
/// requested scope's masking key. `132 + key_length` B (164 … 260 B
/// for a 32 … 128-byte key); not stored on-device.
#[tbor(max_len = 260)]
pub masked_key: Vec<u8>,
}

#[cfg(test)]
mod tests {
use azihsm_ddi_tbor_types::TborOpReq;

use super::*;

#[test]
fn request_encodes_scope_and_hash() {
let req = TborHmacGenerateKeyReq {
session_id: 5,
// KeyScope::Session discriminant (0b001).
scope: 0b001,
hash_algo: HMAC_HASH_SHA384,
key_length: 96,
};

let mut buf = [0u8; 256];
let frame = req.encode_request(&mut buf).expect("encode");

// The 1-byte hash-algo discriminant must appear in the encoded frame.
assert!(
frame.contains(&HMAC_HASH_SHA384),
"encoded frame must carry the hash-algo discriminant",
);
}
}
4 changes: 4 additions & 0 deletions ddi/tbor/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ impl From<SessionControlKind> for u8 {
mod api_rev;
mod evidence;
mod get_unwrapping_key;
mod hmac;
mod hmac_generate_key;
mod key_report;
mod part_final;
mod part_info;
Expand All @@ -103,6 +105,8 @@ mod unwrap_key;
pub use api_rev::*;
pub use evidence::*;
pub use get_unwrapping_key::*;
pub use hmac::*;
pub use hmac_generate_key::*;
pub use key_report::*;
pub use part_final::*;
pub use part_info::*;
Expand Down
161 changes: 161 additions & 0 deletions ddi/tbor/types/tests/commands/hmac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Integration tests for the TBOR `Hmac` command.
//!
//! `Hmac` computes an HMAC tag over a host message using a caller-held
//! **masked** HMAC key (from `HmacGenerateKey`). The device unmasks the
//! key on-device, computes the MAC, and returns the tag.
//!
//! Coverage:
//! * Generate → MAC round-trip per hash variant (SHA-256/384/512): the tag
//! has the digest length, is non-zero and deterministic, and a different
//! message yields a different tag.
//! * Session-scoped key round-trip (masked under the per-session key).
//! * Unwrapped-key → MAC round-trip: an HMAC key imported via `UnwrapKey`
//! computes a valid MAC, and a second independent import reproduces the
//! tag (proving the key material survives wrap/unwrap).
//! * Tampered masked key → `AesGcmDecryptTagDoesNotMatch`.
//! * Empty message is accepted.

#![cfg(feature = "emu")]

use azihsm_ddi_tbor_types::TborHmacGenerateKeyReq;
use azihsm_ddi_tbor_types::TborHmacReq;
use azihsm_ddi_tbor_types::TborStatus;
use azihsm_ddi_tbor_types::HMAC_HASH_SHA256;
use azihsm_ddi_tbor_types::HMAC_HASH_SHA384;
use azihsm_ddi_tbor_types::HMAC_HASH_SHA512;
use azihsm_ddi_tbor_types::KEY_CLASS_HMAC_SHA256;

use crate::commands::hmac_generate_key::SCOPE_EPHEMERAL;
use crate::commands::hmac_generate_key::SCOPE_SESSION;
use crate::commands::sd_sealing_key_gen::finalized_co_session;
use crate::commands::unwrap_key::unwrap;
use crate::harness::TestCtx;

/// Expected tag length (bytes) for a wire hash discriminant.
fn tag_len_for_hash(hash: u8) -> usize {
match hash {
HMAC_HASH_SHA256 => 32,
HMAC_HASH_SHA384 => 48,
HMAC_HASH_SHA512 => 64,
other => panic!("unexpected hash discriminant {other}"),
}
}

/// Generate a masked HMAC key of `(scope, hash)` on `session_id`. Uses
/// the digest-size key length (a valid in-range `VarLenHmac` length) — the
/// MAC tests exercise MAC behaviour, not the key-length range.
fn generate_key(ctx: &TestCtx, session_id: u16, scope: u8, hash: u8) -> Vec<u8> {
let req = TborHmacGenerateKeyReq {
session_id,
scope,
hash_algo: hash,
key_length: tag_len_for_hash(hash) as u8,
};
ctx.tbor(&req).expect("HmacGenerateKey").masked_key
}

/// Compute a MAC tag over `msg` with the masked key.
fn mac(ctx: &TestCtx, session_id: u16, masked_key: &[u8], msg: &[u8]) -> Vec<u8> {
let req = TborHmacReq {
session_id,
masked_key: masked_key.to_vec(),
msg: msg.to_vec(),
};
ctx.tbor(&req).expect("Hmac").tag
}

#[test]
fn hmac_roundtrip_all_hashes_emu() {
let ctx = TestCtx::new();
let session = finalized_co_session(&ctx);
let msg = b"the quick brown fox";

for hash in [HMAC_HASH_SHA256, HMAC_HASH_SHA384, HMAC_HASH_SHA512] {
let masked = generate_key(&ctx, session.session_id, SCOPE_EPHEMERAL, hash);
let tag = mac(&ctx, session.session_id, &masked, msg);

// Tag length matches the hash digest length, and is non-zero.
assert_eq!(tag.len(), tag_len_for_hash(hash), "tag length");
assert!(tag.iter().any(|&b| b != 0), "tag must not be all-zero");

// HMAC is deterministic: the same key + message reproduce the tag.
let tag_again = mac(&ctx, session.session_id, &masked, msg);
assert_eq!(tag, tag_again, "HMAC must be deterministic");

// A different message yields a different tag.
let tag_other = mac(&ctx, session.session_id, &masked, b"a different message");
assert_ne!(tag, tag_other, "distinct messages must yield distinct tags");
}
}

#[test]
fn hmac_session_scope_roundtrip_emu() {
let ctx = TestCtx::new();
let session = finalized_co_session(&ctx);
let masked = generate_key(&ctx, session.session_id, SCOPE_SESSION, HMAC_HASH_SHA256);
let tag = mac(&ctx, session.session_id, &masked, b"session-scoped mac");
assert_eq!(tag.len(), 32);
assert!(tag.iter().any(|&b| b != 0));
}

/// Unwrap an HMAC key (RSA-AES key import) and use it via `Hmac`.
///
/// Exercises the cross-command integration between `UnwrapKey` (imports a
/// host-wrapped HMAC key as a masked blob) and `Hmac` (MACs with the
/// recovered key). A second independent unwrap of the same key must
/// reproduce the tag, proving the key material survives wrap → unwrap →
/// mask → unmask intact.
#[test]
fn hmac_unwrapped_key_roundtrip_emu() {
let ctx = TestCtx::new();
let session = finalized_co_session(&ctx);

let hmac_key = [0x37u8; 32];
let msg = b"unwrap then mac";

let masked = unwrap(&ctx, session.session_id, KEY_CLASS_HMAC_SHA256, &hmac_key).masked_key;
let tag = mac(&ctx, session.session_id, &masked, msg);
assert_eq!(tag.len(), 32, "HMAC-SHA-256 tag length");
assert!(tag.iter().any(|&b| b != 0), "tag must not be all-zero");

// A second independent unwrap of the same key recovers the same key
// material, so MAC-ing the same message reproduces the tag.
let masked2 = unwrap(&ctx, session.session_id, KEY_CLASS_HMAC_SHA256, &hmac_key).masked_key;
let tag2 = mac(&ctx, session.session_id, &masked2, msg);
assert_eq!(tag, tag2, "same unwrapped key must reproduce the MAC");
}

#[test]
fn hmac_empty_message_emu() {
let ctx = TestCtx::new();
let session = finalized_co_session(&ctx);
let masked = generate_key(&ctx, session.session_id, SCOPE_EPHEMERAL, HMAC_HASH_SHA256);
let tag = mac(&ctx, session.session_id, &masked, b"");
assert_eq!(tag.len(), 32);
assert!(
tag.iter().any(|&b| b != 0),
"HMAC of empty message is non-zero"
);
}

#[test]
fn hmac_rejects_tampered_key_emu() {
let ctx = TestCtx::new();
let session = finalized_co_session(&ctx);
let mut masked = generate_key(&ctx, session.session_id, SCOPE_EPHEMERAL, HMAC_HASH_SHA256);

// Flip a byte in the AEAD tag region (last 16 bytes) so the unmask tag
// check fails without changing the cleartext scope metadata.
let last = masked.len() - 1;
masked[last] ^= 0x01;

let req = TborHmacReq {
session_id: session.session_id,
masked_key: masked,
msg: b"whatever".to_vec(),
};
ctx.expect_fw_reject(&req, TborStatus::AesGcmDecryptTagDoesNotMatch);
}
Loading
Loading