-
Notifications
You must be signed in to change notification settings - Fork 11
feat(tbor): implement HmacGenerateKey + Hmac (masked-key HMAC crypto) #580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
|
|
||
| /// 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", | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
|
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", | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.