From 6f76f87c60d8eff845e9f95bc37eed7f7b371a30 Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Sun, 1 Nov 2026 17:28:42 +0100 Subject: [PATCH 1/2] SE050Sim: HKDFExtractAndExpand AN12543 4.15.1.1: RFC 5869 HKDF with an HMACKey object as the input keying material, which never leaves the SE. Digest modes SHA-1/256/384/512, optional salt and info, output of 1-768 bytes returned to the host or written into an existing HMACKey/AESKey of that length (TAG_7). The IKM key needs POLICY_OBJ_ALLOW_KDF when it has a policy. Salt from an HMACKey (TAG_6) is not modelled. Unit tests: RFC 5869 test case 1, and the APDU path with a policy that allows and one that does not allow KDF. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/dispatch.rs | 1 + SE050Sim/se050-sim/src/handlers/kdf.rs | 168 +++++++++++++++++++++ SE050Sim/se050-sim/src/handlers/mod.rs | 1 + SE050Sim/se050-sim/src/object_store/mod.rs | 5 + 4 files changed, 175 insertions(+) create mode 100644 SE050Sim/se050-sim/src/handlers/kdf.rs diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 2661110..abbc896 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -164,6 +164,7 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> } // Digest oneshot + (P1_DEFAULT, handlers::kdf::P2_HKDF) => handlers::kdf::handle_hkdf(apdu, store), (P1_DEFAULT, P2_ONESHOT) => handlers::digest::handle_digest_oneshot(apdu, store), // Digest multi-step diff --git a/SE050Sim/se050-sim/src/handlers/kdf.rs b/SE050Sim/se050-sim/src/handlers/kdf.rs new file mode 100644 index 0000000..448039c --- /dev/null +++ b/SE050Sim/se050-sim/src/handlers/kdf.rs @@ -0,0 +1,168 @@ +/* kdf.rs + * + * Copyright (C) 2026 wolfSSL Inc. and Cardea Key contributors + * + * This file is part of SE050Sim. + * + * SE050Sim is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * SE050Sim is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +//! HKDFExtractAndExpand (AN12543 4.15.1.1): RFC 5869 HKDF with an HMACKey +//! object as the input keying material, which never leaves the SE. +//! +//! TLV[TAG_1] IKM HMACKey id (policy: ALLOW_KDF), TAG_2 digest mode, TAG_3 +//! salt (optional; empty = default salt of zeros), TAG_4 info (optional), +//! TAG_5 2-byte output length (1..768), TAG_7 optional target object (HMACKey +//! or AESKey of exactly that length) instead of returning the output. +//! TAG_6 (salt from an HMACKey) is not modelled. + +use hmac::{Mac, SimpleHmac}; + +use crate::apdu::*; +use crate::object_store::types::SecureObject; +use crate::object_store::ObjectStore; +use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_5}; + +pub const P2_HKDF: u8 = 0x2D; +pub const POLICY_OBJ_ALLOW_KDF: u32 = 0x0080_0000; +const TAG_7: u8 = 0x47; + +fn hkdf(salt: &[u8], ikm: &[u8], info: &[u8], len: usize) -> Option> +where + D: hmac::digest::Digest + hmac::digest::core_api::BlockSizeUser + Clone, +{ + let hash_len = ::output_size(); + if len > 255 * hash_len { + return None; + } + let zeros = vec![0u8; hash_len]; + let salt = if salt.is_empty() { &zeros[..] } else { salt }; + let mut ext = as hmac::digest::KeyInit>::new_from_slice(salt).ok()?; + ext.update(ikm); + let prk = ext.finalize().into_bytes(); + let mut out = Vec::with_capacity(len + hash_len); + let mut t: Vec = Vec::new(); + let mut i: u8 = 1; + while out.len() < len { + let mut m = as hmac::digest::KeyInit>::new_from_slice(&prk).ok()?; + m.update(&t); + m.update(info); + m.update(&[i]); + t = m.finalize().into_bytes().to_vec(); + out.extend_from_slice(&t); + i = i.checked_add(1)?; + } + out.truncate(len); + Some(out) +} + +pub fn handle_hkdf(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Ok(tlvs) = apdu.parse_tlvs() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let id: Option<[u8; 4]> = tlv::find_tlv(&tlvs, TAG_1).and_then(|t| t.value.as_slice().try_into().ok()); + let mode = tlv::find_tlv(&tlvs, TAG_2).and_then(|t| t.value.first().copied()); + let len = tlv::find_tlv(&tlvs, TAG_5) + .filter(|t| t.value.len() == 2) + .map(|t| u16::from_be_bytes([t.value[0], t.value[1]]) as usize); + let (Some(id), Some(mode), Some(len)) = (id, mode, len) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if len == 0 || len > 768 { + return ApduResponse::error(SW_WRONG_DATA); + } + let salt = tlv::find_tlv(&tlvs, TAG_3).map(|t| t.value.clone()).unwrap_or_default(); + let info = tlv::find_tlv(&tlvs, TAG_4).map(|t| t.value.clone()).unwrap_or_default(); + let ikm = match store.get(&id) { + Some(SecureObject::HMACKey { key, .. }) => key.clone(), + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + if !store.policy_allows(&id, POLICY_OBJ_ALLOW_KDF) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } + let out = match mode { + 0x01 => hkdf::(&salt, &ikm, &info, len), + 0x04 => hkdf::(&salt, &ikm, &info, len), + 0x05 => hkdf::(&salt, &ikm, &info, len), + 0x06 => hkdf::(&salt, &ikm, &info, len), + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let Some(out) = out else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if let Some(target) = tlv::find_tlv(&tlvs, TAG_7).and_then(|t| <[u8; 4]>::try_from(t.value.as_slice()).ok()) { + // Output into an existing object of exactly the requested length. + let fits = match store.get(&target) { + Some(SecureObject::HMACKey { key, .. }) | Some(SecureObject::AESKey { key }) => key.len() == len, + _ => false, + }; + if !fits || !store.policy_allows(&target, crate::policy::POLICY_OBJ_ALLOW_WRITE) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + match store.get_mut(&target) { + Some(SecureObject::HMACKey { key, .. }) | Some(SecureObject::AESKey { key }) => *key = out, + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + } + store.touch(); + return ApduResponse::success(); + } + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &out)]) +} + +#[cfg(test)] +mod tests { + use super::*; + + // RFC 5869 test case 1 (SHA-256) + #[test] + fn rfc5869_case1() { + let ikm = [0x0b; 22]; + let salt: Vec = (0x00..=0x0c).collect(); + let info: Vec = (0xf0..=0xf9).collect(); + let okm = hkdf::(&salt, &ikm, &info, 42).unwrap(); + assert_eq!(hex::encode(okm), "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); + } + + #[test] + fn hkdf_apdu_with_policy() { + let mut store = ObjectStore::new(); + let id = [0x7D, 0xA0, 0x00, 0x20]; + store.insert(id, SecureObject::HMACKey { key: vec![0x0b; 22], policy: None }); + let mut pol = vec![8u8]; + pol.extend_from_slice(&[0; 4]); + pol.extend_from_slice(&POLICY_OBJ_ALLOW_KDF.to_be_bytes()); + store.set_creation_metadata(id, Some(pol), 0x01); + let mut data = Tlv::new(TAG_1, &id).encode(); + data.extend(Tlv::new(TAG_2, &[0x04]).encode()); + data.extend(Tlv::new(TAG_3, &(0x00..=0x0c).collect::>()).encode()); + data.extend(Tlv::new(TAG_4, &(0xf0..=0xf9).collect::>()).encode()); + data.extend(Tlv::new(TAG_5, &[0x00, 42]).encode()); + let mut raw = vec![0x80, INS_CRYPTO, 0x00, P2_HKDF, data.len() as u8]; + raw.extend(data); + let r = handle_hkdf(&ParsedApdu::parse(&raw).unwrap(), &mut store); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + assert_eq!(hex::encode(&t[0].value), "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"); + // a key whose policy lacks ALLOW_KDF is refused + let id2 = [0x7D, 0xA0, 0x00, 0x21]; + store.insert(id2, SecureObject::HMACKey { key: vec![1; 32], policy: None }); + let mut pol = vec![8u8]; + pol.extend_from_slice(&[0; 4]); + pol.extend_from_slice(&0x1000_0000u32.to_be_bytes()); + store.set_creation_metadata(id2, Some(pol), 0x01); + raw[5 + 2..5 + 6].copy_from_slice(&id2); + assert_eq!(handle_hkdf(&ParsedApdu::parse(&raw).unwrap(), &mut store).sw, SW_COMMAND_NOT_ALLOWED); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/mod.rs b/SE050Sim/se050-sim/src/handlers/mod.rs index 5671c1d..32714a8 100644 --- a/SE050Sim/se050-sim/src/handlers/mod.rs +++ b/SE050Sim/se050-sim/src/handlers/mod.rs @@ -29,3 +29,4 @@ pub mod rsa; pub mod aes; pub mod digest; pub mod mac; +pub mod kdf; diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 4c53ac8..9529a09 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -129,6 +129,11 @@ impl ObjectStore { self.objects.get(id) } + /// Persist after an in-place change made through get_mut(). + pub fn touch(&mut self) { + self.persist(); + } + pub fn get_mut(&mut self, id: &[u8; 4]) -> Option<&mut SecureObject> { self.objects.get_mut(id) } From 39afaa0cfab998ab6f8f6c71ffa207f199d09e5a Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:14:54 +0100 Subject: [PATCH 2/2] SE050Sim: address review - HKDF refuses TAG_6 and malformed TAG_7 Two fixes from review feedback on HKDFExtractAndExpand. A TAG_7 that was not four bytes was treated as absent, so a malformed request to derive into an object returned the derived key to the host instead. It is now refused with 0x6A80; the host-output path is taken only when TAG_7 is absent. TAG_6 (salt taken from an HMACKey object) was ignored, so such a request succeeded with the default zero salt and produced a different key than asked for. Until object salts are modelled it is refused with 0x6A80. Test for both refusals. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/handlers/kdf.rs | 42 ++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/SE050Sim/se050-sim/src/handlers/kdf.rs b/SE050Sim/se050-sim/src/handlers/kdf.rs index 448039c..4d493d3 100644 --- a/SE050Sim/se050-sim/src/handlers/kdf.rs +++ b/SE050Sim/se050-sim/src/handlers/kdf.rs @@ -26,7 +26,8 @@ //! salt (optional; empty = default salt of zeros), TAG_4 info (optional), //! TAG_5 2-byte output length (1..768), TAG_7 optional target object (HMACKey //! or AESKey of exactly that length) instead of returning the output. -//! TAG_6 (salt from an HMACKey) is not modelled. +//! TAG_6 (salt from an HMACKey) is not modelled and is refused with 0x6A80; +//! a TAG_7 that is not a 4-byte object ID is refused too. use hmac::{Mac, SimpleHmac}; @@ -83,6 +84,19 @@ pub fn handle_hkdf(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { if len == 0 || len > 768 { return ApduResponse::error(SW_WRONG_DATA); } + // TAG_6 names an object holding the salt (Se05x_API_HKDF_Extended). + // Not supported yet: refused rather than silently using the zero salt. + if tlv::find_tlv(&tlvs, crate::tlv::TAG_6).is_some() { + return ApduResponse::error(SW_WRONG_DATA); + } + // TAG_7 present = output into that object; it must be an object ID. + let target: Option<[u8; 4]> = match tlv::find_tlv(&tlvs, TAG_7) { + None => None, + Some(t) => match <[u8; 4]>::try_from(t.value.as_slice()) { + Ok(id) => Some(id), + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }, + }; let salt = tlv::find_tlv(&tlvs, TAG_3).map(|t| t.value.clone()).unwrap_or_default(); let info = tlv::find_tlv(&tlvs, TAG_4).map(|t| t.value.clone()).unwrap_or_default(); let ikm = match store.get(&id) { @@ -102,7 +116,7 @@ pub fn handle_hkdf(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let Some(out) = out else { return ApduResponse::error(SW_WRONG_DATA); }; - if let Some(target) = tlv::find_tlv(&tlvs, TAG_7).and_then(|t| <[u8; 4]>::try_from(t.value.as_slice()).ok()) { + if let Some(target) = target { // Output into an existing object of exactly the requested length. let fits = match store.get(&target) { Some(SecureObject::HMACKey { key, .. }) | Some(SecureObject::AESKey { key }) => key.len() == len, @@ -165,4 +179,28 @@ mod tests { raw[5 + 2..5 + 6].copy_from_slice(&id2); assert_eq!(handle_hkdf(&ParsedApdu::parse(&raw).unwrap(), &mut store).sw, SW_COMMAND_NOT_ALLOWED); } + + #[test] + fn hkdf_refuses_salt_object_and_malformed_target() { + let mut store = ObjectStore::new(); + let id = [0x7D, 0xA0, 0x00, 0x20]; + store.insert(id, SecureObject::HMACKey { key: vec![0x0b; 22], policy: None }); + let mut base = |extra: &[u8]| { + let mut data = Tlv::new(TAG_1, &id).encode(); + data.extend(Tlv::new(TAG_2, &[0x04]).encode()); + data.extend(Tlv::new(TAG_5, &[0x00, 32]).encode()); + data.extend_from_slice(extra); + let mut raw = vec![0x80, INS_CRYPTO, 0x00, P2_HKDF, data.len() as u8]; + raw.extend(data); + handle_hkdf(&ParsedApdu::parse(&raw).unwrap(), &mut store) + }; + // salt from an object (TAG_6): not supported, refused + assert_eq!(base(&Tlv::new(crate::tlv::TAG_6, &[0x7D, 0xA0, 0x00, 0x30]).encode()).sw, SW_WRONG_DATA); + // TAG_7 that is not an object ID: refused, nothing returned + let r = base(&Tlv::new(TAG_7, &[0x7D, 0xA0, 0x00]).encode()); + assert_eq!(r.sw, SW_WRONG_DATA); + assert!(r.data.is_empty()); + // without either, the key comes back + assert_eq!(base(&[]).sw, SW_NO_ERROR); + } }