diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 4dd0429..d45f9fa 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -120,7 +120,8 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { ApduResponse::success_with_tlvs( &[crate::tlv::Tlv::new(crate::tlv::TAG_1, &curve_list)]) } - _ => handlers::object_mgmt::handle_read(apdu, store), + _ => handlers::object_mgmt::handle_read( + apdu, store, handlers::ec::strict_ecdh_from_env()), }, INS_CRYPTO => match (cred_type, apdu.p2) { diff --git a/SE050Sim/se050-sim/src/handlers/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index 0fced73..16e6339 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -22,7 +22,7 @@ use crate::apdu::*; use crate::object_store::types::SecureObject; use crate::object_store::{CryptoObjectState, ObjectStore}; -use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4}; +use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY}; use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit}; use aes::cipher::generic_array::GenericArray; @@ -82,8 +82,10 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR } /// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC). -/// Tag1=obj_id(4B), Tag3=key_data. HMAC keys have no fixed length, so any -/// non-empty Tag3 value is accepted as-is. +/// Policy(opt), Tag1=obj_id(4B), Tag3=key_data. HMAC keys have no fixed +/// length, so any non-empty Tag3 value is accepted as-is. An attached +/// policy is recorded with the object; the applet 7.2 read-policy +/// contract (see handle_read_object) keys off it. pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -99,9 +101,20 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu _ => return ApduResponse::error(SW_WRONG_DATA), }; + // A present but malformed (or empty) policy TLV is rejected up front, + // like the applet would, rather than being recorded as "no policy" and + // surfacing later as a strict-mode read denial. + let policy = match tlv::find_tlv(&tlvs, TAG_POLICY) { + Some(t) => match crate::policy::ar_header_union(&t.value) { + Some(header) => Some(header), + None => return ApduResponse::error(SW_WRONG_DATA), + }, + None => None, + }; + match tlv::find_tlv(&tlvs, TAG_3) { Some(t) if !t.value.is_empty() => { - store.insert(obj_id, SecureObject::HMACKey { key: t.value.clone() }); + store.insert(obj_id, SecureObject::HMACKey { key: t.value.clone(), policy }); ApduResponse::success() } _ => ApduResponse::error(SW_WRONG_DATA), @@ -441,11 +454,73 @@ mod hmac_write_tests { let resp = dispatch(&apdu, &mut store); assert_eq!(resp.sw, 0x9000); match store.get(&[0x00, 0x00, 0x00, 0x66]) { - Some(SecureObject::HMACKey { key: stored }) => assert_eq!(stored, &key), + Some(SecureObject::HMACKey { key: stored, policy }) => { + assert_eq!(stored, &key); + assert_eq!(*policy, None); + } + _ => panic!("HMACKey object not stored"), + } + } + + #[test] + fn test_write_hmac_key_records_policy() { + // WriteSymmKey with a leading policy TLV, as sent by + // sss_key_store_set_key when an sss_policy_t is attached. One + // entry: len=8, authId=0, AR header granting read. + let key = vec![0x5Au8; 32]; + let header: u32 = crate::policy::POLICY_OBJ_ALLOW_READ | 0x0014_0000; + let mut data = vec![crate::tlv::TAG_POLICY, 0x09, 0x08, 0x00, 0x00, 0x00, 0x00]; + data.extend_from_slice(&header.to_be_bytes()); + data.extend_from_slice(&[TAG_1, 0x04, 0x00, 0x00, 0x00, 0x67]); + data.push(TAG_3); + data.push(key.len() as u8); + data.extend_from_slice(&key); + + let apdu = ParsedApdu { + cla: 0x80, + ins: 0x21, + p1: P1_HMAC, + p2: P2_DEFAULT, + data, + le: None, + }; + let mut store = ObjectStore::new(); + let resp = dispatch(&apdu, &mut store); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0x00, 0x00, 0x00, 0x67]) { + Some(SecureObject::HMACKey { key: stored, policy }) => { + assert_eq!(stored, &key); + assert_eq!(*policy, Some(header)); + } _ => panic!("HMACKey object not stored"), } } + #[test] + fn test_write_hmac_key_malformed_policy_rejected() { + // A policy TLV that does not parse as an entry sequence must fail + // the write with SW_WRONG_DATA, not be recorded as "no policy". + let key = vec![0xA5u8; 32]; + let mut data = vec![crate::tlv::TAG_POLICY, 0x03, 0xDE, 0xAD, 0xBE]; + data.extend_from_slice(&[TAG_1, 0x04, 0x00, 0x00, 0x00, 0x68]); + data.push(TAG_3); + data.push(key.len() as u8); + data.extend_from_slice(&key); + + let apdu = ParsedApdu { + cla: 0x80, + ins: 0x21, + p1: P1_HMAC, + p2: P2_DEFAULT, + data, + le: None, + }; + let mut store = ObjectStore::new(); + let resp = dispatch(&apdu, &mut store); + assert_eq!(resp.sw, SW_WRONG_DATA); + assert!(store.get(&[0x00, 0x00, 0x00, 0x68]).is_none()); + } + #[test] fn test_write_hmac_key_empty_value_rejected() { let data = vec![TAG_1, 0x04, 0x00, 0x00, 0x00, 0x66, TAG_3, 0x00]; diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index 435d3d9..9361b79 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -463,7 +463,10 @@ pub fn handle_verify(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse /// Whether the applet 7.2 strict ECDH InObject contract is enforced. /// Off by default so hosts that predate the contract keep working; set -/// SE050_SIM_STRICT_ECDH=1 to enforce it. +/// SE050_SIM_STRICT_ECDH=1 to enforce it. Strict mode also enforces the +/// derive-target read policy: ReadObject on an HMACKey object is refused +/// with SW_COMMAND_NOT_ALLOWED unless the policy attached at creation +/// grants POLICY_OBJ_ALLOW_READ (see object_mgmt::handle_read). pub fn strict_ecdh_from_env() -> bool { std::env::var("SE050_SIM_STRICT_ECDH").map(|v| v == "1").unwrap_or(false) } @@ -518,9 +521,9 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> // On the real applet the Tag7 target must already exist as an HMACKey // object; the applet refuses to create it implicitly. In lenient mode a // missing (or non-HMACKey) target keeps the legacy implicit-create - // behavior instead (target_len None). - let target_len = match store.get(&output_id) { - Some(SecureObject::HMACKey { key }) => Some(key.len()), + // behavior instead (target None). + let target = match store.get(&output_id) { + Some(SecureObject::HMACKey { key, policy }) => Some((key.len(), *policy)), _ if strict => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), _ => None, }; @@ -543,12 +546,14 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> match shared_secret { Some(secret) => { - match target_len { - Some(len) => { + match target { + Some((len, policy)) => { if secret.len() != len { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } - store.insert(output_id, SecureObject::HMACKey { key: secret }); + // The derive overwrites the object's value; the policy + // attached at creation stays with the object. + store.insert(output_id, SecureObject::HMACKey { key: secret, policy }); } None => { // Legacy lenient behavior: implicitly create the target @@ -726,7 +731,8 @@ mod tests { // HMACKey, in strict and lenient mode alike. for strict in [true, false] { let (apdu, mut store, _) = ecdh_inobject_fixture(); - store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0u8; 16] }); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0u8; 16], policy: None }); let resp = handle_ecdh(&apdu, &mut store, strict); assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "strict={}", strict); } @@ -736,16 +742,37 @@ mod tests { fn test_ecdh_tag7_precreated_hmackey_succeeds_both_modes() { for strict in [true, false] { let (apdu, mut store, expected) = ecdh_inobject_fixture(); - store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0u8; 32] }); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0u8; 32], policy: None }); let resp = handle_ecdh(&apdu, &mut store, strict); assert_eq!(resp.sw, 0x9000, "strict={}", strict); match store.get(&[0, 0, 0, 0x66]) { - Some(SecureObject::HMACKey { key }) => assert_eq!(key, &expected), + Some(SecureObject::HMACKey { key, .. }) => assert_eq!(key, &expected), other => panic!("expected HMACKey with shared secret, got {:?}", other.is_some()), } } } + #[test] + fn test_ecdh_preserves_target_policy() { + // The policy attached when the derive target was created must + // survive the derive overwriting the value, or a subsequent + // strict-mode ReadObject of the shared secret would be refused. + let (apdu, mut store, expected) = ecdh_inobject_fixture(); + let policy = Some(crate::policy::POLICY_OBJ_ALLOW_READ); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0u8; 32], policy }); + let resp = handle_ecdh(&apdu, &mut store, true); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0, 0, 0, 0x66]) { + Some(SecureObject::HMACKey { key, policy: p }) => { + assert_eq!(key, &expected); + assert_eq!(*p, policy); + } + other => panic!("expected HMACKey, got {:?}", other.is_some()), + } + } + #[test] fn test_import_private_only_p256_derives_public() { // wc_ecc_use_key_id imports a private-only key pair and then reads diff --git a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs index 2a102a3..c2c2143 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -34,10 +34,12 @@ pub fn handle_write(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse } } -/// Handle READ commands for objects. -pub fn handle_read(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// Handle READ commands for objects. With `strict` (the applet 7.2 +/// contract, see ec::strict_ecdh_from_env) ReadObject enforces the +/// symmetric key read policy; size/list/type reads are unaffected. +pub fn handle_read(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> ApduResponse { match apdu.p2 { - P2_DEFAULT => handle_read_object(apdu, store), + P2_DEFAULT => handle_read_object(apdu, store, strict), P2_SIZE => handle_read_size(apdu, store), P2_LIST => handle_read_id_list(apdu, store), P2_TYPE => handle_read_type(apdu, store), @@ -190,7 +192,7 @@ fn handle_write_counter(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespo ApduResponse::success() } -fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), @@ -238,7 +240,21 @@ fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespons SecureObject::AESKey { key } => key.clone(), SecureObject::UserID { value } => value.clone(), SecureObject::Counter { value } => value.to_be_bytes().to_vec(), - SecureObject::HMACKey { key } => key.clone(), + SecureObject::HMACKey { key, policy } => { + // Applet 7.2 refuses to export a symmetric key object + // unless the policy attached at creation grants + // POLICY_OBJ_ALLOW_READ; an object created with no + // policy attached is not readable. This is the ECDH + // derive target readback contract, enforced only in + // strict mode so hosts that predate it keep working. + if strict + && policy.map_or(true, + |p| p & crate::policy::POLICY_OBJ_ALLOW_READ == 0) + { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } + key.clone() + } }; ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &data)]) } @@ -347,3 +363,82 @@ fn handle_delete(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { store.remove(&obj_id); ApduResponse::success() } + +#[cfg(test)] +mod read_policy_tests { + use super::*; + use crate::policy::POLICY_OBJ_ALLOW_READ; + + fn read_apdu(obj_id: [u8; 4]) -> ParsedApdu { + ParsedApdu { + cla: 0x80, + ins: INS_READ, + p1: P1_DEFAULT, + p2: P2_DEFAULT, + data: vec![TAG_1, 0x04, obj_id[0], obj_id[1], obj_id[2], obj_id[3]], + le: None, + } + } + + #[test] + fn test_strict_read_hmackey_without_policy_returns_6986() { + // Applet 7.2 behavior seen on SE05x hardware: an HMACKey object + // created with no policy attached cannot be read back, so the + // ECDH shared secret export fails unless the derive target was + // created with a read policy. + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0xAB; 32], policy: None }); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); + assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); + } + + #[test] + fn test_strict_read_hmackey_policy_without_read_returns_6986() { + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0xAB; 32], policy: Some(0x0014_0000) }); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); + assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); + } + + #[test] + fn test_strict_read_hmackey_with_read_policy_succeeds() { + let key = vec![0xABu8; 32]; + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { + key: key.clone(), + policy: Some(POLICY_OBJ_ALLOW_READ | 0x0014_0000), + }); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, key); + } + + #[test] + fn test_lenient_read_hmackey_without_policy_succeeds() { + // Hosts that predate the applet 7.2 contract keep working when + // strict mode is off. + let key = vec![0xABu8; 32]; + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: key.clone(), policy: None }); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, false); + assert_eq!(resp.sw, 0x9000); + } + + #[test] + fn test_strict_read_size_of_unreadable_hmackey_succeeds() { + // Only the object content is policy-guarded; ReadSize must keep + // working since sss_key_store_get_key sizes its buffer with it. + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x66], + SecureObject::HMACKey { key: vec![0xAB; 32], policy: None }); + let mut apdu = read_apdu([0, 0, 0, 0x66]); + apdu.p2 = P2_SIZE; + let resp = handle_read(&apdu, &mut store, true); + assert_eq!(resp.sw, 0x9000); + } +} diff --git a/SE050Sim/se050-sim/src/lib.rs b/SE050Sim/se050-sim/src/lib.rs index 6cd9b4f..6ef05b6 100644 --- a/SE050Sim/se050-sim/src/lib.rs +++ b/SE050Sim/se050-sim/src/lib.rs @@ -23,6 +23,7 @@ pub mod apdu; pub mod dispatch; pub mod handlers; pub mod object_store; +pub mod policy; pub mod t1; pub mod tlv; pub mod transport; diff --git a/SE050Sim/se050-sim/src/object_store/types.rs b/SE050Sim/se050-sim/src/object_store/types.rs index 1fbab24..9b42f67 100644 --- a/SE050Sim/se050-sim/src/object_store/types.rs +++ b/SE050Sim/se050-sim/src/object_store/types.rs @@ -110,6 +110,12 @@ pub enum SecureObject { }, HMACKey { key: Vec, + /// Union of the AR headers from the TAG_POLICY TLV attached at + /// creation; None when the object was created with no policy. + /// The applet 7.2 read-policy contract keys off this (see + /// crate::policy). + #[serde(default)] + policy: Option, }, } @@ -166,7 +172,7 @@ impl SecureObject { SecureObject::Binary { data } => data.len(), SecureObject::UserID { value } => value.len(), SecureObject::Counter { .. } => 8, - SecureObject::HMACKey { key } => key.len(), + SecureObject::HMACKey { key, .. } => key.len(), } } } diff --git a/SE050Sim/se050-sim/src/policy.rs b/SE050Sim/se050-sim/src/policy.rs new file mode 100644 index 0000000..b6c5637 --- /dev/null +++ b/SE050Sim/se050-sim/src/policy.rs @@ -0,0 +1,112 @@ +/* policy.rs + * + * Copyright (C) 2026 wolfSSL Inc. + * + * 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 + */ + +//! Minimal model of SE05x secure object policies. +//! +//! Covers only what the strict applet 7.2 ECDH derive-target contract +//! needs: a TAG_POLICY TLV attached to an object creation is a sequence +//! of entries `length(1) | authObjectId(4) | AR header(4, big endian) | +//! extension...` (see se05x_const.h in the Plug & Trust middleware, where +//! DEFAULT_OBJECT_POLICY_SIZE = 8 covers authObjectId + AR header), and +//! ReadObject on a symmetric key object is refused unless the policy +//! grants POLICY_OBJ_ALLOW_READ. + +/// POLICY_OBJ_ALLOW_READ bit of the 4-byte object policy AR header, +/// per se05x_const.h in the Plug & Trust middleware. +pub const POLICY_OBJ_ALLOW_READ: u32 = 0x0020_0000; + +/// Extract the union of all AR headers from a TAG_POLICY TLV value. +/// +/// The real applet evaluates the policy entry matching the session's +/// auth object; the simulator has no authentication, so it ORs every +/// entry's header together. Returns None if the value does not parse as +/// a policy entry sequence (including an empty value). +pub fn ar_header_union(value: &[u8]) -> Option { + let mut rest = value; + let mut header: u32 = 0; + let mut seen = false; + while !rest.is_empty() { + let len = rest[0] as usize; + // A valid entry covers at least authObjectId(4) + AR header(4); + // anything past that is a policy extension we don't model. + if len < 8 || rest.len() < 1 + len { + return None; + } + header |= u32::from_be_bytes([rest[5], rest[6], rest[7], rest[8]]); + seen = true; + rest = &rest[1 + len..]; + } + if seen { + Some(header) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(auth_id: u32, header: u32, ext: &[u8]) -> Vec { + let mut e = vec![(8 + ext.len()) as u8]; + e.extend_from_slice(&auth_id.to_be_bytes()); + e.extend_from_slice(&header.to_be_bytes()); + e.extend_from_slice(ext); + e + } + + #[test] + fn test_single_entry() { + let value = entry(0, POLICY_OBJ_ALLOW_READ | 0x0010_0000, &[]); + assert_eq!( + ar_header_union(&value), + Some(POLICY_OBJ_ALLOW_READ | 0x0010_0000) + ); + } + + #[test] + fn test_multiple_entries_are_ored() { + let mut value = entry(0, 0x0010_0000, &[]); + value.extend_from_slice(&entry(0x7DA00001, POLICY_OBJ_ALLOW_READ, &[])); + assert_eq!( + ar_header_union(&value), + Some(POLICY_OBJ_ALLOW_READ | 0x0010_0000) + ); + } + + #[test] + fn test_entry_with_extension() { + let value = entry(0, POLICY_OBJ_ALLOW_READ, &[0xAA; 4]); + assert_eq!(ar_header_union(&value), Some(POLICY_OBJ_ALLOW_READ)); + } + + #[test] + fn test_empty_value_is_none() { + assert_eq!(ar_header_union(&[]), None); + } + + #[test] + fn test_truncated_entry_is_none() { + let mut value = entry(0, POLICY_OBJ_ALLOW_READ, &[]); + value.truncate(6); + assert_eq!(ar_header_union(&value), None); + } +}