From 160ee2d3e9de14e43963df21f4943719903ceae0 Mon Sep 17 00:00:00 2001 From: Jayant Gandhi Date: Sat, 6 Jun 2026 01:04:00 +0000 Subject: [PATCH 1/2] feat(fw): implement DDI HkdfDerive + KbkdfCounterHmacDerive handlers Wire up the previously-unimplemented HkdfDerive (1075) and KbkdfCounterHmacDerive (1076) DDI ops in the firmware application layer, deriving key material from an existing ECDH shared secret and storing the result in the partition vault. Behavior mirrors the reference firmware (mcr-hsm) with one deliberate divergence: every HMAC output is stored as the variable-length HMAC vault kind (VarLenHmacSha256/384/512) rather than the deprecated fixed-length _HmacSha* kinds. - Input key must be an ECDH shared secret (Secret256/384/521) with the `derive` permission; any other kind is rejected with InvalidKeyType. - Output key_type dispatch: - Aes128/192/256 -> AES vault kinds (encrypt/decrypt usage) - HmacSha256/384/512 -> VarLenHmacSha256/384/512 (sign/verify) - VarHmac256/384/512 -> VarLenHmacSha256/384/512 (sign/verify), key_length required (else InvalidKeyType) and range-checked (256:32-64, 384:48-128, 512:64-128; else InvalidKeyLength) - bulk AES / other -> InvalidKeyType (out of scope) - HKDF runs RFC 5869 Extract-then-Expand; KBKDF runs the SP 800-108 counter-mode HMAC PRF. Absent salt/info/label/context use a zero-length DmaBuf. - masked_key is an empty placeholder pending the UnmaskKey handler, consistent with the other key-creating handlers. New: kdf.rs (shared input/output resolution), hkdf_derive.rs, kbkdf_derive.rs, key_attrs::for_var_hmac; mod.rs dispatch wiring. Tests: hkdf_smoke.rs / kbkdf_smoke.rs (AES round-trip + fixed-HMAC derive on both backends; var-HMAC derive + length validation gated to emu, since the sim has no variable-length HMAC kind). Validation: - emu hkdf_smoke 5/5, kbkdf_smoke 5/5; mock hkdf_smoke 2/2, kbkdf_smoke 2/2. - emu secret_hkdf_derive / secret_kbkdf_derive: all in-scope tests pass (remaining failures depend on the unimplemented Hmac/OpenKey ops and bulk AES, which were already failing as UnsupportedCmd). - emu smoke suite 37/37; mock secret_hkdf_derive 29/29 (no regression). - cargo xtask clippy clean; clippy --tests clean under emu and mock; fmt and copyright clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ddi/mbor/types/tests/azihsm_ddi_tests.rs | 2 + .../types/tests/integration/hkdf_smoke.rs | 212 ++++++++++++++++++ .../types/tests/integration/kbkdf_smoke.rs | 212 ++++++++++++++++++ fw/core/lib/src/ddi/mbor/hkdf_derive.rs | 106 +++++++++ fw/core/lib/src/ddi/mbor/kbkdf_derive.rs | 108 +++++++++ fw/core/lib/src/ddi/mbor/kdf.rs | 143 ++++++++++++ fw/core/lib/src/ddi/mbor/key_attrs.rs | 43 ++++ fw/core/lib/src/ddi/mbor/mod.rs | 7 + 8 files changed, 833 insertions(+) create mode 100644 ddi/mbor/types/tests/integration/hkdf_smoke.rs create mode 100644 ddi/mbor/types/tests/integration/kbkdf_smoke.rs create mode 100644 fw/core/lib/src/ddi/mbor/hkdf_derive.rs create mode 100644 fw/core/lib/src/ddi/mbor/kbkdf_derive.rs create mode 100644 fw/core/lib/src/ddi/mbor/kdf.rs diff --git a/ddi/mbor/types/tests/azihsm_ddi_tests.rs b/ddi/mbor/types/tests/azihsm_ddi_tests.rs index b09ff0c7..c0734dd1 100644 --- a/ddi/mbor/types/tests/azihsm_ddi_tests.rs +++ b/ddi/mbor/types/tests/azihsm_ddi_tests.rs @@ -41,9 +41,11 @@ mod integration { pub mod get_session_encryption_key; pub mod get_session_encryption_key_smoke; pub mod get_unwrapping_key; + pub mod hkdf_smoke; pub mod hmac; pub mod init_bk3_smoke; pub mod invalid_ecc_pub_key_vectors; + pub mod kbkdf_smoke; pub mod live_migration_expected_errors; pub mod live_migration_sim; pub mod lm_context; diff --git a/ddi/mbor/types/tests/integration/hkdf_smoke.rs b/ddi/mbor/types/tests/integration/hkdf_smoke.rs new file mode 100644 index 00000000..7bc085e9 --- /dev/null +++ b/ddi/mbor/types/tests/integration/hkdf_smoke.rs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! HKDF derivation smoke tests. +//! +//! - **AES output** (both backends): generate two ECC key pairs, +//! ECDH them into a shared secret, derive an `Aes256` key from each +//! secret, then encrypt with one and decrypt with the other and +//! confirm the message round-trips — proving the derived AES key is +//! usable and the derivation is deterministic. +//! - **Variable-length HMAC output** (emu only — the sim does not +//! support the var-len HMAC output kind): derive `VarHmac256` / +//! `VarHmac384` / `VarHmac512` keys and confirm the handler accepts +//! in-range lengths and rejects a missing / out-of-range +//! `key_length`. +//! +//! The masked-key contents and a follow-up `Hmac` MAC over a derived +//! HMAC key are out of scope here — masking and the `Hmac` handler +//! are implemented separately. + +#![cfg(test)] + +use azihsm_ddi::*; +use azihsm_ddi_mbor_codec::MborByteArray; +use azihsm_ddi_mbor_types::*; +use test_with_tracing::test; + +use super::common::*; + +#[test] +fn test_hkdf_aes_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + let (secret1, secret2) = create_ecdh_secrets(session_id, dev, DdiKeyType::Secret256); + let rev = Some(DdiApiRev { major: 1, minor: 0 }); + let salt = Some(MborByteArray::from_slice("salt".as_bytes()).unwrap()); + let info = Some(MborByteArray::from_slice("info".as_bytes()).unwrap()); + let key_props = + helper_key_properties(DdiKeyUsage::EncryptDecrypt, DdiKeyAvailability::App); + + // Derive the same AES-256 key from each (equal) shared secret. + let derive = |dev: &mut ::Dev, secret_id| { + helper_hkdf_derive( + dev, + Some(session_id), + rev, + secret_id, + DdiHashAlgorithm::Sha256, + salt, + info, + DdiKeyType::Aes256, + None, + key_props, + None, + ) + .expect("HKDF AES-256 derive should succeed") + .data + .key_id + }; + let key1 = derive(dev, secret1); + let key2 = derive(dev, secret2); + + // Encrypt with key1, decrypt with key2 — recovers the input + // iff both derivations produced the same usable AES key. + let plaintext = [0xABu8; 32]; + let iv = MborByteArray::from_slice(&[0u8; 16]).unwrap(); + let enc = helper_aes_encrypt_decrypt( + dev, + Some(session_id), + rev, + key1, + DdiAesOp::Encrypt, + MborByteArray::from_slice(&plaintext).unwrap(), + iv, + ) + .expect("encrypt with derived AES key"); + let dec = helper_aes_encrypt_decrypt( + dev, + Some(session_id), + rev, + key2, + DdiAesOp::Decrypt, + enc.data.msg, + iv, + ) + .expect("decrypt with derived AES key"); + assert_eq!( + dec.data.msg.as_slice(), + plaintext, + "HKDF-derived AES key must round-trip encrypt/decrypt" + ); + }, + ); +} + +/// Run an ECDH → HKDF derivation into a sign/verify HMAC-family key +/// (`HmacSha*` or `VarHmac*`) and return the result. +fn hkdf_hmac_derive( + dev: &mut ::Dev, + session_id: u16, + key_type: DdiKeyType, + key_length: Option, +) -> Result { + let (secret_key_id, _) = create_ecdh_secrets(session_id, dev, DdiKeyType::Secret256); + let key_props = helper_key_properties(DdiKeyUsage::SignVerify, DdiKeyAvailability::Session); + helper_hkdf_derive( + dev, + Some(session_id), + Some(DdiApiRev { major: 1, minor: 0 }), + secret_key_id, + DdiHashAlgorithm::Sha256, + Some(MborByteArray::from_slice("salt".as_bytes()).unwrap()), + Some(MborByteArray::from_slice("info".as_bytes()).unwrap()), + key_type, + None, + key_props, + key_length, + ) +} + +#[test] +fn test_hkdf_hmac_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // Fixed-length HMAC output types are supported by both + // backends. A follow-up `Hmac` MAC over the key needs the + // `Hmac` handler (not yet implemented), so assert only that + // the derive succeeds. + for key_type in [ + DdiKeyType::HmacSha256, + DdiKeyType::HmacSha384, + DdiKeyType::HmacSha512, + ] { + let resp = hkdf_hmac_derive(dev, session_id, key_type, None); + assert!(resp.is_ok(), "HKDF {key_type:?} should derive: {resp:?}"); + let resp = resp.unwrap(); + assert_eq!(resp.hdr.op, DdiOp::HkdfDerive); + assert_eq!(resp.hdr.status, DdiStatus::Success); + } + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_hkdf_var_hmac_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // (key_type, in-range key_length) for each var-HMAC variant. + let cases = [ + (DdiKeyType::VarHmac256, 32u8), + (DdiKeyType::VarHmac256, 64), + (DdiKeyType::VarHmac384, 48), + (DdiKeyType::VarHmac384, 128), + (DdiKeyType::VarHmac512, 64), + (DdiKeyType::VarHmac512, 128), + ]; + for (key_type, key_len) in cases { + let resp = hkdf_hmac_derive(dev, session_id, key_type, Some(key_len)); + assert!( + resp.is_ok(), + "HKDF {key_type:?} len {key_len} should derive: {resp:?}" + ); + let resp = resp.unwrap(); + assert_eq!(resp.hdr.op, DdiOp::HkdfDerive); + assert_eq!(resp.hdr.status, DdiStatus::Success); + } + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_hkdf_var_hmac_missing_length_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // A var-len HMAC output requires an explicit key_length. + let err = hkdf_hmac_derive(dev, session_id, DdiKeyType::VarHmac256, None) + .expect_err("missing key_length must be rejected"); + assert!( + matches!(err, DdiError::DdiStatus(DdiStatus::InvalidKeyType)), + "expected InvalidKeyType, got {err:?}" + ); + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_hkdf_var_hmac_out_of_range_length_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // 16 is below VarHmac256's 32-byte minimum. + let err = hkdf_hmac_derive(dev, session_id, DdiKeyType::VarHmac256, Some(16)) + .expect_err("out-of-range key_length must be rejected"); + assert!( + matches!(err, DdiError::DdiStatus(DdiStatus::InvalidKeyLength)), + "expected InvalidKeyLength, got {err:?}" + ); + }, + ); +} diff --git a/ddi/mbor/types/tests/integration/kbkdf_smoke.rs b/ddi/mbor/types/tests/integration/kbkdf_smoke.rs new file mode 100644 index 00000000..a0856c5d --- /dev/null +++ b/ddi/mbor/types/tests/integration/kbkdf_smoke.rs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! KBKDF (SP 800-108 counter-mode HMAC) derivation smoke tests. +//! +//! - **AES output** (both backends): generate two ECC key pairs, +//! ECDH them into a shared secret, derive an `Aes256` key from each +//! secret, then encrypt with one and decrypt with the other and +//! confirm the message round-trips — proving the derived AES key is +//! usable and the derivation is deterministic. +//! - **Variable-length HMAC output** (emu only — the sim does not +//! support the var-len HMAC output kind): derive `VarHmac256` / +//! `VarHmac384` / `VarHmac512` keys and confirm the handler accepts +//! in-range lengths and rejects a missing / out-of-range +//! `key_length`. +//! +//! The masked-key contents and a follow-up `Hmac` MAC over a derived +//! HMAC key are out of scope here — masking and the `Hmac` handler +//! are implemented separately. + +#![cfg(test)] + +use azihsm_ddi::*; +use azihsm_ddi_mbor_codec::MborByteArray; +use azihsm_ddi_mbor_types::*; +use test_with_tracing::test; + +use super::common::*; + +#[test] +fn test_kbkdf_aes_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + let (secret1, secret2) = create_ecdh_secrets(session_id, dev, DdiKeyType::Secret256); + let rev = Some(DdiApiRev { major: 1, minor: 0 }); + let label = Some(MborByteArray::from_slice("label".as_bytes()).unwrap()); + let context = Some(MborByteArray::from_slice("context".as_bytes()).unwrap()); + let key_props = + helper_key_properties(DdiKeyUsage::EncryptDecrypt, DdiKeyAvailability::App); + + // Derive the same AES-256 key from each (equal) shared secret. + let derive = |dev: &mut ::Dev, secret_id| { + helper_kbkdf_derive( + dev, + Some(session_id), + rev, + secret_id, + DdiHashAlgorithm::Sha256, + label, + context, + DdiKeyType::Aes256, + None, + key_props, + None, + ) + .expect("KBKDF AES-256 derive should succeed") + .data + .key_id + }; + let key1 = derive(dev, secret1); + let key2 = derive(dev, secret2); + + // Encrypt with key1, decrypt with key2 — recovers the input + // iff both derivations produced the same usable AES key. + let plaintext = [0xABu8; 32]; + let iv = MborByteArray::from_slice(&[0u8; 16]).unwrap(); + let enc = helper_aes_encrypt_decrypt( + dev, + Some(session_id), + rev, + key1, + DdiAesOp::Encrypt, + MborByteArray::from_slice(&plaintext).unwrap(), + iv, + ) + .expect("encrypt with derived AES key"); + let dec = helper_aes_encrypt_decrypt( + dev, + Some(session_id), + rev, + key2, + DdiAesOp::Decrypt, + enc.data.msg, + iv, + ) + .expect("decrypt with derived AES key"); + assert_eq!( + dec.data.msg.as_slice(), + plaintext, + "KBKDF-derived AES key must round-trip encrypt/decrypt" + ); + }, + ); +} + +/// Run an ECDH → KBKDF derivation into a sign/verify HMAC-family key +/// (`HmacSha*` or `VarHmac*`) and return the result. +fn kbkdf_hmac_derive( + dev: &mut ::Dev, + session_id: u16, + key_type: DdiKeyType, + key_length: Option, +) -> Result { + let (secret_key_id, _) = create_ecdh_secrets(session_id, dev, DdiKeyType::Secret256); + let key_props = helper_key_properties(DdiKeyUsage::SignVerify, DdiKeyAvailability::Session); + helper_kbkdf_derive( + dev, + Some(session_id), + Some(DdiApiRev { major: 1, minor: 0 }), + secret_key_id, + DdiHashAlgorithm::Sha256, + Some(MborByteArray::from_slice("label".as_bytes()).unwrap()), + Some(MborByteArray::from_slice("context".as_bytes()).unwrap()), + key_type, + None, + key_props, + key_length, + ) +} + +#[test] +fn test_kbkdf_hmac_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // Fixed-length HMAC output types are supported by both + // backends. A follow-up `Hmac` MAC over the key needs the + // `Hmac` handler (not yet implemented), so assert only that + // the derive succeeds. + for key_type in [ + DdiKeyType::HmacSha256, + DdiKeyType::HmacSha384, + DdiKeyType::HmacSha512, + ] { + let resp = kbkdf_hmac_derive(dev, session_id, key_type, None); + assert!(resp.is_ok(), "KBKDF {key_type:?} should derive: {resp:?}"); + let resp = resp.unwrap(); + assert_eq!(resp.hdr.op, DdiOp::KbkdfCounterHmacDerive); + assert_eq!(resp.hdr.status, DdiStatus::Success); + } + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_kbkdf_var_hmac_derive_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // (key_type, in-range key_length) for each var-HMAC variant. + let cases = [ + (DdiKeyType::VarHmac256, 32u8), + (DdiKeyType::VarHmac256, 64), + (DdiKeyType::VarHmac384, 48), + (DdiKeyType::VarHmac384, 128), + (DdiKeyType::VarHmac512, 64), + (DdiKeyType::VarHmac512, 128), + ]; + for (key_type, key_len) in cases { + let resp = kbkdf_hmac_derive(dev, session_id, key_type, Some(key_len)); + assert!( + resp.is_ok(), + "KBKDF {key_type:?} len {key_len} should derive: {resp:?}" + ); + let resp = resp.unwrap(); + assert_eq!(resp.hdr.op, DdiOp::KbkdfCounterHmacDerive); + assert_eq!(resp.hdr.status, DdiStatus::Success); + } + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_kbkdf_var_hmac_missing_length_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // A var-len HMAC output requires an explicit key_length. + let err = kbkdf_hmac_derive(dev, session_id, DdiKeyType::VarHmac256, None) + .expect_err("missing key_length must be rejected"); + assert!( + matches!(err, DdiError::DdiStatus(DdiStatus::InvalidKeyType)), + "expected InvalidKeyType, got {err:?}" + ); + }, + ); +} + +#[cfg(not(feature = "mock"))] +#[test] +fn test_kbkdf_var_hmac_out_of_range_length_smoke() { + ddi_dev_test( + common_setup, + common_cleanup, + |dev, _ddi, _path, session_id| { + // 16 is below VarHmac256's 32-byte minimum. + let err = kbkdf_hmac_derive(dev, session_id, DdiKeyType::VarHmac256, Some(16)) + .expect_err("out-of-range key_length must be rejected"); + assert!( + matches!(err, DdiError::DdiStatus(DdiStatus::InvalidKeyLength)), + "expected InvalidKeyLength, got {err:?}" + ); + }, + ); +} diff --git a/fw/core/lib/src/ddi/mbor/hkdf_derive.rs b/fw/core/lib/src/ddi/mbor/hkdf_derive.rs new file mode 100644 index 00000000..2f37feb7 --- /dev/null +++ b/fw/core/lib/src/ddi/mbor/hkdf_derive.rs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! DDI HkdfDerive command handler. +//! +//! Within an open session, derive key material from an existing ECDH +//! shared secret via HKDF (RFC 5869: Extract-then-Expand), persist +//! the result in the partition vault — optionally session-scoped so +//! it is torn down by [`CloseSession`](super::close_session) — and +//! return the assigned `key_id` plus an (empty placeholder) masked-key +//! envelope the host may re-import on a future session. +//! +//! The input key must be an ECDH shared secret (`Secret256` / +//! `Secret384` / `Secret521`) with the `derive` permission. The +//! requested output `key_type` selects the vault kind: AES outputs +//! are stored as AES keys, while every HMAC output (fixed `HmacSha*` +//! or `VarHmac*`) is stored as a variable-length HMAC key. See +//! [`kdf`](super::kdf) for the full mapping and length rules. + +use azihsm_fw_ddi_mbor_types::derive_hkdf::DdiHkdfDeriveReq; +use azihsm_fw_ddi_mbor_types::derive_hkdf::DdiHkdfDeriveResp; + +use super::kdf::KdfClass; +use super::*; + +/// Handle `DdiHkdfDeriveCmd`. +/// +/// No `partition_lock` is needed: this handler does not perform any +/// multi-step read-then-mutate against partition state. Its single +/// state mutation — `vault_key_create` — is sync and atomic. +pub(crate) async fn hkdf_derive<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + decoder: &mut DdiDecoder<'_>, + hdr: &DdiReqHdr, +) -> HsmResult<&'p DmaBuf> { + let body: DdiHkdfDeriveReq = decoder.decode_data()?; + let sess_id = hdr.sess_id.ok_or(HsmError::SessionExpected)?; + let input_key_id = HsmKeyId::from(body.key_id); + + // The IKM must be an ECDH shared secret carrying `derive`. + super::kdf::validate_input_secret(pal.vault_key_kind(io, input_key_id)?)?; + if !pal.vault_key_attrs(io, input_key_id)?.derive() { + return Err(HsmError::InvalidPermissions); + } + + let algo = super::from_ddi::hash(body.hash_algorithm)?; + let target = super::kdf::resolve_target(body.key_type, body.key_length)?; + let attrs = match target.class { + KdfClass::Aes => super::key_attrs::for_aes(&body.key_properties.key_metadata)?, + KdfClass::Hmac => super::key_attrs::for_var_hmac(&body.key_properties.key_metadata)?, + }; + super::key_attrs::check_session_key_tag(attrs, body.key_tag)?; + + // Derive the OKM into a DMA scratch slot; `vault_key_create` + // copies it into vault-owned storage so the scratch can drop + // after. HKDF-Extract with an absent salt uses a zero-length + // `DmaBuf` (`split_at_mut(0)`), which the std PAL maps to the + // RFC 5869 default all-zero salt. + let out = pal.dma_alloc(io, target.out_len)?; + let prk_area = pal.dma_alloc(io, algo.digest_len())?; + let (empty, prk) = prk_area.split_at_mut(0); + + let salt_buf: &DmaBuf = match body.salt.as_deref() { + Some(salt) => salt, + None => empty, + }; + { + let ikm = pal.vault_key(io, input_key_id)?; + pal.hkdf_extract(io, algo, salt_buf, ikm, prk).await?; + } + + let info_buf: &DmaBuf = match body.info.as_deref() { + Some(info) => info, + None => empty, + }; + pal.hkdf_expand(io, algo, prk, info_buf, out).await?; + + // RAII vault entry — rolls back if response encoding below fails. + // `masked_key` is the host's opaque re-import blob; firmware-side + // masking is pending the `UnmaskKey` handler, so we emit an empty + // placeholder for wire validity. + let guard = pal.vault_key_create( + io, + out, + target.kind, + attrs.session().then_some(HsmSessId::from(sess_id)), + attrs, + body.key_properties.key_label, + )?; + let key_id: u16 = guard.key_id().into(); + + let resp = pal.dma_alloc_var(io, |buf| { + super::encode_resp( + &super::success_hdr_sess(hdr, DdiOp::HkdfDerive, sess_id), + &DdiHkdfDeriveResp { + key_id, + masked_key: &[], + bulk_key_id: None, + }, + buf, + ) + })?; + let _ = guard.dismiss(); + Ok(resp) +} diff --git a/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs b/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs new file mode 100644 index 00000000..42f8b6d8 --- /dev/null +++ b/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! DDI KbkdfCounterHmacDerive command handler. +//! +//! Within an open session, derive key material from an existing ECDH +//! shared secret via the SP 800-108 Counter Mode KDF with an HMAC +//! PRF, persist the result in the partition vault — optionally +//! session-scoped so it is torn down by +//! [`CloseSession`](super::close_session) — and return the assigned +//! `key_id` plus an (empty placeholder) masked-key envelope the host +//! may re-import on a future session. +//! +//! The input key must be an ECDH shared secret (`Secret256` / +//! `Secret384` / `Secret521`) with the `derive` permission. The +//! requested output `key_type` selects the vault kind: AES outputs +//! are stored as AES keys, while every HMAC output (fixed `HmacSha*` +//! or `VarHmac*`) is stored as a variable-length HMAC key. See +//! [`kdf`](super::kdf) for the full mapping and length rules. +//! +//! Per the underlying PRF, at least one of `label` / `context` must +//! be present; deriving with both absent is rejected by the KDF. + +use azihsm_fw_ddi_mbor_types::derive_kbkdf::DdiKbkdfCounterHmacDeriveReq; +use azihsm_fw_ddi_mbor_types::derive_kbkdf::DdiKbkdfCounterHmacDeriveResp; + +use super::kdf::KdfClass; +use super::*; + +/// Handle `DdiKbkdfCounterHmacDeriveCmd`. +/// +/// No `partition_lock` is needed: this handler does not perform any +/// multi-step read-then-mutate against partition state. Its single +/// state mutation — `vault_key_create` — is sync and atomic. +pub(crate) async fn kbkdf_counter_hmac_derive<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + decoder: &mut DdiDecoder<'_>, + hdr: &DdiReqHdr, +) -> HsmResult<&'p DmaBuf> { + let body: DdiKbkdfCounterHmacDeriveReq = decoder.decode_data()?; + let sess_id = hdr.sess_id.ok_or(HsmError::SessionExpected)?; + let input_key_id = HsmKeyId::from(body.key_id); + + // The KDK must be an ECDH shared secret carrying `derive`. + super::kdf::validate_input_secret(pal.vault_key_kind(io, input_key_id)?)?; + if !pal.vault_key_attrs(io, input_key_id)?.derive() { + return Err(HsmError::InvalidPermissions); + } + + let algo = super::from_ddi::hash(body.hash_algorithm)?; + let target = super::kdf::resolve_target(body.key_type, body.key_length)?; + let attrs = match target.class { + KdfClass::Aes => super::key_attrs::for_aes(&body.key_properties.key_metadata)?, + KdfClass::Hmac => super::key_attrs::for_var_hmac(&body.key_properties.key_metadata)?, + }; + super::key_attrs::check_session_key_tag(attrs, body.key_tag)?; + + // Derive the OKM into a DMA scratch slot; `vault_key_create` + // copies it into vault-owned storage so the scratch can drop + // after. Absent label / context use a zero-length `DmaBuf` + // (`split_at_mut(0)` carves an empty slice off the output + // allocation). + let out_area = pal.dma_alloc(io, target.out_len)?; + let (empty, out) = out_area.split_at_mut(0); + + let label_buf: &DmaBuf = match body.label.as_deref() { + Some(label) => label, + None => empty, + }; + let context_buf: &DmaBuf = match body.context.as_deref() { + Some(context) => context, + None => empty, + }; + { + let kdk = pal.vault_key(io, input_key_id)?; + pal.sp800_108_kdf(io, algo, kdk, label_buf, context_buf, out) + .await?; + } + + // RAII vault entry — rolls back if response encoding below fails. + // `masked_key` is the host's opaque re-import blob; firmware-side + // masking is pending the `UnmaskKey` handler, so we emit an empty + // placeholder for wire validity. + let guard = pal.vault_key_create( + io, + out, + target.kind, + attrs.session().then_some(HsmSessId::from(sess_id)), + attrs, + body.key_properties.key_label, + )?; + let key_id: u16 = guard.key_id().into(); + + let resp = pal.dma_alloc_var(io, |buf| { + super::encode_resp( + &super::success_hdr_sess(hdr, DdiOp::KbkdfCounterHmacDerive, sess_id), + &DdiKbkdfCounterHmacDeriveResp { + key_id, + masked_key: &[], + bulk_key_id: None, + }, + buf, + ) + })?; + let _ = guard.dismiss(); + Ok(resp) +} diff --git a/fw/core/lib/src/ddi/mbor/kdf.rs b/fw/core/lib/src/ddi/mbor/kdf.rs new file mode 100644 index 00000000..905b1baa --- /dev/null +++ b/fw/core/lib/src/ddi/mbor/kdf.rs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared logic for the HKDF / KBKDF key-derivation handlers +//! ([`hkdf_derive`](super::hkdf_derive) and +//! [`kbkdf_derive`](super::kbkdf_derive)). +//! +//! Both commands take the same shape — an input ECDH shared-secret +//! key (the IKM / KDK), an output `key_type` + optional `key_length`, +//! and target key properties — so the input validation and the +//! output target resolution are factored here to keep the two +//! handlers byte-for-byte consistent. +//! +//! ## Output storage policy +//! +//! Mirrors the reference firmware's `key_type` dispatch with one +//! deliberate divergence: **all HMAC outputs are stored as the +//! variable-length HMAC vault kind** ([`HsmVaultKeyKind::VarLenHmacSha256`] +//! etc.), never the deprecated fixed-length `_HmacSha*` kinds. AES +//! outputs still map to the matching AES vault kind. +//! +//! | Requested `key_type` | Vault kind | Output length | +//! |---|---|---| +//! | `Aes128` / `Aes192` / `Aes256` | `Aes128` / `Aes192` / `Aes256` | 16 / 24 / 32 | +//! | `HmacSha256` / `384` / `512` | `VarLenHmacSha256` / `384` / `512` | 32 / 48 / 64 | +//! | `VarHmac256` / `384` / `512` | `VarLenHmacSha256` / `384` / `512` | `key_length` | +//! +//! Variable-length HMAC outputs require an explicit `key_length` +//! (absent → [`HsmError::InvalidKeyType`], matching the reference) +//! validated against the per-variant range (out of range → +//! [`HsmError::InvalidKeyLength`]): +//! +//! | Vault kind | min | max | +//! |---|---|---| +//! | `VarLenHmacSha256` | 32 | 64 | +//! | `VarLenHmacSha384` | 48 | 128 | +//! | `VarLenHmacSha512` | 64 | 128 | + +use azihsm_fw_ddi_mbor_types::DdiKeyType; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; + +/// Which attribute family the derived key is created with. +/// +/// The KDF output's vault kind decides the permitted usage: AES keys +/// carry `encrypt`/`decrypt`, HMAC keys carry `sign`/`verify`. The +/// handler selects the matching [`key_attrs`](super::key_attrs) +/// builder from this tag. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum KdfClass { + /// AES key — `encrypt` / `decrypt` usage. + Aes, + /// (Variable-length) HMAC key — `sign` / `verify` usage. + Hmac, +} + +/// Resolved derivation target: where the OKM is stored and how many +/// bytes to derive. +pub(crate) struct KdfTarget { + /// Vault kind the derived bytes are stored under. + pub kind: HsmVaultKeyKind, + /// Number of OKM bytes to derive. + pub out_len: usize, + /// Attribute family for the created key. + pub class: KdfClass, +} + +/// Reject an input key whose kind is not an ECDH shared secret. +/// +/// HKDF / KBKDF derive from an ECDH shared secret (`Secret256` / +/// `Secret384` / `Secret521`); any other vault kind is rejected with +/// [`HsmError::InvalidKeyType`] — matching the reference firmware's +/// `ecdh_key(..)` lookup and the sim's input-kind check. +pub(crate) fn validate_input_secret(kind: HsmVaultKeyKind) -> HsmResult<()> { + match kind { + HsmVaultKeyKind::Secret256 | HsmVaultKeyKind::Secret384 | HsmVaultKeyKind::Secret521 => { + Ok(()) + } + _ => Err(HsmError::InvalidKeyType), + } +} + +/// Resolve the requested output `key_type` (+ optional `key_length`) +/// into the vault kind, OKM length, and attribute family. +/// +/// See the [module docs](self) for the full mapping. Unsupported +/// output types (ECC / RSA / Secret / bulk AES) return +/// [`HsmError::InvalidKeyType`]. +pub(crate) fn resolve_target(key_type: DdiKeyType, key_len: Option) -> HsmResult { + let aes = |kind, out_len| { + Ok(KdfTarget { + kind, + out_len, + class: KdfClass::Aes, + }) + }; + let hmac = |kind, out_len| { + Ok(KdfTarget { + kind, + out_len, + class: KdfClass::Hmac, + }) + }; + + match key_type { + DdiKeyType::Aes128 => aes(HsmVaultKeyKind::Aes128, 16), + DdiKeyType::Aes192 => aes(HsmVaultKeyKind::Aes192, 24), + DdiKeyType::Aes256 => aes(HsmVaultKeyKind::Aes256, 32), + + DdiKeyType::HmacSha256 => hmac(HsmVaultKeyKind::VarLenHmacSha256, 32), + DdiKeyType::HmacSha384 => hmac(HsmVaultKeyKind::VarLenHmacSha384, 48), + DdiKeyType::HmacSha512 => hmac(HsmVaultKeyKind::VarLenHmacSha512, 64), + + DdiKeyType::VarHmac256 => hmac( + HsmVaultKeyKind::VarLenHmacSha256, + var_hmac_len(key_len, 32, 64)?, + ), + DdiKeyType::VarHmac384 => hmac( + HsmVaultKeyKind::VarLenHmacSha384, + var_hmac_len(key_len, 48, 128)?, + ), + DdiKeyType::VarHmac512 => hmac( + HsmVaultKeyKind::VarLenHmacSha512, + var_hmac_len(key_len, 64, 128)?, + ), + + _ => Err(HsmError::InvalidKeyType), + } +} + +/// Validate a variable-length HMAC `key_length`. +/// +/// A missing length is [`HsmError::InvalidKeyType`] (the reference +/// firmware's sentinel for "var-len HMAC without an explicit +/// length"); an out-of-range length is [`HsmError::InvalidKeyLength`]. +fn var_hmac_len(key_len: Option, min: usize, max: usize) -> HsmResult { + let len = usize::from(key_len.ok_or(HsmError::InvalidKeyType)?); + if len < min || len > max { + return Err(HsmError::InvalidKeyLength); + } + Ok(len) +} diff --git a/fw/core/lib/src/ddi/mbor/key_attrs.rs b/fw/core/lib/src/ddi/mbor/key_attrs.rs index a063f9fc..cea0d088 100644 --- a/fw/core/lib/src/ddi/mbor/key_attrs.rs +++ b/fw/core/lib/src/ddi/mbor/key_attrs.rs @@ -149,6 +149,49 @@ pub(crate) fn for_ecdh_secret(metadata: &DdiTargetKeyMetadata) -> HsmResult HsmResult { + validate_pairs(metadata)?; + let mut attrs = HsmVaultKeyAttrs::new().with_local(true); + + let sign_verify = metadata.sign() && metadata.verify(); + let encrypt_decrypt = metadata.encrypt() && metadata.decrypt(); + let derive = metadata.derive(); + let wrap = metadata.wrap(); + let unwrap = metadata.unwrap(); + + let usage_count = (sign_verify as u8) + + (encrypt_decrypt as u8) + + (derive as u8) + + (wrap as u8) + + (unwrap as u8); + if usage_count != 1 { + return Err(HsmError::InvalidPermissions); + } + + if encrypt_decrypt || wrap || unwrap { + return Err(HsmError::InvalidPermissions); + } + + if sign_verify { + attrs = attrs.with_sign(true).with_verify(true); + } + if derive { + attrs = attrs.with_derive(true); + } + + if metadata.session() { + attrs = attrs.with_session(true); + } + + Ok(attrs) +} + /// Reject metadata where one half of a paired usage flag is set /// without the other (`sign` without `verify`, or `encrypt` /// without `decrypt`). The host is supposed to encode these as diff --git a/fw/core/lib/src/ddi/mbor/mod.rs b/fw/core/lib/src/ddi/mbor/mod.rs index 9ee8c1fd..e8dd8fa7 100644 --- a/fw/core/lib/src/ddi/mbor/mod.rs +++ b/fw/core/lib/src/ddi/mbor/mod.rs @@ -17,7 +17,10 @@ pub(crate) mod get_device_info; pub(crate) mod get_establish_cred_encryption_key; pub(crate) mod get_sealed_bk3; pub(crate) mod get_session_encryption_key; +pub(crate) mod hkdf_derive; pub(crate) mod init_bk3; +pub(crate) mod kbkdf_derive; +pub(crate) mod kdf; pub(crate) mod key_attrs; pub(crate) mod open_session; pub(crate) mod set_sealed_bk3; @@ -42,7 +45,9 @@ pub(crate) use get_device_info::*; pub(crate) use get_establish_cred_encryption_key::*; pub(crate) use get_sealed_bk3::*; pub(crate) use get_session_encryption_key::*; +pub(crate) use hkdf_derive::*; pub(crate) use init_bk3::*; +pub(crate) use kbkdf_derive::*; pub(crate) use open_session::*; pub(crate) use set_sealed_bk3::*; pub(crate) use sha_digest::*; @@ -126,6 +131,8 @@ pub(crate) async fn dispatch<'p, P: HsmPal>( DdiOp::EccGenerateKeyPair => ecc_generate_key_pair(pal, io, decoder, hdr).await, DdiOp::EccSign => ecc_sign(pal, io, decoder, hdr).await, DdiOp::EcdhKeyExchange => ecdh_key_exchange(pal, io, decoder, hdr).await, + DdiOp::HkdfDerive => hkdf_derive(pal, io, decoder, hdr).await, + DdiOp::KbkdfCounterHmacDerive => kbkdf_counter_hmac_derive(pal, io, decoder, hdr).await, _ => Err(HsmError::UnsupportedCmd), } } From 70805e8b89f127ea2cb7056bf0c4b863866370d2 Mon Sep 17 00:00:00 2001 From: Jayant Gandhi Date: Mon, 8 Jun 2026 22:36:34 +0000 Subject: [PATCH 2/2] refactor(fw): make optional KDF inputs Option<&DmaBuf> in PAL Previously the optional HKDF (salt/info) and SP 800-108 KBKDF (label/context) inputs were passed as a non-optional `&DmaBuf`, where a zero-length buffer signaled "absent". Handlers had to materialize an empty `DmaBuf` via `split_at_mut(0)` and branch on `match body.field.as_deref() { Some(x) => x, None => empty }`. Make these params `Option<&DmaBuf>` through the HsmKdf trait, the std PAL impl, the std KDF driver, and all call sites, so handlers pass `body.field.as_deref()` directly. The std driver keeps the empty-as-absent normalization (`owned_nonempty`) so the bytes handed to OpenSSL stay byte-identical to the previous flow. In hkdf_derive, `out` and `prk` are now two independent dma_alloc calls (each 4-byte aligned) rather than carving an empty placeholder off a shared buffer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/core/crypto/hpke/src/kdf.rs | 4 +- .../lib/src/ddi/mbor/establish_credential.rs | 18 +- fw/core/lib/src/ddi/mbor/hkdf_derive.rs | 29 ++-- fw/core/lib/src/ddi/mbor/kbkdf_derive.rs | 26 ++- fw/core/lib/src/ddi/mbor/open_session.rs | 13 +- .../lib/src/ddi/tbor/open_session_finish.rs | 13 +- fw/pal/traits/src/crypto/kdf.rs | 16 +- fw/plat/std/pal/src/drivers/kdf.rs | 162 +++++++++++------- fw/plat/std/pal/src/kdf.rs | 26 ++- fw/plat/std/pal/src/part.rs | 11 +- 10 files changed, 185 insertions(+), 133 deletions(-) diff --git a/fw/core/crypto/hpke/src/kdf.rs b/fw/core/crypto/hpke/src/kdf.rs index 9eea03b5..db16222b 100644 --- a/fw/core/crypto/hpke/src/kdf.rs +++ b/fw/core/crypto/hpke/src/kdf.rs @@ -110,7 +110,7 @@ where let labeled_ikm = concat_alloc(&[HPKE_V1, suite_id, label, ikm], alloc)?; let salt_dma = dma_copy_in(alloc, salt)?; let prk_scratch = alloc.dma_alloc(prk_out.len())?; - pal.hkdf_extract(io, algo, salt_dma, labeled_ikm, prk_scratch) + pal.hkdf_extract(io, algo, Some(salt_dma), labeled_ikm, prk_scratch) .await?; prk_out.copy_from_slice(prk_scratch); Ok(()) @@ -173,7 +173,7 @@ where let prk_dma = dma_copy_in(alloc, prk)?; let labeled_info = concat_alloc(&[&l_bytes, HPKE_V1, suite_id, label, info], alloc)?; let out_scratch = alloc.dma_alloc(out.len())?; - pal.hkdf_expand(io, algo, prk_dma, labeled_info, out_scratch) + pal.hkdf_expand(io, algo, prk_dma, Some(labeled_info), out_scratch) .await?; out.copy_from_slice(out_scratch); Ok(()) diff --git a/fw/core/lib/src/ddi/mbor/establish_credential.rs b/fw/core/lib/src/ddi/mbor/establish_credential.rs index 182a32cd..33312ea6 100644 --- a/fw/core/lib/src/ddi/mbor/establish_credential.rs +++ b/fw/core/lib/src/ddi/mbor/establish_credential.rs @@ -323,14 +323,12 @@ async fn derive_credential_keys( .await?; } - // HKDF-Extract with empty salt (RFC 5869 §2.2). `split_at_mut(0)` - // yields a zero-length DmaBuf for the salt argument. - let prk_area = pal.dma_alloc(io, HsmHashAlgo::Sha384.digest_len())?; - let (empty_salt, prk) = prk_area.split_at_mut(0); - pal.hkdf_extract(io, HsmHashAlgo::Sha384, empty_salt, secret, prk) + // HKDF-Extract with the RFC 5869 §2.2 default (absent) salt. + let prk = pal.dma_alloc(io, HsmHashAlgo::Sha384.digest_len())?; + pal.hkdf_extract(io, HsmHashAlgo::Sha384, None, secret, prk) .await?; - pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, nonce, okm_out) + pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, Some(nonce), okm_out) .await } @@ -465,16 +463,12 @@ async fn derive_bk3_session( let session_label = pal.dma_alloc(io, SESSION_BK3_LABEL.len())?; session_label.copy_from_slice(SESSION_BK3_LABEL); - // Empty 0-length `&DmaBuf` for `context` — borrowed off `bk3` to - // avoid a separate DMA allocation just for an empty buffer. - let (empty_context, _) = bk3.split_at(0); - pal.sp800_108_kdf( io, HsmHashAlgo::Sha384, bk3, - session_label, - empty_context, + Some(session_label), + None, bk3_session_out, ) .await diff --git a/fw/core/lib/src/ddi/mbor/hkdf_derive.rs b/fw/core/lib/src/ddi/mbor/hkdf_derive.rs index 2f37feb7..f3a99fb5 100644 --- a/fw/core/lib/src/ddi/mbor/hkdf_derive.rs +++ b/fw/core/lib/src/ddi/mbor/hkdf_derive.rs @@ -54,27 +54,26 @@ pub(crate) async fn hkdf_derive<'p, P: HsmPal>( // Derive the OKM into a DMA scratch slot; `vault_key_create` // copies it into vault-owned storage so the scratch can drop - // after. HKDF-Extract with an absent salt uses a zero-length - // `DmaBuf` (`split_at_mut(0)`), which the std PAL maps to the - // RFC 5869 default all-zero salt. + // after. An absent salt (`None`) selects the RFC 5869 default + // all-zero salt. + // + // `out` and `prk` are allocated separately rather than carved + // from one buffer: each `dma_alloc` is independently 4-byte + // aligned, which the crypto DMA engine requires. `out_len` is + // caller-controlled and need not be 4-aligned (variable-length + // HMAC outputs), so splitting a single buffer at `out_len` could + // leave `prk` misaligned. let out = pal.dma_alloc(io, target.out_len)?; - let prk_area = pal.dma_alloc(io, algo.digest_len())?; - let (empty, prk) = prk_area.split_at_mut(0); + let prk = pal.dma_alloc(io, algo.digest_len())?; - let salt_buf: &DmaBuf = match body.salt.as_deref() { - Some(salt) => salt, - None => empty, - }; { let ikm = pal.vault_key(io, input_key_id)?; - pal.hkdf_extract(io, algo, salt_buf, ikm, prk).await?; + pal.hkdf_extract(io, algo, body.salt.as_deref(), ikm, prk) + .await?; } - let info_buf: &DmaBuf = match body.info.as_deref() { - Some(info) => info, - None => empty, - }; - pal.hkdf_expand(io, algo, prk, info_buf, out).await?; + pal.hkdf_expand(io, algo, prk, body.info.as_deref(), out) + .await?; // RAII vault entry — rolls back if response encoding below fails. // `masked_key` is the host's opaque re-import blob; firmware-side diff --git a/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs b/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs index 42f8b6d8..88de6240 100644 --- a/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs +++ b/fw/core/lib/src/ddi/mbor/kbkdf_derive.rs @@ -58,24 +58,20 @@ pub(crate) async fn kbkdf_counter_hmac_derive<'p, P: HsmPal>( // Derive the OKM into a DMA scratch slot; `vault_key_create` // copies it into vault-owned storage so the scratch can drop - // after. Absent label / context use a zero-length `DmaBuf` - // (`split_at_mut(0)` carves an empty slice off the output - // allocation). - let out_area = pal.dma_alloc(io, target.out_len)?; - let (empty, out) = out_area.split_at_mut(0); + // after. Absent label / context are passed as `None`. + let out = pal.dma_alloc(io, target.out_len)?; - let label_buf: &DmaBuf = match body.label.as_deref() { - Some(label) => label, - None => empty, - }; - let context_buf: &DmaBuf = match body.context.as_deref() { - Some(context) => context, - None => empty, - }; { let kdk = pal.vault_key(io, input_key_id)?; - pal.sp800_108_kdf(io, algo, kdk, label_buf, context_buf, out) - .await?; + pal.sp800_108_kdf( + io, + algo, + kdk, + body.label.as_deref(), + body.context.as_deref(), + out, + ) + .await?; } // RAII vault entry — rolls back if response encoding below fails. diff --git a/fw/core/lib/src/ddi/mbor/open_session.rs b/fw/core/lib/src/ddi/mbor/open_session.rs index f8ec2dfc..a2858f86 100644 --- a/fw/core/lib/src/ddi/mbor/open_session.rs +++ b/fw/core/lib/src/ddi/mbor/open_session.rs @@ -120,8 +120,8 @@ pub(crate) async fn open_session<'p, P: HsmPal>( io, HsmHashAlgo::Sha384, bk_boot, - session_bk_label, - body.encrypted_credential.encrypted_seed, + Some(session_bk_label), + Some(body.encrypted_credential.encrypted_seed), bk_session, ) .await?; @@ -228,13 +228,12 @@ async fn derive_session_credential_keys( .await?; } - // HKDF-Extract with empty salt (RFC 5869 §2.2). - let prk_area = pal.dma_alloc(io, HsmHashAlgo::Sha384.digest_len())?; - let (empty_salt, prk) = prk_area.split_at_mut(0); - pal.hkdf_extract(io, HsmHashAlgo::Sha384, empty_salt, secret, prk) + // HKDF-Extract with the RFC 5869 §2.2 default (absent) salt. + let prk = pal.dma_alloc(io, HsmHashAlgo::Sha384.digest_len())?; + pal.hkdf_extract(io, HsmHashAlgo::Sha384, None, secret, prk) .await?; - pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, nonce, okm_out) + pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, Some(nonce), okm_out) .await } diff --git a/fw/core/lib/src/ddi/tbor/open_session_finish.rs b/fw/core/lib/src/ddi/tbor/open_session_finish.rs index eeae7012..94e09d59 100644 --- a/fw/core/lib/src/ddi/tbor/open_session_finish.rs +++ b/fw/core/lib/src/ddi/tbor/open_session_finish.rs @@ -416,7 +416,7 @@ async fn hkdf_expand_labeled<'a, P: HsmPal>( info[..label.len()].copy_from_slice(label); info[label.len()..].copy_from_slice(&(out_len as u16).to_be_bytes()); let out = alloc.dma_alloc(out_len)?; - pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, info, out) + pal.hkdf_expand(io, HsmHashAlgo::Sha384, prk, Some(info), out) .await?; Ok(out) } @@ -501,8 +501,15 @@ async fn derive_bk_session<'a, P: HsmPal>( label.copy_from_slice(SESSION_BK_LABEL); let bk_session = alloc.dma_alloc(SESSION_BK_LEN)?; - pal.sp800_108_kdf(io, HsmHashAlgo::Sha384, bk_boot, label, seed, bk_session) - .await?; + pal.sp800_108_kdf( + io, + HsmHashAlgo::Sha384, + bk_boot, + Some(label), + Some(seed), + bk_session, + ) + .await?; Ok(bk_session) } diff --git a/fw/pal/traits/src/crypto/kdf.rs b/fw/pal/traits/src/crypto/kdf.rs index 3b60917d..7ef3910d 100644 --- a/fw/pal/traits/src/crypto/kdf.rs +++ b/fw/pal/traits/src/crypto/kdf.rs @@ -108,7 +108,7 @@ pub trait HsmKdf { /// /// - `io` — caller's I/O context (per-IO scope). /// - `algo` — underlying hash algorithm (e.g. SHA-256). - /// - `salt` — optional salt; `&[]` selects the RFC 5869 default + /// - `salt` — optional salt; `None` selects the RFC 5869 default /// (a string of zero bytes of `algo.digest_len()`). /// - `ikm` — input keying material. /// - `prk` — output PRK; must be at least `algo.digest_len()` @@ -126,7 +126,7 @@ pub trait HsmKdf { &self, io: &impl HsmIo, algo: HsmHashAlgo, - salt: &DmaBuf, + salt: Option<&DmaBuf>, ikm: &DmaBuf, prk: &mut DmaBuf, ) -> HsmResult<()>; @@ -146,7 +146,7 @@ pub trait HsmKdf { /// - `algo` — underlying hash algorithm. /// - `prk` — PRK from /// [`hkdf_extract`](Self::hkdf_extract). - /// - `info` — context / application info; `&[]` to omit. + /// - `info` — context / application info; `None` to omit. /// - `output` — OKM destination; `output.len()` must satisfy /// `output.len() <= 255 * algo.digest_len()`. /// @@ -163,7 +163,7 @@ pub trait HsmKdf { io: &impl HsmIo, algo: HsmHashAlgo, prk: &DmaBuf, - info: &DmaBuf, + info: Option<&DmaBuf>, output: &mut DmaBuf, ) -> HsmResult<()>; @@ -177,8 +177,8 @@ pub trait HsmKdf { /// - `io` — caller's I/O context (per-IO scope). /// - `algo` — HMAC underlying hash (e.g. SHA-384). /// - `key` — key-derivation key (KDK). - /// - `label` — purpose string; `&[]` to omit. - /// - `context` — binding context; `&[]` to omit. + /// - `label` — purpose string; `None` to omit. + /// - `context` — binding context; `None` to omit. /// - `output` — derived-key destination; `output.len()` bytes /// are produced. /// @@ -194,8 +194,8 @@ pub trait HsmKdf { io: &impl HsmIo, algo: HsmHashAlgo, key: &DmaBuf, - label: &DmaBuf, - context: &DmaBuf, + label: Option<&DmaBuf>, + context: Option<&DmaBuf>, output: &mut DmaBuf, ) -> HsmResult<()>; diff --git a/fw/plat/std/pal/src/drivers/kdf.rs b/fw/plat/std/pal/src/drivers/kdf.rs index 9649468e..1e4e52ee 100644 --- a/fw/plat/std/pal/src/drivers/kdf.rs +++ b/fw/plat/std/pal/src/drivers/kdf.rs @@ -40,6 +40,16 @@ use azihsm_fw_hsm_pal_traits::*; use crate::worker::WorkerPool; +/// Copies an optional KDF input into an owned buffer for the worker +/// closure, treating a present-but-empty slice as absent (`None`). +/// +/// Salt / info / label / context are all optional; a zero-length +/// slice is collapsed to `None` so the empty and omitted cases stay +/// byte-identical and OpenSSL is never handed an explicit empty input. +fn owned_nonempty(input: Option<&[u8]>) -> Option> { + input.filter(|s| !s.is_empty()).map(<[u8]>::to_vec) +} + /// Std KDF driver — software HKDF/KBKDF via OpenSSL with async worker dispatch. /// /// Created once during PAL initialization and shared across all IO tasks. @@ -61,10 +71,9 @@ impl StdKdf { /// - `hash_algo` — The hash algorithm for the underlying HMAC /// (e.g., `HashAlgo::sha256()`). /// - `mode` — Which HKDF phase(s) to perform. - /// - `salt` — Optional salt value. Pass an empty slice to use the - /// default salt. - /// - `info` — Context and application-specific info. Pass an empty - /// slice if not needed. + /// - `salt` — Optional salt value. `None` selects the default + /// salt. + /// - `info` — Optional context / application-specific info. /// - `output` — Buffer for the derived output key material (OKM). /// The buffer length determines how many bytes are derived. /// @@ -75,13 +84,13 @@ impl StdKdf { key: &[u8], hash_algo: HashAlgo, mode: azihsm_crypto::HkdfMode, - salt: &[u8], - info: &[u8], + salt: Option<&[u8]>, + info: Option<&[u8]>, output: &mut [u8], ) -> HsmResult<()> { let key_owned = key.to_vec(); - let salt_owned = salt.to_vec(); - let info_owned = info.to_vec(); + let salt_owned = owned_nonempty(salt); + let info_owned = owned_nonempty(info); let derive_len = output.len(); let result = self @@ -89,17 +98,12 @@ impl StdKdf { .submit_with_result(async move { let input_key = GenericSecretKey::from_bytes(&key_owned).map_err(|_| HsmError::HkdfError)?; - let salt_opt = if salt_owned.is_empty() { - None - } else { - Some(salt_owned.as_slice()) - }; - let info_opt = if info_owned.is_empty() { - None - } else { - Some(info_owned.as_slice()) - }; - let algo = HkdfAlgo::new(mode, &hash_algo, salt_opt, info_opt); + let algo = HkdfAlgo::new( + mode, + &hash_algo, + salt_owned.as_deref(), + info_owned.as_deref(), + ); let derived = algo .derive(&input_key, derive_len) .map_err(|_| HsmError::HkdfError)?; @@ -118,10 +122,10 @@ impl StdKdf { /// # Parameters /// - `key` — The key-derivation key (KDK). /// - `hash_algo` — The HMAC hash algorithm (e.g., `HashAlgo::sha256()`). - /// - `label` — A string identifying the purpose of the derived key. - /// Pass an empty slice if not needed. - /// - `context` — Context information binding the derived key to a - /// specific use. Pass an empty slice if not needed. + /// - `label` — Optional string identifying the purpose of the + /// derived key. + /// - `context` — Optional context information binding the derived + /// key to a specific use. /// - `output` — Buffer for the derived key material. The buffer /// length determines how many bytes are derived. /// @@ -131,13 +135,13 @@ impl StdKdf { &self, key: &[u8], hash_algo: HashAlgo, - label: &[u8], - context: &[u8], + label: Option<&[u8]>, + context: Option<&[u8]>, output: &mut [u8], ) -> HsmResult<()> { let key_owned = key.to_vec(); - let label_owned = label.to_vec(); - let context_owned = context.to_vec(); + let label_owned = owned_nonempty(label); + let context_owned = owned_nonempty(context); let derive_len = output.len(); let result = self @@ -145,17 +149,7 @@ impl StdKdf { .submit_with_result(async move { let input_key = GenericSecretKey::from_bytes(&key_owned).map_err(|_| HsmError::KbkdfError)?; - let label_opt = if label_owned.is_empty() { - None - } else { - Some(label_owned) - }; - let context_opt = if context_owned.is_empty() { - None - } else { - Some(context_owned) - }; - let algo = KbkdfAlgo::with_len(hash_algo, label_opt, context_opt); + let algo = KbkdfAlgo::with_len(hash_algo, label_owned, context_owned); let derived = algo .derive(&input_key, derive_len) .map_err(|_| HsmError::KbkdfError)?; @@ -193,8 +187,8 @@ mod tests { &key, HashAlgo::sha256(), azihsm_crypto::HkdfMode::ExtractAndExpand, - salt, - info, + Some(salt), + Some(info), &mut output, ) .await @@ -214,8 +208,8 @@ mod tests { &key, HashAlgo::sha384(), azihsm_crypto::HkdfMode::ExtractAndExpand, - salt, - info, + Some(salt), + Some(info), &mut output, ) .await @@ -235,8 +229,8 @@ mod tests { &key, HashAlgo::sha512(), azihsm_crypto::HkdfMode::ExtractAndExpand, - salt, - info, + Some(salt), + Some(info), &mut output, ) .await @@ -255,8 +249,8 @@ mod tests { &key, HashAlgo::sha256(), azihsm_crypto::HkdfMode::Extract, - salt, - &[], + Some(salt), + None, &mut prk, ) .await @@ -277,8 +271,8 @@ mod tests { &key, HashAlgo::sha256(), azihsm_crypto::HkdfMode::Extract, - salt, - &[], + Some(salt), + None, &mut prk, ) .await @@ -291,8 +285,8 @@ mod tests { &prk, HashAlgo::sha256(), azihsm_crypto::HkdfMode::Expand, - &[], - b"expand-info", + None, + Some(b"expand-info"), &mut okm, ) .await @@ -321,8 +315,8 @@ mod tests { &ikm, HashAlgo::sha256(), azihsm_crypto::HkdfMode::ExtractAndExpand, - &salt, - &info, + Some(&salt), + Some(&info), &mut okm, ) .await @@ -344,11 +338,23 @@ mod tests { let mut out1 = [0u8; 32]; let mut out2 = [0u8; 32]; driver - .kbkdf(&key, HashAlgo::sha256(), label, context, &mut out1) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(label), + Some(context), + &mut out1, + ) .await .unwrap(); driver - .kbkdf(&key, HashAlgo::sha256(), label, context, &mut out2) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(label), + Some(context), + &mut out2, + ) .await .unwrap(); assert_ne!(out1, [0u8; 32]); @@ -361,7 +367,13 @@ mod tests { let key = [0xeeu8; 48]; let mut output = [0u8; 48]; driver - .kbkdf(&key, HashAlgo::sha384(), b"label", b"ctx", &mut output) + .kbkdf( + &key, + HashAlgo::sha384(), + Some(b"label"), + Some(b"ctx"), + &mut output, + ) .await .unwrap(); assert_ne!(output, [0u8; 48]); @@ -373,7 +385,13 @@ mod tests { let key = [0xffu8; 64]; let mut output = [0u8; 64]; driver - .kbkdf(&key, HashAlgo::sha512(), b"label", b"ctx", &mut output) + .kbkdf( + &key, + HashAlgo::sha512(), + Some(b"label"), + Some(b"ctx"), + &mut output, + ) .await .unwrap(); assert_ne!(output, [0u8; 64]); @@ -387,11 +405,23 @@ mod tests { let mut out_a = [0u8; 32]; let mut out_b = [0u8; 32]; driver - .kbkdf(&key, HashAlgo::sha256(), b"label-a", context, &mut out_a) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(b"label-a"), + Some(context), + &mut out_a, + ) .await .unwrap(); driver - .kbkdf(&key, HashAlgo::sha256(), b"label-b", context, &mut out_b) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(b"label-b"), + Some(context), + &mut out_b, + ) .await .unwrap(); assert_ne!( @@ -408,11 +438,23 @@ mod tests { let mut out_a = [0u8; 32]; let mut out_b = [0u8; 32]; driver - .kbkdf(&key, HashAlgo::sha256(), label, b"ctx-a", &mut out_a) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(label), + Some(b"ctx-a"), + &mut out_a, + ) .await .unwrap(); driver - .kbkdf(&key, HashAlgo::sha256(), label, b"ctx-b", &mut out_b) + .kbkdf( + &key, + HashAlgo::sha256(), + Some(label), + Some(b"ctx-b"), + &mut out_b, + ) .await .unwrap(); assert_ne!( diff --git a/fw/plat/std/pal/src/kdf.rs b/fw/plat/std/pal/src/kdf.rs index fefcf66c..0b4d5a66 100644 --- a/fw/plat/std/pal/src/kdf.rs +++ b/fw/plat/std/pal/src/kdf.rs @@ -11,6 +11,8 @@ //! OpenSSL. The remaining hash-based KDF helpers are currently left as //! `todo!()` stubs. +use core::ops::Deref; + use azihsm_crypto::HashAlgo; use super::*; @@ -29,7 +31,7 @@ impl HsmKdf for StdHsmPal { &self, _io: &impl HsmIo, algo: HsmHashAlgo, - salt: &DmaBuf, + salt: Option<&DmaBuf>, ikm: &DmaBuf, prk: &mut DmaBuf, ) -> HsmResult<()> { @@ -38,8 +40,8 @@ impl HsmKdf for StdHsmPal { ikm, to_hash_algo(algo), azihsm_crypto::HkdfMode::Extract, - salt, - &[], + salt.map(|s| s.deref()), + None, prk, ) .await @@ -50,7 +52,7 @@ impl HsmKdf for StdHsmPal { _io: &impl HsmIo, algo: HsmHashAlgo, prk: &DmaBuf, - info: &DmaBuf, + info: Option<&DmaBuf>, output: &mut DmaBuf, ) -> HsmResult<()> { self.kdf @@ -58,8 +60,8 @@ impl HsmKdf for StdHsmPal { prk, to_hash_algo(algo), azihsm_crypto::HkdfMode::Expand, - &[], - info, + None, + info.map(|s| s.deref()), output, ) .await @@ -70,12 +72,18 @@ impl HsmKdf for StdHsmPal { _io: &impl HsmIo, algo: HsmHashAlgo, key: &DmaBuf, - label: &DmaBuf, - context: &DmaBuf, + label: Option<&DmaBuf>, + context: Option<&DmaBuf>, output: &mut DmaBuf, ) -> HsmResult<()> { self.kdf - .kbkdf(key, to_hash_algo(algo), label, context, output) + .kbkdf( + key, + to_hash_algo(algo), + label.map(|s| s.deref()), + context.map(|s| s.deref()), + output, + ) .await } diff --git a/fw/plat/std/pal/src/part.rs b/fw/plat/std/pal/src/part.rs index b08ce0d4..c4aa3e2c 100644 --- a/fw/plat/std/pal/src/part.rs +++ b/fw/plat/std/pal/src/part.rs @@ -658,8 +658,15 @@ impl HsmPartitionManager for StdHsmPal { } } - self.sp800_108_kdf(io, HsmHashAlgo::Sha384, key_dma, label_dma, ctx_dma, output) - .await + self.sp800_108_kdf( + io, + HsmHashAlgo::Sha384, + key_dma, + Some(label_dma), + Some(ctx_dma), + output, + ) + .await } fn part_verify_nonce(&self, io: &impl HsmIo, nonce: &[u8]) -> HsmResult<()> {