From 651e0f830ea348b8fd55a570cec568642f4df259 Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:02:35 +0100 Subject: [PATCH 1/6] SE050Sim: ECKey applet sessions, Authentication Objects, per-owner policies - CreateSession / ProcessSessionCmd / CloseSession (AN12543 3.6.2, 4.5.1) with ECKey authentication (ECKeySessionInternalAuthenticate, master key from ECDH with the SE's ECKey session key pair 0x7FFF0201) and the session's SCP03 channel, also nested inside Platform SCP03. - Authentication Objects (INS_AUTH_OBJECT) with a persistent failed-attempt counter and a hard block at the maximum; ReadObjectAttributes reports it. - Policies evaluated for the session owner (rules are not inherited across Authentication Object IDs), read-only default for Authentication Objects, ALLOW_SIGN / ALLOW_VERIFY checked for MAC operations. - TLV lengths in the 0x81 nn form, as the NXP host encodes them. - SE050_SIM_TRACE=1 logs every command after SCP03 unwrapping. Unit test of the whole flow (counting, blocking, policy scoping, close). Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/applet_session.rs | 573 ++++++++++++++++++ SE050Sim/se050-sim/src/handlers/ec.rs | 9 + SE050Sim/se050-sim/src/handlers/mac.rs | 12 + .../se050-sim/src/handlers/object_mgmt.rs | 17 +- SE050Sim/se050-sim/src/lib.rs | 1 + SE050Sim/se050-sim/src/object_store/mod.rs | 86 ++- SE050Sim/se050-sim/src/object_store/types.rs | 14 + SE050Sim/se050-sim/src/policy.rs | 19 + SE050Sim/se050-sim/src/scp03/mod.rs | 16 + SE050Sim/se050-sim/src/t1.rs | 23 +- SE050Sim/se050-sim/src/tlv.rs | 6 + 11 files changed, 765 insertions(+), 11 deletions(-) create mode 100644 SE050Sim/se050-sim/src/applet_session.rs diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs new file mode 100644 index 0000000..4e1e6c0 --- /dev/null +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -0,0 +1,573 @@ +/* applet_session.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 + */ + +//! Applet sessions (AN12543 3.6.2, 4.5.1): CreateSession, ProcessSessionCmd +//! and CloseSession, with ECKey authentication (AN12543 3.6.3.3, 4.5.4.1). +//! +//! An ECKey session is opened against an Authentication Object holding a NIST +//! P-256 public key. The host proves possession of the private key with +//! ECKeySessionInternalAuthenticate: an ECDSA signature over the control +//! reference template and an ephemeral public key. A wrong signature counts +//! as a failed attempt of the Authentication Object; once its maximum is +//! reached the object no longer authenticates. On success the session keys +//! come from ECDH between the ephemeral key and the SE's own key pair +//! (RESERVED_ID_ECKEY_SESSION), and every further command of the session is +//! SCP03-wrapped with them, inside the Platform SCP03 channel if one is open. +//! Commands run inside a session are evaluated with the session's +//! Authentication Object as the policy owner. +//! +//! The host side this mirrors is NXP Plug & Trust +//! (sss/src/se05x/fsl_sss_se05x_eckey.c and se05x_tlv.c). + +use std::collections::HashMap; + +use p256::ecdsa::signature::Verifier; +use rand::RngCore; +use sha2::{Digest, Sha256}; + +use crate::apdu::*; +use crate::object_store::types::{ECCurve, SecureObject}; +use crate::object_store::{ObjectStore, RESERVED_ID_ECKEY_SESSION}; +use crate::scp03::crypto; +use crate::scp03::Session as ScpSession; +use crate::tlv::{self, Tlv, TAG_1, TAG_SESSION_ID}; + +pub const P2_SESSION_CREATE: u8 = 0x1B; +pub const P2_SESSION_CLOSE: u8 = 0x1C; +const INS_AUTH_OBJECT: u8 = 0x40; +const INS_INTERNAL_AUTHENTICATE: u8 = 0x88; +const DDC_INITIAL_MCV: u8 = 0x08; + +/// SE050_SIM_TRACE=1: log every command as dispatched (after any SCP03 +/// unwrapping) with its status word, to stderr. For debugging a host. +pub fn trace(context: &str, apdu: &ParsedApdu, sw: u16) { + if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { + let data: String = apdu.data.iter().take(48).map(|b| format!("{:02x}", b)).collect(); + eprintln!( + "[trace] {:<8} {:02x} {:02x} {:02x} {:02x} [{}] {}{} -> {:04x}", + context, apdu.cla, apdu.ins, apdu.p1, apdu.p2, apdu.data.len(), data, + if apdu.data.len() > 48 { "..." } else { "" }, sw + ); + } +} + +struct AppletSession { + auth_id: [u8; 4], + /// None until ECKeySessionInternalAuthenticate succeeds. + scp: Option, +} + +/// Per-connection applet sessions. Reset by SELECT. +#[derive(Default)] +pub struct AppletSessions { + sessions: HashMap<[u8; 8], AppletSession>, +} + +impl AppletSessions { + pub fn reset(&mut self) { + self.sessions.clear(); + } + + /// Handles session management commands and the rules that depend on the + /// channel state. Returns None for ordinary commands, which the caller + /// dispatches in the default session. + pub fn intercept(&mut self, apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> Option { + store.set_session_owner([0; 4]); + if apdu.cla != 0x80 && apdu.cla != 0x84 { + return None; + } + let base_ins = apdu.base_ins(); + if base_ins == INS_MGMT && apdu.p2 == P2_SESSION_CREATE { + return Some(self.create(apdu, store)); + } + if base_ins == INS_PROCESS { + return Some(self.process(apdu, store, scp_active)); + } + // Authentication Objects are created only over an authenticated + // channel: bench-verified for UserID (0x6985 in a plain session on + // SE050C 3.1.1 and SE051 7.2.0); assumed alike for the other types. + if base_ins == INS_WRITE && apdu.ins & INS_AUTH_OBJECT != 0 && !scp_active { + return Some(ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED)); + } + if base_ins == INS_READ { + store.ensure_eckey_session_key(); + } + None + } + + fn create(&mut self, apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Some(auth_id) = object_id(apdu) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + // Only ECKey Authentication Objects (NIST P-256) are supported. + let is_eckey = matches!( + store.get(&auth_id), + Some(SecureObject::ECPublicKey { curve: ECCurve::NistP256, .. }) + | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) + ); + if store.auth_state(&auth_id).is_none() || !is_eckey { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let mut id = [0u8; 8]; + rand::rngs::OsRng.fill_bytes(&mut id); + self.sessions.insert(id, AppletSession { auth_id, scp: None }); + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &id)]) + } + + fn process(&mut self, apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> ApduResponse { + let Ok(tlvs) = apdu.parse_tlvs() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let (Some(sid), Some(inner)) = (tlv::find_tlv(&tlvs, TAG_SESSION_ID), tlv::find_tlv(&tlvs, TAG_1)) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let Ok(sid): Result<[u8; 8], _> = sid.value.as_slice().try_into() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let raw = inner.value.clone(); + let Some(session) = self.sessions.get_mut(&sid) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + if raw.len() < 4 { + return ApduResponse::error(SW_WRONG_DATA); + } + + let Some(scp) = session.scp.as_mut() else { + // Not authenticated yet: only the authentication (or closing). + let Ok(cmd) = ParsedApdu::parse(&raw) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if cmd.ins == INS_INTERNAL_AUTHENTICATE { + let auth_id = session.auth_id; + let result = internal_authenticate(&cmd, auth_id, store); + trace("eckauth", &cmd, result.as_ref().map(|_| SW_NO_ERROR).unwrap_or_else(|e| *e)); + return match result { + Ok((resp, scp)) => { + session.scp = Some(scp); + resp + } + Err(sw) => { + self.sessions.remove(&sid); + ApduResponse::error(sw) + } + }; + } + if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE { + self.sessions.remove(&sid); + return ApduResponse::success(); + } + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + + if raw[0] & 0x04 == 0 { + self.sessions.remove(&sid); + return ApduResponse::error(SW_SECURITY_STATUS); + } + let cmd = match scp.unwrap_command(&raw) { + Ok(c) => c, + Err(sw) => { + if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { + eprintln!("[trace] session unwrap failed {:04x}, session closed", sw); + } + self.sessions.remove(&sid); + return ApduResponse::error(sw); + } + }; + let closing = cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE; + let resp = if closing { + ApduResponse::success() + } else if cmd.base_ins() == INS_PROCESS || (cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CREATE) { + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) + } else { + store.set_session_owner(session.auth_id); + if cmd.base_ins() == INS_READ { + store.ensure_eckey_session_key(); + } + let r = crate::dispatch::dispatch(&cmd, store, scp_active); + store.set_session_owner([0; 4]); + r + }; + trace("session", &cmd, resp.sw); + let wrapped = scp.wrap_response(resp); + // The session also ends when its own Authentication Object is gone + // (deleted from within the session, AN12543 3.6.4). + if closing || !store.exists(&session.auth_id) { + self.sessions.remove(&sid); + } + wrapped + } +} + +fn object_id(apdu: &ParsedApdu) -> Option<[u8; 4]> { + let tlvs = apdu.parse_tlvs().ok()?; + tlv::find_tlv(&tlvs, TAG_1)?.value.as_slice().try_into().ok() +} + +/// One BER-TLV with 1- or 2-byte tag: (tag, value, total encoded length). +fn ber_tlv(data: &[u8]) -> Option<(u16, &[u8], usize)> { + let mut i = 0; + let mut tag = *data.first()? as u16; + i += 1; + if tag & 0x1F == 0x1F { + tag = (tag << 8) | *data.get(i)? as u16; + i += 1; + } + let mut len = *data.get(i)? as usize; + i += 1; + if len == 0x81 { + len = *data.get(i)? as usize; + i += 1; + } else if len == 0x82 { + len = ((*data.get(i)? as usize) << 8) | *data.get(i + 1)? as usize; + i += 2; + } else if len > 0x80 { + return None; + } + let value = data.get(i..i + len)?; + Some((tag, value, i + len)) +} + +fn find_ber(data: &[u8], want: u16) -> Option<&[u8]> { + let mut rest = data; + while !rest.is_empty() { + let (tag, value, used) = ber_tlv(rest)?; + if tag == want { + return Some(value); + } + rest = &rest[used..]; + } + None +} + +/// ECKeySessionInternalAuthenticate. On success: the plain response (SE +/// challenge and receipt) and the session's SCP03 state. +fn internal_authenticate(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore) -> Result<(ApduResponse, ScpSession), u16> { + let data = &cmd.data; + // Layout: A6 {4F AID, 90 SCP params, 80 key type, 81 key length}, + // 7F49 {B0 host ephemeral public key, F0 curve}, 5F37 signature. + let (t1, crt, n1) = ber_tlv(data).ok_or(SW_WRONG_DATA)?; + let (t2, epk, n2) = ber_tlv(&data[n1..]).ok_or(SW_WRONG_DATA)?; + let (t3, sig, _) = ber_tlv(&data[n1 + n2..]).ok_or(SW_WRONG_DATA)?; + if t1 != 0xA6 || t2 != 0x7F49 || t3 != 0x5F37 { + return Err(SW_WRONG_DATA); + } + let signed = &data[..n1 + n2]; + let scp_params = find_ber(crt, 0x90).ok_or(SW_WRONG_DATA)?; + if scp_params.len() != 3 || scp_params[0] != 0xAB || scp_params[1] != 0x01 { + return Err(SW_WRONG_DATA); + } + let level = scp_params[2]; + if level != 0x33 || find_ber(crt, 0x80) != Some(&[0x88][..]) || find_ber(crt, 0x81) != Some(&[0x10][..]) { + return Err(SW_WRONG_DATA); + } + let host_epk = find_ber(epk, 0xB0).ok_or(SW_WRONG_DATA)?; + if find_ber(epk, 0xF0) != Some(&[0x03][..]) { + return Err(SW_WRONG_DATA); + } + + if store.auth_blocked(&auth_id) { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + let auth_pub = match store.get(&auth_id) { + Some(SecureObject::ECPublicKey { public_key, .. }) | Some(SecureObject::ECKeyPair { public_key, .. }) => public_key.clone(), + _ => return Err(SW_CONDITIONS_NOT_SATISFIED), + }; + let verified = p256::ecdsa::VerifyingKey::from_sec1_bytes(&auth_pub) + .ok() + .zip(p256::ecdsa::Signature::from_der(sig).ok()) + .map(|(vk, s)| vk.verify(signed, &s).is_ok()) + .unwrap_or(false); + if !verified { + store.auth_failed(&auth_id); + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + store.auth_succeeded(&auth_id); + + // Key agreement with the SE's own ECKey session key pair. + store.ensure_eckey_session_key(); + let se_priv = match store.get(&RESERVED_ID_ECKEY_SESSION) { + Some(SecureObject::ECKeyPair { private_key, .. }) => private_key.clone(), + _ => return Err(SW_CONDITIONS_NOT_SATISFIED), + }; + let sk = p256::SecretKey::from_slice(&se_priv).map_err(|_| SW_CONDITIONS_NOT_SATISFIED)?; + let pk = p256::PublicKey::from_sec1_bytes(host_epk).map_err(|_| SW_WRONG_DATA)?; + let shared = p256::ecdh::diffie_hellman(sk.to_nonzero_scalar(), pk.as_affine()); + + let mut rnd = [0u8; 16]; + rand::rngs::OsRng.fill_bytes(&mut rnd); + // Master key: 16 MSBs of SHA-256(counter 00000001 || shared secret || + // SE random || SCP parameters 01 level || key type 88 || key length 10). + let mut h = Sha256::new(); + h.update([0x00, 0x00, 0x00, 0x01]); + h.update(shared.raw_secret_bytes()); + h.update(rnd); + h.update([0x01, level, 0x88, 0x10]); + let master = h.finalize()[..16].to_vec(); + + let s_enc = crypto::kdf(&master, crypto::DDC_S_ENC, &[], 128).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + let s_mac = crypto::kdf(&master, crypto::DDC_S_MAC, &[], 128).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + let s_rmac = crypto::kdf(&master, crypto::DDC_S_RMAC, &[], 128).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + let mcv_v = crypto::kdf(&master, DDC_INITIAL_MCV, &[], 128).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + let receipt = crate::handlers::mac::cmac_aes(&s_rmac, signed).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + let mut mcv = [0u8; 16]; + mcv.copy_from_slice(&mcv_v); + + let mut out = vec![0x85, 0x10]; + out.extend_from_slice(&rnd); + out.extend_from_slice(&[0x86, 0x10]); + out.extend_from_slice(&receipt[..16]); + Ok((ApduResponse::success_with_data(out), ScpSession::from_keys(s_enc, s_mac, s_rmac, mcv, level))) +} + +#[cfg(test)] +mod tests { + use super::*; + use p256::ecdsa::signature::Signer; + use p256::elliptic_curve::sec1::ToEncodedPoint; + + const AUTH_ID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + const HMAC_ID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x02]; + const AID: [u8; 16] = [0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00]; + + fn apdu(raw: &[u8]) -> ParsedApdu { + ParsedApdu::parse(raw).unwrap() + } + + fn cmd(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8]) -> Vec { + let mut v = vec![cla, ins, p1, p2]; + if !data.is_empty() { + if data.len() < 256 { + v.push(data.len() as u8); + } else { + v.extend_from_slice(&[0, (data.len() >> 8) as u8, data.len() as u8]); + } + v.extend_from_slice(data); + } + v + } + + fn tlv(tag: u8, value: &[u8]) -> Vec { + Tlv::new(tag, value).encode() + } + + fn run(sessions: &mut AppletSessions, store: &mut ObjectStore, raw: &[u8]) -> ApduResponse { + let a = apdu(raw); + sessions.intercept(&a, store, true).unwrap_or_else(|| crate::dispatch::dispatch(&a, store, true)) + } + + /// Store with an ECKey Authentication Object (max 3 attempts, delete only + /// for itself) and an HMAC key that only it may use to sign. + fn setup(auth: &p256::ecdsa::SigningKey) -> (ObjectStore, AppletSessions) { + let mut store = ObjectStore::new(); + let mut s = AppletSessions::default(); + let pubkey = auth.verifying_key().to_encoded_point(false).as_bytes().to_vec(); + let mut policy = vec![8]; + policy.extend_from_slice(&AUTH_ID); + policy.extend_from_slice(&crate::policy::POLICY_OBJ_ALLOW_DELETE.to_be_bytes()); + let mut data = tlv(crate::tlv::TAG_POLICY, &policy); + data.extend(tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0x00, 0x03])); + data.extend(tlv(TAG_1, &AUTH_ID)); + data.extend(tlv(crate::tlv::TAG_2, &[0x03])); + data.extend(tlv(crate::tlv::TAG_4, &pubkey)); + let r = run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_EC | P1_PUBLIC_KEY, 0x00, &data)); + assert_eq!(r.sw, SW_NO_ERROR); + + let mut hp = vec![8]; + hp.extend_from_slice(&AUTH_ID); + hp.extend_from_slice(&crate::policy::POLICY_OBJ_ALLOW_SIGN.to_be_bytes()); + hp.push(8); + hp.extend_from_slice(&[0; 4]); + hp.extend_from_slice(&crate::policy::POLICY_OBJ_ALLOW_DELETE.to_be_bytes()); + let mut data = tlv(crate::tlv::TAG_POLICY, &hp); + data.extend(tlv(TAG_1, &HMAC_ID)); + data.extend(tlv(crate::tlv::TAG_3, &[0x5A; 32])); + let r = run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_HMAC, 0x00, &data)); + assert_eq!(r.sw, SW_NO_ERROR); + (store, s) + } + + fn create(s: &mut AppletSessions, store: &mut ObjectStore) -> [u8; 8] { + let r = run(s, store, &cmd(0x80, INS_MGMT, 0x00, P2_SESSION_CREATE, &tlv(TAG_1, &AUTH_ID))); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + tlv::find_tlv(&t, TAG_1).unwrap().value.as_slice().try_into().unwrap() + } + + fn process(s: &mut AppletSessions, store: &mut ObjectStore, sid: &[u8; 8], inner: &[u8]) -> ApduResponse { + let mut data = tlv(TAG_SESSION_ID, sid); + data.extend(tlv(TAG_1, inner)); + run(s, store, &cmd(0x80, INS_PROCESS, 0x00, 0x00, &data)) + } + + struct Host { + s_enc: Vec, + s_mac: Vec, + s_rmac: Vec, + mcv: [u8; 16], + counter: u32, + } + + /// Host side of ECKeySessionInternalAuthenticate, as fsl_sss_se05x_eckey.c. + fn authenticate(s: &mut AppletSessions, store: &mut ObjectStore, sid: &[u8; 8], auth: &p256::ecdsa::SigningKey) -> Result { + let eph = p256::SecretKey::random(&mut rand::rngs::OsRng); + let eph_pub = eph.public_key().to_encoded_point(false).as_bytes().to_vec(); + let mut crt = tlv(0x4F, &AID); + crt.extend(tlv(0x90, &[0xAB, 0x01, 0x33])); + crt.extend(tlv(0x80, &[0x88])); + crt.extend(tlv(0x81, &[0x10])); + let mut signed = tlv(0xA6, &crt); + let mut epk = tlv(0xB0, &eph_pub); + epk.extend(tlv(0xF0, &[0x03])); + signed.extend_from_slice(&[0x7F, 0x49, epk.len() as u8]); + signed.extend(epk); + let sig: p256::ecdsa::Signature = auth.sign(&signed); + let der = sig.to_der(); + let mut data = signed.clone(); + data.extend_from_slice(&[0x5F, 0x37, der.as_bytes().len() as u8]); + data.extend_from_slice(der.as_bytes()); + let r = process(s, store, sid, &cmd(0x84, INS_INTERNAL_AUTHENTICATE, 0, 0, &data)); + if r.sw != SW_NO_ERROR { + return Err(r.sw); + } + assert_eq!(r.data.len(), 36); + let rnd = &r.data[2..18]; + let receipt = &r.data[20..36]; + let se_pub = match store.get(&RESERVED_ID_ECKEY_SESSION) { + Some(SecureObject::ECKeyPair { public_key, .. }) => p256::PublicKey::from_sec1_bytes(public_key).unwrap(), + _ => panic!("no SE key"), + }; + let shared = p256::ecdh::diffie_hellman(eph.to_nonzero_scalar(), se_pub.as_affine()); + let mut h = Sha256::new(); + h.update([0, 0, 0, 1]); + h.update(shared.raw_secret_bytes()); + h.update(rnd); + h.update([0x01, 0x33, 0x88, 0x10]); + let master = h.finalize()[..16].to_vec(); + let s_rmac = crypto::kdf(&master, crypto::DDC_S_RMAC, &[], 128).unwrap(); + assert_eq!(&crate::handlers::mac::cmac_aes(&s_rmac, &signed).unwrap()[..16], receipt, "receipt"); + let mut mcv = [0u8; 16]; + mcv.copy_from_slice(&crypto::kdf(&master, DDC_INITIAL_MCV, &[], 128).unwrap()); + Ok(Host { + s_enc: crypto::kdf(&master, crypto::DDC_S_ENC, &[], 128).unwrap(), + s_mac: crypto::kdf(&master, crypto::DDC_S_MAC, &[], 128).unwrap(), + s_rmac, + mcv, + counter: 1, + }) + } + + impl Host { + /// Wrap as se05x_Transform_scp (session case), send, unwrap as nxpSCP03_Decrypt_ResponseAPDU. + fn call(&mut self, s: &mut AppletSessions, store: &mut ObjectStore, sid: &[u8; 8], cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8]) -> (Vec, u16) { + let enc = if data.is_empty() { + vec![] + } else { + let icv = crypto::counter_icv(&self.s_enc, self.counter, false).unwrap(); + crypto::cbc_crypt(&self.s_enc, &icv, &crypto::pad_m2(data), true).unwrap() + }; + let mut inner = vec![cla | 0x04, ins, p1, p2, (enc.len() + 8) as u8]; + inner.extend_from_slice(&enc); + self.mcv = crypto::cmac_chain(&self.s_mac, &self.mcv, &inner).unwrap(); + inner.extend_from_slice(&self.mcv[..8]); + let r = process(s, store, sid, &inner); + let counter = self.counter; + self.counter += 1; + if r.sw != SW_NO_ERROR { + return (r.data, r.sw); + } + let (body, mac) = r.data.split_at(r.data.len() - 8); + let mut mac_in = body.to_vec(); + mac_in.extend_from_slice(&[0x90, 0x00]); + let full = crypto::cmac_chain(&self.s_rmac, &self.mcv, &mac_in).unwrap(); + assert_eq!(&full[..8], mac, "R-MAC"); + if body.is_empty() { + return (vec![], r.sw); + } + let icv = crypto::counter_icv(&self.s_enc, counter, true).unwrap(); + (crypto::unpad_m2(&crypto::cbc_crypt(&self.s_enc, &icv, body, false).unwrap()).unwrap(), r.sw) + } + } + + fn hmac_cmd() -> Vec { + let mut d = tlv(TAG_1, &HMAC_ID); + d.extend(tlv(crate::tlv::TAG_2, &[0x19])); // kSE05x_MACAlgo_HMAC_SHA256 + d.extend(tlv(crate::tlv::TAG_3, b"pin-kek")); + d + } + + #[test] + fn eckey_session_counts_blocks_and_scopes_policy() { + let good = p256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let bad = p256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let (mut store, mut s) = setup(&good); + + // Authentication Objects: not readable without READ in their policy, + // not creatable outside an authenticated channel. + let r = s.intercept(&apdu(&cmd(0x80, INS_WRITE | 0x40, P1_EC | P1_PUBLIC_KEY, 0, &tlv(TAG_1, &[1, 2, 3, 4]))), &mut store, false); + assert_eq!(r.unwrap().sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_READ, 0, 0, &tlv(TAG_1, &AUTH_ID))); + assert_ne!(r.sw, SW_NO_ERROR); + + // The HMAC key does not sign in the default session. + let r = run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_MAC, P2_GENERATE_ONESHOT, &hmac_cmd())); + assert_eq!(r.sw, SW_COMMAND_NOT_ALLOWED); + + // A wrong key counts. + let sid = create(&mut s, &mut store); + assert_eq!(authenticate(&mut s, &mut store, &sid, &bad).err(), Some(SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().attempts, 1); + + // The right key resets the counter; inside the session the key signs. + let sid = create(&mut s, &mut store); + let mut host = authenticate(&mut s, &mut store, &sid, &good).unwrap(); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().attempts, 0); + let (mac1, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_CRYPTO, P1_MAC, P2_GENERATE_ONESHOT, &hmac_cmd()); + assert_eq!(sw, SW_NO_ERROR); + let (mac2, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_CRYPTO, P1_MAC, P2_GENERATE_ONESHOT, &hmac_cmd()); + assert_eq!(sw, SW_NO_ERROR); + assert_eq!(mac1, mac2); + // Attributes: attempts and maximum, readable in the session. + let (attrs, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_READ, 0, P2_ATTRIBUTES, &tlv(TAG_1, &AUTH_ID)); + assert_eq!(sw, SW_NO_ERROR); + let a = &crate::tlv::parse_tlvs(&attrs).unwrap()[0].value; + assert_eq!((a[5], &a[6..8], &a[12..14]), (0x02, &[0, 0][..], &[0, 3][..])); + let (_, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_MGMT, 0, P2_SESSION_CLOSE, &[]); + assert_eq!(sw, SW_NO_ERROR); + assert_eq!(process(&mut s, &mut store, &sid, &[0x84, 0x02, 0, 0]).sw, SW_CONDITIONS_NOT_SATISFIED); + + // Three wrong keys block it for good, even for the right key. + for n in 1..=3 { + let sid = create(&mut s, &mut store); + assert!(authenticate(&mut s, &mut store, &sid, &bad).is_err()); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().attempts, n); + } + let sid = create(&mut s, &mut store); + assert_eq!(authenticate(&mut s, &mut store, &sid, &good).err(), Some(SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().attempts, 3); + + // The default session may delete the HMAC key (policy rule for 0) but + // not the Authentication Object (no rule for 0). + let r = run(&mut s, &mut store, &cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &AUTH_ID))); + assert_ne!(r.sw, SW_NO_ERROR); + let r = run(&mut s, &mut store, &cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &HMAC_ID))); + assert_eq!(r.sw, SW_NO_ERROR); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index bff0a7d..0020d2c 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -147,6 +147,15 @@ pub fn handle_write_ec_key( creation_policy, if generated { 0x02 } else { 0x01 }, ); + // INS_AUTH_OBJECT: an ECKey Authentication Object, with its maximum + // number of failed attempts (0 = unlimited, at most 0x7FFF). + if apdu.ins & 0x40 != 0 { + let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) + .filter(|t| t.value.len() == 2) + .map(|t| u16::from_be_bytes([t.value[0], t.value[1]]) & 0x7FFF) + .unwrap_or(0); + store.set_auth_object(obj_id, max); + } } response } diff --git a/SE050Sim/se050-sim/src/handlers/mac.rs b/SE050Sim/se050-sim/src/handlers/mac.rs index ecc4000..fd63107 100644 --- a/SE050Sim/se050-sim/src/handlers/mac.rs +++ b/SE050Sim/se050-sim/src/handlers/mac.rs @@ -150,6 +150,12 @@ pub fn handle_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool Some(obj) => obj.clone(), None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + // Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session + // owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table). + let needed = if validate { 0x0800_0000 } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; + if !store.policy_allows(&key_id, needed) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } match compute_mac(algo, &key_obj, &data) { Ok(mac) => mac_response(mac, expected.as_deref()), Err(sw) => ApduResponse::error(sw), @@ -260,6 +266,12 @@ pub fn handle_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse Some(obj) => obj.clone(), None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + // Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session + // owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table). + let needed = if validate { 0x0800_0000 } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; + if !store.policy_allows(&key_id, needed) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } match compute_mac(algo, &key_obj, &data) { Ok(mac) => mac_response(mac, expected.as_deref()), Err(sw) => ApduResponse::error(sw), diff --git a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs index 8ffb5d3..13d304c 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -440,10 +440,19 @@ fn handle_read_attributes( let mut attributes = Vec::new(); attributes.extend_from_slice(&obj_id); attributes.push(object.type_code(true)); - attributes.push(0x01); // kSE05x_SetIndicator_NOT_SET - attributes.extend_from_slice(&[0x00, 0x00]); // minimum AEAD tag length - attributes.extend_from_slice(&[0x00; 4]); // owner: unauthenticated session - attributes.extend_from_slice(&[0x00, 0x00]); // RFU + if let Some(auth) = store.auth_state(&obj_id) { + // Authentication Object layout (AN12543 table 3): indicator SET, + // failed attempts, owner, maximum attempts. + attributes.push(0x02); // kSE05x_SetIndicator_SET + attributes.extend_from_slice(&auth.attempts.to_be_bytes()); + attributes.extend_from_slice(&[0x00; 4]); + attributes.extend_from_slice(&auth.max_attempts.to_be_bytes()); + } else { + attributes.push(0x01); // kSE05x_SetIndicator_NOT_SET + attributes.extend_from_slice(&[0x00, 0x00]); // minimum AEAD tag length + attributes.extend_from_slice(&[0x00; 4]); // owner: unauthenticated session + attributes.extend_from_slice(&[0x00, 0x00]); // RFU + } if let Some(metadata) = store.metadata(&obj_id) { if let Some(policy) = &metadata.policy { attributes.extend_from_slice(policy); diff --git a/SE050Sim/se050-sim/src/lib.rs b/SE050Sim/se050-sim/src/lib.rs index 8072e55..1737ad1 100644 --- a/SE050Sim/se050-sim/src/lib.rs +++ b/SE050Sim/se050-sim/src/lib.rs @@ -21,6 +21,7 @@ pub mod apdu; pub mod applet; +pub mod applet_session; pub mod dispatch; pub mod handlers; pub mod object_store; diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 4c53ac8..1485d0d 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -84,8 +84,16 @@ pub struct ObjectStore { /// defaults (or environment overrides). Factory reset deliberately does /// not restore these keys, matching the irrecoverable real-card behavior. platform_scp: Option, + /// Authentication Object ID of the session running the current command + /// (all zeros = the default session). Set by the transport for each + /// command; policies are evaluated for this owner. Not persisted. + session_owner: [u8; 4], } +/// RESERVED_ID_ECKEY_SESSION (kSE05x_AppletResID_KP_ECKEY_USER): the SE's +/// own NIST P-256 key pair used for ECKey session key agreement. +pub const RESERVED_ID_ECKEY_SESSION: [u8; 4] = [0x7F, 0xFF, 0x02, 0x01]; + fn default_curves() -> HashMap { // P-192, P-224, P-256, P-384, P-521 fully parameterized. (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() @@ -102,6 +110,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], } } @@ -115,6 +124,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], }; store.load(); store @@ -143,7 +153,68 @@ impl ObjectStore { ) { self.metadata .entry(id) - .or_insert(ObjectMetadata { policy, origin }); + .or_insert(ObjectMetadata { policy, origin, auth: None }); + self.persist(); + } + + /// Mark a freshly created object as an Authentication Object. + pub fn set_auth_object(&mut self, id: [u8; 4], max_attempts: u16) { + if let Some(m) = self.metadata.get_mut(&id) { + m.auth = Some(crate::object_store::types::AuthState { max_attempts, attempts: 0 }); + self.persist(); + } + } + + pub fn auth_state(&self, id: &[u8; 4]) -> Option { + self.metadata.get(id).and_then(|m| m.auth) + } + + /// True when the Authentication Object has used up its attempts. + pub fn auth_blocked(&self, id: &[u8; 4]) -> bool { + matches!(self.auth_state(id), Some(a) if a.max_attempts != 0 && a.attempts >= a.max_attempts) + } + + /// Count a failed authentication (only when a maximum is set). + pub fn auth_failed(&mut self, id: &[u8; 4]) { + if let Some(a) = self.metadata.get_mut(id).and_then(|m| m.auth.as_mut()) { + if a.max_attempts != 0 && a.attempts < a.max_attempts { + a.attempts += 1; + self.persist(); + } + } + } + + /// A successful authentication resets the counter. + pub fn auth_succeeded(&mut self, id: &[u8; 4]) { + if let Some(a) = self.metadata.get_mut(id).and_then(|m| m.auth.as_mut()) { + if a.attempts != 0 { + a.attempts = 0; + self.persist(); + } + } + } + + pub fn session_owner(&self) -> [u8; 4] { + self.session_owner + } + + pub fn set_session_owner(&mut self, owner: [u8; 4]) { + self.session_owner = owner; + } + + /// The SE's ECKey session key pair, created on first use and persisted. + pub fn ensure_eckey_session_key(&mut self) { + if self.objects.contains_key(&RESERVED_ID_ECKEY_SESSION) { + return; + } + use p256::elliptic_curve::sec1::ToEncodedPoint; + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let pk = sk.public_key().to_encoded_point(false).as_bytes().to_vec(); + self.objects.insert(RESERVED_ID_ECKEY_SESSION, SecureObject::ECKeyPair { + curve: crate::object_store::types::ECCurve::NistP256, + private_key: sk.to_bytes().to_vec(), + public_key: pk, + }); self.persist(); } @@ -151,13 +222,18 @@ impl ObjectStore { self.metadata.get(id) } - /// No attached policy means the applet default allows the operation. - /// Once a policy is attached, an omitted permission is a denial. + /// No attached policy means the applet default: everything for ordinary + /// objects, read only for Authentication Objects (AN12543 table 17). + /// Once a policy is attached, only the access rule of the current session + /// owner applies (rules are not inherited across owners, AN12543 3.7.1.2); + /// an owner without a rule, or an omitted permission, is a denial. pub fn policy_allows(&self, id: &[u8; 4], permission: u32) -> bool { - match self.metadata.get(id).and_then(|m| m.policy.as_deref()) { - Some(raw) => crate::policy::ar_header_union(raw) + let meta = self.metadata.get(id); + match meta.and_then(|m| m.policy.as_deref()) { + Some(raw) => crate::policy::ar_header_for(raw, &self.session_owner) .map(|header| header & permission != 0) .unwrap_or(false), + None if meta.and_then(|m| m.auth).is_some() => permission == crate::policy::POLICY_OBJ_ALLOW_READ, None => true, } } diff --git a/SE050Sim/se050-sim/src/object_store/types.rs b/SE050Sim/se050-sim/src/object_store/types.rs index d0a0f98..414fb7c 100644 --- a/SE050Sim/se050-sim/src/object_store/types.rs +++ b/SE050Sim/se050-sim/src/object_store/types.rs @@ -32,6 +32,19 @@ pub struct ObjectMetadata { /// trust-provisioned=3. #[serde(default = "default_object_origin")] pub origin: u8, + /// Authentication Object (created with INS_AUTH_OBJECT): it can open an + /// applet session, and its failed attempts are counted (AN12543 3.2.4). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auth: Option, +} + +/// Attempt counter of an Authentication Object. With `max_attempts` != 0 the +/// object no longer authenticates once `attempts` reaches it; a successful +/// authentication resets `attempts` to 0. Persisted with the object. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct AuthState { + pub max_attempts: u16, + pub attempts: u16, } impl Default for ObjectMetadata { @@ -39,6 +52,7 @@ impl Default for ObjectMetadata { Self { policy: None, origin: default_object_origin(), + auth: None, } } } diff --git a/SE050Sim/se050-sim/src/policy.rs b/SE050Sim/se050-sim/src/policy.rs index 291d973..9ca13ed 100644 --- a/SE050Sim/se050-sim/src/policy.rs +++ b/SE050Sim/se050-sim/src/policy.rs @@ -33,6 +33,7 @@ pub const POLICY_OBJ_ALLOW_READ: u32 = 0x0020_0000; pub const POLICY_OBJ_ALLOW_WRITE: u32 = 0x0010_0000; pub const POLICY_OBJ_ALLOW_DELETE: u32 = 0x0004_0000; +pub const POLICY_OBJ_ALLOW_SIGN: u32 = 0x1000_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidPolicy; @@ -77,6 +78,24 @@ pub fn ar_header_union(value: &[u8]) -> Option { } } +/// Access rule header for one Authentication Object ID (all zeros = the +/// default session). None when the policy has no rule for that owner or is +/// malformed. +pub fn ar_header_for(value: &[u8], owner: &[u8; 4]) -> Option { + let mut rest = value; + while !rest.is_empty() { + let len = rest[0] as usize; + if len < 8 || rest.len() < 1 + len { + return None; + } + if rest[1..5] == owner[..] { + return Some(u32::from_be_bytes([rest[5], rest[6], rest[7], rest[8]])); + } + rest = &rest[1 + len..]; + } + None +} + #[cfg(test)] mod tests { use super::*; diff --git a/SE050Sim/se050-sim/src/scp03/mod.rs b/SE050Sim/se050-sim/src/scp03/mod.rs index 01544d5..6dfc124 100644 --- a/SE050Sim/se050-sim/src/scp03/mod.rs +++ b/SE050Sim/se050-sim/src/scp03/mod.rs @@ -261,6 +261,22 @@ impl Scp03State { } impl Session { + /// A session whose keys come from elsewhere: the ECKey applet session + /// derives them from its master key (AN12543 3.6.3.3) and starts with the + /// command counter at 1 and an initial MAC chaining value. + pub fn from_keys(s_enc: Vec, s_mac: Vec, s_rmac: Vec, mcv: [u8; 16], security_level: u8) -> Self { + Session { + s_enc, + s_mac, + s_rmac, + security_level, + mcv, + cmd_counter: 1, + last_cmd_len: 0, + legacy: false, + } + } + /// Unwrap a secured command: verify the C-MAC, decrypt the data field if /// C-DECRYPTION is active, and return the inner plain APDU for dispatch. /// A MAC/padding failure returns Err(SW) and the caller drops the session. diff --git a/SE050Sim/se050-sim/src/t1.rs b/SE050Sim/se050-sim/src/t1.rs index 64fb2ff..e9871da 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -166,6 +166,8 @@ pub struct T1Responder { /// PUT KEY is valid only after selecting the Security Domain, not while /// the IoT applet is selected. selected_ssd: bool, + /// Applet sessions (CreateSession / ProcessSessionCmd), reset by SELECT. + applet_sessions: crate::applet_session::AppletSessions, } impl T1Responder { @@ -179,6 +181,7 @@ impl T1Responder { apdu_reassembly: Vec::new(), scp03: Scp03State::new(), selected_ssd: false, + applet_sessions: Default::default(), } } @@ -299,6 +302,7 @@ impl T1Responder { // SELECT terminates any secure channel, then proceeds plain. if cla == 0x00 && ins == 0xA4 { self.scp03.reset(); + self.applet_sessions.reset(); self.selected_ssd = ParsedApdu::parse(apdu_bytes) .map(|a| crate::handlers::session::selects_ssd(&a)) .unwrap_or(false); @@ -343,6 +347,9 @@ impl T1Responder { let inner = match unwrapped { Ok(a) => a, Err(sw) => { + if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { + eprintln!("[trace] platform unwrap failed {:04x}, channel closed", sw); + } self.scp03.reset(); return ApduResponse::error(sw); } @@ -367,7 +374,12 @@ impl T1Responder { } } } else { - dispatch::dispatch(&inner, store, true) + let r = match self.applet_sessions.intercept(&inner, store, true) { + Some(r) => r, + None => dispatch::dispatch(&inner, store, true), + }; + crate::applet_session::trace("platform", &inner, r.sw); + r }; return match &mut self.scp03 { Scp03State::Active(sess) => sess.wrap_response(resp), @@ -391,7 +403,14 @@ impl T1Responder { } match ParsedApdu::parse(apdu_bytes) { - Ok(apdu) => dispatch::dispatch(&apdu, store, false), + Ok(apdu) => { + let r = match self.applet_sessions.intercept(&apdu, store, false) { + Some(r) => r, + None => dispatch::dispatch(&apdu, store, false), + }; + crate::applet_session::trace("plain", &apdu, r.sw); + r + } Err(e) => { log::warn!("Failed to parse APDU: {:?}", e); ApduResponse::error(0x6700) // Wrong length diff --git a/SE050Sim/se050-sim/src/tlv.rs b/SE050Sim/se050-sim/src/tlv.rs index 822108e..ce6f1b6 100644 --- a/SE050Sim/se050-sim/src/tlv.rs +++ b/SE050Sim/se050-sim/src/tlv.rs @@ -66,6 +66,12 @@ pub fn parse_tlvs(data: &[u8]) -> Result, TlvError> { } let len = ((data[offset + 2] as usize) << 8) | (data[offset + 3] as usize); (len, 4) + } else if data[offset + 1] == 0x81 { + // One extra length byte (128..255), as the NXP host encodes them. + if offset + 3 > data.len() { + return Err(TlvError::TooShort); + } + (data[offset + 2] as usize, 3) } else if data[offset + 1] < 0x80 { (data[offset + 1] as usize, 2) } else { From bcd899f79313efc69584c889f53d4ed5bab80870 Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:03:48 +0100 Subject: [PATCH 2/6] SE050Sim: T=1 retransmission on R-blocks with error; store writes off the command path - An R-block with an error indication (the host did not get the last block, e.g. it gave up waiting) now gets the last single-frame response again. Before it got no answer, so the host, and any bridge relaying frames, waited forever. - With persistence enabled, the store was rewritten synchronously on every change, several times per command; on a busy disk that took up to a second, longer than NXP's T=1 host waits before asking for a retransmission. A background thread now writes the latest snapshot (temporary file, then rename). Unit tests keep the synchronous write. Unit test for the retransmission. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/object_store/mod.rs | 38 +++++++++++++++++++++- SE050Sim/se050-sim/src/t1.rs | 29 +++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 1485d0d..85b4237 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -99,6 +99,42 @@ fn default_curves() -> HashMap { (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() } +/// Writes the persisted store without holding up the APDU being processed: +/// a background thread writes the latest snapshot (to a temporary file, then +/// renamed). A synchronous write in the command path took up to a second on a +/// busy disk, longer than hosts wait for an answer (NXP's T=1 stack then asks +/// for a retransmission). Unit tests write synchronously. +#[cfg(not(test))] +fn write_store(path: &std::path::Path, json: String) { + use std::sync::mpsc::{channel, Sender}; + use std::sync::{Mutex, OnceLock}; + static WRITER: OnceLock>> = OnceLock::new(); + let tx = WRITER.get_or_init(|| { + let (tx, rx) = channel::<(PathBuf, String)>(); + std::thread::spawn(move || { + while let Ok((mut p, mut j)) = rx.recv() { + while let Ok((p2, j2)) = rx.try_recv() { + p = p2; + j = j2; + } + let tmp = p.with_extension("json.tmp"); + if std::fs::write(&tmp, &j).is_ok() { + let _ = std::fs::rename(&tmp, &p); + } + } + }); + Mutex::new(tx) + }); + if let Ok(tx) = tx.lock() { + let _ = tx.send((path.to_path_buf(), json)); + } +} + +#[cfg(test)] +fn write_store(path: &std::path::Path, json: String) { + let _ = std::fs::write(path, json); +} + impl ObjectStore { pub fn new() -> Self { Self { @@ -363,7 +399,7 @@ impl ObjectStore { "platform_scp": self.platform_scp, }); if let Ok(json) = serde_json::to_string_pretty(&doc) { - let _ = std::fs::write(path, json); + write_store(path, json); } } diff --git a/SE050Sim/se050-sim/src/t1.rs b/SE050Sim/se050-sim/src/t1.rs index e9871da..90f5315 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -168,6 +168,8 @@ pub struct T1Responder { selected_ssd: bool, /// Applet sessions (CreateSession / ProcessSessionCmd), reset by SELECT. applet_sessions: crate::applet_session::AppletSessions, + /// Last single-frame response, for an R-block asking for a retransmission. + last_response: Vec>, } impl T1Responder { @@ -182,6 +184,7 @@ impl T1Responder { scp03: Scp03State::new(), selected_ssd: false, applet_sessions: Default::default(), + last_response: Vec::new(), } } @@ -207,6 +210,13 @@ impl T1Responder { FrameType::IFrame { seq, multi } => { self.handle_i_frame(seq, multi, &frame.payload, store) } + FrameType::RFrame { err, .. } if err != 0 => { + // R-block with an error indication: the host did not get our + // last block (e.g. it gave up waiting) and asks for it again. + // Without an answer the host and a bridge in between would + // wait forever. + self.last_response.clone() + } FrameType::RFrame { .. } => { // R-frames during multi-frame response sending // Currently we pre-queue all response frames, so we just ignore R-frames @@ -432,7 +442,9 @@ impl T1Responder { let (header, payload_crc) = build_frame(self.nad_se2hd, ft, response_bytes); chunks.push(header); chunks.push(payload_crc); + self.last_response = chunks.clone(); } else { + self.last_response.clear(); // Multi-frame response let mut offset = 0; while offset < response_bytes.len() { @@ -463,6 +475,23 @@ impl T1Responder { mod tests { use super::*; + #[test] + fn test_r_block_with_error_repeats_last_response() { + let mut t1 = T1Responder::new(0x5A); + let mut store = ObjectStore::new(); + // I-block 0 carrying SELECT of the IoT applet + let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00]; + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select); + let first = t1.process_frame(&[h, p].concat(), &mut store); + assert_eq!(first.len(), 2); + // R-block, N(R) = 1, error bits set: the same response again + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x01 }, &[]); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store), first); + // without error bits (chaining acknowledgement) nothing is sent + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x00 }, &[]); + assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty()); + } + #[test] fn test_crc16() { // From driver test: assert_eq!(0x78a1, Se050CRC::calculate(&[0,48,95,111,242])); From 0743e317200377fb07406fb3ee3ea0da0af28802 Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:03:48 +0100 Subject: [PATCH 3/6] SE050Sim: UserID and AESKey sessions, attested reads; NXP SDK test in all session modes - UserID sessions (AN12543 3.6.3.1, 4.5.2.1): VerifySessionUserID, plain session afterwards; attempts counted but not reported. WriteUserID creates UserID Authentication Objects (4-16 bytes, at most 255 attempts, not updatable). - AESKey sessions (3.6.3.2, 4.5.3): SCP03 handshake with the object's AES-128 key as static ENC/MAC/DEK and KVN 0; AES-128 Authentication Objects. - Attested reads (INS_READ | INS_ATTEST, applet 7.x layout) signed by the SE's attestation key pair 0xF0000012, as NXP's ECKey session setup verifies the SE's ECKey session public key that way. - SE050_SIM_DEMO_AUTH=1 provisions the demo Authentication Objects that Plug & Trust's examples use (0x7DA00001..3), and sdk-test/run_test.sh enables it for the session variants (and loads the Platform SCP03 keys for every *_PlatfSCP03 variant). With this, Dockerfile.sdk-test passes 32/32 with SE05X_AUTH = UserID, AESKey, ECKey, UserID_PlatfSCP03, AESKey_PlatfSCP03 and ECKey_PlatfSCP03, as with None and PlatfSCP03. Unit tests for the UserID and AESKey sessions. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/sdk-test/run_test.sh | 12 +- SE050Sim/se050-sim/src/applet_session.rs | 383 ++++++++++++++++-- SE050Sim/se050-sim/src/handlers/aes.rs | 12 + .../se050-sim/src/handlers/object_mgmt.rs | 65 ++- SE050Sim/se050-sim/src/object_store/mod.rs | 64 ++- SE050Sim/se050-sim/src/scp03/mod.rs | 8 + 6 files changed, 485 insertions(+), 59 deletions(-) diff --git a/SE050Sim/sdk-test/run_test.sh b/SE050Sim/sdk-test/run_test.sh index e50844d..875db0e 100644 --- a/SE050Sim/sdk-test/run_test.sh +++ b/SE050Sim/sdk-test/run_test.sh @@ -16,7 +16,8 @@ fi # Platform SCP03 variant: provision the simulator and the SDK from the same # key file so the two sides cannot drift. -if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then +case "$SE05X_AUTH" in *PlatfSCP03) PLATFSCP=1 ;; *) PLATFSCP=0 ;; esac +if [ "$PLATFSCP" = 1 ]; then echo "=== Platform SCP03 variant: provisioning keys from $KEYFILE ===" export SE050_SIM_SCP03_ENC=$(awk '/^ENC /{print $2}' "$KEYFILE") export SE050_SIM_SCP03_MAC=$(awk '/^MAC /{print $2}' "$KEYFILE") @@ -25,6 +26,15 @@ if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then echo " simulator MAC=$SE050_SIM_SCP03_MAC" fi +# Applet session variants: the SDK opens a UserID / AESKey / ECKey session with +# its demo Authentication Objects (ex_sss_auth.h); the simulator provisions them. +case "$SE05X_AUTH" in + UserID*|AESKey*|ECKey*) + echo "=== Applet session variant ($SE05X_AUTH): demo Authentication Objects ===" + export SE050_SIM_DEMO_AUTH=1 + ;; +esac + start_sim() { /app/se050-sim-server & SIM_PID=$! diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs index 4e1e6c0..1ad4003 100644 --- a/SE050Sim/se050-sim/src/applet_session.rs +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -20,7 +20,18 @@ */ //! Applet sessions (AN12543 3.6.2, 4.5.1): CreateSession, ProcessSessionCmd -//! and CloseSession, with ECKey authentication (AN12543 3.6.3.3, 4.5.4.1). +//! and CloseSession, with the three kinds of Authentication Object: +//! +//! * UserID (3.6.3.1, 4.5.2.1): VerifySessionUserID compares the value; the +//! session then runs without secure messaging. +//! * AESKey (3.6.3.2, 4.5.3): an SCP03 handshake (INITIALIZE UPDATE with +//! KVN 0, EXTERNAL AUTHENTICATE) with the object's AES-128 key as the static +//! ENC/MAC/DEK keys; the session is then SCP03-wrapped. +//! * ECKey (3.6.3.3, 4.5.4.1), described below. +//! +//! In every kind a failed authentication counts as a failed attempt of the +//! Authentication Object, and once the maximum is reached it no longer +//! authenticates. //! //! An ECKey session is opened against an Authentication Object holding a NIST //! P-256 public key. The host proves possession of the private key with @@ -52,7 +63,11 @@ use crate::tlv::{self, Tlv, TAG_1, TAG_SESSION_ID}; pub const P2_SESSION_CREATE: u8 = 0x1B; pub const P2_SESSION_CLOSE: u8 = 0x1C; +pub const P2_SESSION_USERID: u8 = 0x2C; +const INS_INITIALIZE_UPDATE: u8 = 0x50; +const INS_EXTERNAL_AUTHENTICATE: u8 = 0x82; const INS_AUTH_OBJECT: u8 = 0x40; +const INS_ATTEST: u8 = 0x20; const INS_INTERNAL_AUTHENTICATE: u8 = 0x88; const DDC_INITIAL_MCV: u8 = 0x08; @@ -69,16 +84,35 @@ pub fn trace(context: &str, apdu: &ParsedApdu, sw: u16) { } } +#[derive(Clone, Copy, PartialEq)] +enum Kind { + UserId, + AesKey, + EcKey, +} + +enum State { + /// Created, not authenticated yet. + Created, + /// AESKey: INITIALIZE UPDATE answered, waiting for EXTERNAL AUTHENTICATE. + AesPending(crate::scp03::Scp03State), + /// Authenticated UserID session: commands travel plain. + Plain, + /// Authenticated AESKey / ECKey session: commands are SCP03-wrapped. + Scp(ScpSession), +} + struct AppletSession { auth_id: [u8; 4], - /// None until ECKeySessionInternalAuthenticate succeeds. - scp: Option, + kind: Kind, + state: State, } /// Per-connection applet sessions. Reset by SELECT. #[derive(Default)] pub struct AppletSessions { sessions: HashMap<[u8; 8], AppletSession>, + demo_checked: bool, } impl AppletSessions { @@ -91,6 +125,12 @@ impl AppletSessions { /// dispatches in the default session. pub fn intercept(&mut self, apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> Option { store.set_session_owner([0; 4]); + if !self.demo_checked { + self.demo_checked = true; + if std::env::var("SE050_SIM_DEMO_AUTH").map(|v| v == "1").unwrap_or(false) { + store.ensure_demo_auth_objects(); + } + } if apdu.cla != 0x80 && apdu.cla != 0x84 { return None; } @@ -109,6 +149,9 @@ impl AppletSessions { } if base_ins == INS_READ { store.ensure_eckey_session_key(); + if apdu.ins & INS_ATTEST != 0 { + return Some(read_attested(apdu, store)); + } } None } @@ -117,18 +160,19 @@ impl AppletSessions { let Some(auth_id) = object_id(apdu) else { return ApduResponse::error(SW_WRONG_DATA); }; - // Only ECKey Authentication Objects (NIST P-256) are supported. - let is_eckey = matches!( - store.get(&auth_id), + let kind = match store.get(&auth_id) { + Some(SecureObject::UserID { .. }) => Kind::UserId, + Some(SecureObject::AESKey { key }) if key.len() == 16 => Kind::AesKey, Some(SecureObject::ECPublicKey { curve: ECCurve::NistP256, .. }) - | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) - ); - if store.auth_state(&auth_id).is_none() || !is_eckey { + | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) => Kind::EcKey, + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + if store.auth_state(&auth_id).is_none() { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } let mut id = [0u8; 8]; rand::rngs::OsRng.fill_bytes(&mut id); - self.sessions.insert(id, AppletSession { auth_id, scp: None }); + self.sessions.insert(id, AppletSession { auth_id, kind, state: State::Created }); ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &id)]) } @@ -150,38 +194,101 @@ impl AppletSessions { return ApduResponse::error(SW_WRONG_DATA); } - let Some(scp) = session.scp.as_mut() else { - // Not authenticated yet: only the authentication (or closing). - let Ok(cmd) = ParsedApdu::parse(&raw) else { - return ApduResponse::error(SW_WRONG_DATA); - }; - if cmd.ins == INS_INTERNAL_AUTHENTICATE { - let auth_id = session.auth_id; - let result = internal_authenticate(&cmd, auth_id, store); - trace("eckauth", &cmd, result.as_ref().map(|_| SW_NO_ERROR).unwrap_or_else(|e| *e)); - return match result { - Ok((resp, scp)) => { - session.scp = Some(scp); + let auth_id = session.auth_id; + match std::mem::replace(&mut session.state, State::Created) { + State::Created => { + let Ok(cmd) = ParsedApdu::parse(&raw) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE { + self.sessions.remove(&sid); + return ApduResponse::success(); + } + let kind = session.kind; + let (result, ctx) = match kind { + Kind::EcKey if cmd.ins == INS_INTERNAL_AUTHENTICATE => { + let r = internal_authenticate(&cmd, auth_id, store); + trace("eckauth", &cmd, r.as_ref().map(|_| SW_NO_ERROR).unwrap_or_else(|e| *e)); + (r.map(|(resp, scp)| (resp, State::Scp(scp))), "eckauth") + } + Kind::UserId if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_USERID => { + (verify_user_id(&cmd, auth_id, store).map(|resp| (resp, State::Plain)), "userid") + } + Kind::AesKey if cmd.cla == 0x80 && cmd.ins == INS_INITIALIZE_UPDATE => { + (initialize_update(&cmd, auth_id, store), "initupd") + } + _ => (Err(SW_CONDITIONS_NOT_SATISFIED), "auth"), + }; + if ctx != "eckauth" { + trace(ctx, &cmd, result.as_ref().map(|r| r.0.sw).unwrap_or_else(|e| *e)); + } + match result { + Ok((resp, state)) => { + if let Some(s) = self.sessions.get_mut(&sid) { + s.state = state; + } resp } Err(sw) => { self.sessions.remove(&sid); ApduResponse::error(sw) } - }; + } } - if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE { - self.sessions.remove(&sid); - return ApduResponse::success(); + State::AesPending(mut pending) => { + // EXTERNAL AUTHENTICATE completes the AESKey handshake. + if raw.len() < 4 || raw[0] != 0x84 || raw[1] != INS_EXTERNAL_AUTHENTICATE { + self.sessions.remove(&sid); + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let resp = pending.external_authenticate(&raw); + if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { + eprintln!("[trace] extauth 84 82 {:02x} {:02x} -> {:04x}", raw[2], raw[3], resp.sw); + } + match pending.into_session() { + Some(scp) if resp.sw == SW_NO_ERROR => { + store.auth_succeeded(&auth_id); + if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Scp(scp); + } + resp + } + _ => { + store.auth_failed(&auth_id); + self.sessions.remove(&sid); + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) + } + } } - return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); - }; + State::Plain => { + let Ok(cmd) = ParsedApdu::parse(&raw) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let closing = cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE; + let resp = if closing { + ApduResponse::success() + } else { + run_in_session(&cmd, auth_id, store, scp_active) + }; + trace("session", &cmd, resp.sw); + if closing || !store.exists(&auth_id) { + self.sessions.remove(&sid); + } else if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Plain; + } + resp + } + State::Scp(scp) => self.process_scp(sid, auth_id, scp, &raw, store, scp_active), + } + } + fn process_scp(&mut self, sid: [u8; 8], auth_id: [u8; 4], mut scp: ScpSession, raw: &[u8], store: &mut ObjectStore, + scp_active: bool) -> ApduResponse { if raw[0] & 0x04 == 0 { self.sessions.remove(&sid); return ApduResponse::error(SW_SECURITY_STATUS); } - let cmd = match scp.unwrap_command(&raw) { + let cmd = match scp.unwrap_command(raw) { Ok(c) => c, Err(sw) => { if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { @@ -197,25 +304,136 @@ impl AppletSessions { } else if cmd.base_ins() == INS_PROCESS || (cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CREATE) { ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) } else { - store.set_session_owner(session.auth_id); - if cmd.base_ins() == INS_READ { - store.ensure_eckey_session_key(); - } - let r = crate::dispatch::dispatch(&cmd, store, scp_active); - store.set_session_owner([0; 4]); - r + run_in_session(&cmd, auth_id, store, scp_active) }; trace("session", &cmd, resp.sw); let wrapped = scp.wrap_response(resp); // The session also ends when its own Authentication Object is gone // (deleted from within the session, AN12543 3.6.4). - if closing || !store.exists(&session.auth_id) { + if closing || !store.exists(&auth_id) { self.sessions.remove(&sid); + } else if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Scp(scp); } wrapped } } +/// ReadObject with attestation (applet 7.x layout, as read by Plug & Trust's +/// Se05x_API_ReadObject_W_Attst_V2): TAG_1 object ID, TAG_5 attestation key +/// ID, TAG_6 algorithm, TAG_7 random. Response: TAG_1 value (public key for +/// EC keys), TAG_2 chip ID, TAG_3 attributes, TAG_4 object size, TAG_TIMESTAMP, +/// TAG_SIGNATURE. The signature is ECDSA-SHA256 by the attestation key over +/// SHA-256(command) || the response TLVs with 3-byte lengths, as the host +/// rebuilds them (nxECKey_VerifyAttestation). Only EC objects and only the +/// SE's attestation key (ECDSA-SHA256) are supported. +fn read_attested(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 att: Option<[u8; 4]> = tlv::find_tlv(&tlvs, crate::tlv::TAG_5).and_then(|t| t.value.as_slice().try_into().ok()); + let (Some(id), Some(att)) = (id, att) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if att != crate::object_store::RESERVED_ID_ATTESTATION_KEY || !store.policy_allows(&id, crate::policy::POLICY_OBJ_ALLOW_READ) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let (value, size) = match store.get(&id) { + Some(SecureObject::ECKeyPair { curve, public_key, .. }) | Some(SecureObject::ECPublicKey { curve, public_key }) => { + (public_key.clone(), curve.scalar_len() as u16) + } + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let Some(attributes) = crate::handlers::object_mgmt::object_attributes(store, &id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let att_priv = match store.get(&att) { + Some(SecureObject::ECKeyPair { private_key, .. }) => private_key.clone(), + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let chip_id = *b"SE050SIM-CHIP-0001"; + static TS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let mut timestamp = [0u8; 12]; + timestamp[8..].copy_from_slice(&TS.fetch_add(1, std::sync::atomic::Ordering::Relaxed).to_be_bytes()); + let size = size.to_be_bytes(); + + // The command as the host hashes it: header, 00 00 Lc (Lc < 256), data. + let mut cmd = vec![apdu.cla & !0x04, apdu.ins, apdu.p1, apdu.p2, 0x00, 0x00, apdu.data.len() as u8]; + cmd.extend_from_slice(&apdu.data); + let mut signed = Sha256::digest(&cmd).to_vec(); + for (tag, v) in [ + (TAG_1, &value[..]), + (crate::tlv::TAG_2, &chip_id[..]), + (crate::tlv::TAG_3, &attributes[..]), + (crate::tlv::TAG_4, &size[..]), + (0x4F, ×tamp[..]), + ] { + signed.extend_from_slice(&[tag, 0x82, (v.len() >> 8) as u8, v.len() as u8]); + signed.extend_from_slice(v); + } + use p256::ecdsa::signature::Signer; + let Ok(sk) = p256::ecdsa::SigningKey::from_slice(&att_priv) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let sig: p256::ecdsa::Signature = sk.sign(&signed); + ApduResponse::success_with_tlvs(&[ + Tlv::new(TAG_1, &value), + Tlv::new(crate::tlv::TAG_2, &chip_id), + Tlv::new(crate::tlv::TAG_3, &attributes), + Tlv::new(crate::tlv::TAG_4, &size), + Tlv::new(0x4F, ×tamp), + Tlv::new(0x52, sig.to_der().as_bytes()), + ]) +} + +/// Dispatches a command of an authenticated session, with the session's +/// Authentication Object as the policy owner. +fn run_in_session(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore, scp_active: bool) -> ApduResponse { + store.set_session_owner(auth_id); + if cmd.base_ins() == INS_READ { + store.ensure_eckey_session_key(); + } + let r = crate::dispatch::dispatch(cmd, store, scp_active); + store.set_session_owner([0; 4]); + r +} + +/// VerifySessionUserID: TLV[TAG_1] = the UserID value. +fn verify_user_id(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore) -> Result { + if store.auth_blocked(&auth_id) { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + let tlvs = cmd.parse_tlvs().map_err(|_| SW_WRONG_DATA)?; + let given = tlv::find_tlv(&tlvs, TAG_1).ok_or(SW_WRONG_DATA)?; + let ok = matches!(store.get(&auth_id), Some(SecureObject::UserID { value }) if *value == given.value); + if !ok { + store.auth_failed(&auth_id); + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + store.auth_succeeded(&auth_id); + Ok(ApduResponse::success()) +} + +/// AESKey session, INITIALIZE UPDATE: the object's key is the static ENC, MAC +/// and DEK key; the key version is 0 (AN12543 4.5.3.1). +fn initialize_update(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore) -> Result<(ApduResponse, State), u16> { + if store.auth_blocked(&auth_id) { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + let key = match store.get(&auth_id) { + Some(SecureObject::AESKey { key }) if key.len() == 16 => key.clone(), + _ => return Err(SW_CONDITIONS_NOT_SATISFIED), + }; + let cfg = crate::scp03::keys::Scp03Config { kvn: 0x00, enc: key.clone(), mac: key.clone(), dek: key }; + let mut st = crate::scp03::Scp03State::new(); + let resp = st.initialize_update_with_config(cmd.p1, &cmd.data, crate::applet::AppletVersion::from_env(), cfg); + if resp.sw != SW_NO_ERROR { + return Err(resp.sw); + } + Ok((resp, State::AesPending(st))) +} + fn object_id(apdu: &ParsedApdu) -> Option<[u8; 4]> { let tlvs = apdu.parse_tlvs().ok()?; tlv::find_tlv(&tlvs, TAG_1)?.value.as_slice().try_into().ok() @@ -570,4 +788,95 @@ mod tests { let r = run(&mut s, &mut store, &cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &HMAC_ID))); assert_eq!(r.sw, SW_NO_ERROR); } + + fn demo_store() -> (ObjectStore, AppletSessions) { + let mut store = ObjectStore::new(); + store.ensure_demo_auth_objects(); + (store, AppletSessions::default()) + } + + fn create_for(s: &mut AppletSessions, store: &mut ObjectStore, id: [u8; 4]) -> [u8; 8] { + let r = run(s, store, &cmd(0x80, INS_MGMT, 0x00, P2_SESSION_CREATE, &tlv(TAG_1, &id))); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + tlv::find_tlv(&t, TAG_1).unwrap().value.as_slice().try_into().unwrap() + } + + #[test] + fn userid_session_plain_commands_and_counter() { + let (mut store, mut s) = demo_store(); + const UID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + // a UserID with a maximum, created over the (assumed) authenticated channel + let mut d = tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 2]); + d.extend(tlv(TAG_1, &[0x7D, 0xA0, 0x00, 0x10])); + d.extend(tlv(crate::tlv::TAG_2, b"abcd")); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_NO_ERROR); + // not updatable + assert_ne!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_NO_ERROR); + + let sid = create_for(&mut s, &mut store, UID); + let wrong = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 5])); + assert_eq!(process(&mut s, &mut store, &sid, &wrong).sw, SW_CONDITIONS_NOT_SATISFIED); + let sid = create_for(&mut s, &mut store, UID); + let right = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 4])); + assert_eq!(process(&mut s, &mut store, &sid, &right).sw, SW_NO_ERROR); + // plain command inside the session: GetVersion + let r = process(&mut s, &mut store, &sid, &[0x80, 0x04, 0x00, 0x20]); + assert_eq!(r.sw, SW_NO_ERROR); + assert!(!r.data.is_empty()); + assert_eq!(process(&mut s, &mut store, &sid, &[0x80, 0x04, 0x00, P2_SESSION_CLOSE]).sw, SW_NO_ERROR); + + // the limited UserID blocks after 2 wrong values; attempts are not reported + const U2: [u8; 4] = [0x7D, 0xA0, 0x00, 0x10]; + for _ in 0..2 { + let sid = create_for(&mut s, &mut store, U2); + let bad = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, b"abce")); + assert_eq!(process(&mut s, &mut store, &sid, &bad).sw, SW_CONDITIONS_NOT_SATISFIED); + } + let sid = create_for(&mut s, &mut store, U2); + let good = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, b"abcd")); + assert_eq!(process(&mut s, &mut store, &sid, &good).sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_READ, 0, P2_ATTRIBUTES, &tlv(TAG_1, &U2))); + let a = &crate::tlv::parse_tlvs(&r.data).unwrap()[0].value; + assert_eq!((a[5], &a[6..8], &a[12..14]), (0x02, &[0, 0][..], &[0, 2][..])); + } + + #[test] + fn aeskey_session_handshake_and_wrapped_commands() { + let (mut store, mut s) = demo_store(); + const AID_AES: [u8; 4] = [0x7D, 0xA0, 0x00, 0x02]; + let key: Vec = (0x40..=0x4F).collect(); + + // wrong key: host cryptogram does not verify -> counted (max 0 = unlimited, so no block) + for (k, expect_ok) in [(vec![0u8; 16], false), (key.clone(), true)] { + let sid = create_for(&mut s, &mut store, AID_AES); + let host_ch = [7u8; 8]; + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_INITIALIZE_UPDATE, 0x00, 0x00, &host_ch)); + assert_eq!(r.sw, SW_NO_ERROR); + assert_eq!(r.data.len(), 29); + let mut card_ch = [0u8; 8]; + card_ch.copy_from_slice(&r.data[13..21]); + let (s_enc, s_mac, s_rmac) = crypto::derive_session_keys(&k, &k, &host_ch, &card_ch).unwrap(); + let card_ok = crypto::cryptogram(&s_mac, &host_ch, &card_ch, crypto::DDC_CARD_CRYPTOGRAM).unwrap()[..] == r.data[21..29]; + assert_eq!(card_ok, expect_ok); + let host_crypto = crypto::cryptogram(&s_mac, &host_ch, &card_ch, 0x01).unwrap(); + let mut ea = vec![0x84, INS_EXTERNAL_AUTHENTICATE, 0x33, 0x00, 0x10]; + ea.extend_from_slice(&host_crypto); + let mcv = crypto::cmac_chain(&s_mac, &[0u8; 16], &ea).unwrap(); + ea.extend_from_slice(&mcv[..8]); + let r = process(&mut s, &mut store, &sid, &ea); + if !expect_ok { + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + assert_eq!(store.auth_state(&AID_AES).unwrap().attempts, 0); // unlimited: not counted + continue; + } + assert_eq!(r.sw, SW_NO_ERROR); + let mut host = Host { s_enc, s_mac, s_rmac, mcv, counter: 1 }; + let (v, sw) = host.call(&mut s, &mut store, &sid, 0x80, 0x04, 0x00, 0x20, &[]); + assert_eq!(sw, SW_NO_ERROR); + assert!(!v.is_empty()); + let (_, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_MGMT, 0, P2_SESSION_CLOSE, &[]); + assert_eq!(sw, SW_NO_ERROR); + } + } } diff --git a/SE050Sim/se050-sim/src/handlers/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index c188faf..a93d816 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -252,6 +252,18 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR creation_policy, if generated { 0x02 } else { 0x01 }, ); + // INS_AUTH_OBJECT: an AESKey Authentication Object (AES-128 only). + if apdu.ins & 0x40 != 0 { + if !matches!(store.get(&obj_id), Some(SecureObject::AESKey { key }) if key.len() == 16) { + store.remove(&obj_id); + return ApduResponse::error(SW_WRONG_DATA); + } + let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) + .filter(|t| t.value.len() == 2) + .map(|t| u16::from_be_bytes([t.value[0], t.value[1]]) & 0x7FFF) + .unwrap_or(0); + store.set_auth_object(obj_id, max); + } } response } diff --git a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs index 13d304c..ff7cd67 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -24,15 +24,53 @@ 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_POLICY}; +/// WriteUserID (AN12543 4.7.1.5): policy and maximum attempts (< 256) only on +/// creation, TAG_1 object ID, TAG_2 value of 4 to 16 bytes. An existing UserID +/// cannot be updated. +fn handle_write_userid(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Ok(tlvs) = apdu.parse_tlvs() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let Some(obj_id) = extract_object_id(&tlvs) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let Some(value) = tlv::find_tlv(&tlvs, TAG_2).map(|t| t.value.clone()) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if !(4..=16).contains(&value.len()) { + return ApduResponse::error(SW_WRONG_DATA); + } + if store.exists(&obj_id) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) + .filter(|t| t.value.len() == 2) + .map(|t| u16::from_be_bytes([t.value[0], t.value[1]])) + .unwrap_or(0); + if max > 255 { + return ApduResponse::error(SW_WRONG_DATA); + } + let policy = match crate::policy::creation_policy(&tlvs) { + Ok(p) => p, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + store.insert(obj_id, SecureObject::UserID { value }); + store.set_creation_metadata(obj_id, policy, 0x01); + store.set_auth_object(obj_id, max); + ApduResponse::success() +} + /// Handle WRITE commands for Binary, UserID, and Counter objects. pub fn handle_write(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { match apdu.cred_type() { P1_BINARY => handle_write_binary(apdu, store), // WriteUserID is refused in plain (unauthenticated) sessions: // bench-verified 0x6985 on SE050C applet 3.1.1 and SE051 - // applet 7.2.0 alike, via both the raw APDU and the sss layer. - // The simulator only models plain sessions, so the write is - // always refused. + // applet 7.2.0 alike, via both the raw APDU and the sss layer + // (the transport refuses Authentication Object writes outside an + // authenticated channel, see applet_session::intercept). UserIDs + // exist only as Authentication Objects. + P1_USERID if apdu.ins & 0x40 != 0 => handle_write_userid(apdu, store), P1_USERID => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), P1_COUNTER => handle_write_counter(apdu, store), _ => ApduResponse::error(SW_WRONG_P1P2), @@ -433,9 +471,17 @@ fn handle_read_attributes( Some(id) => id, None => return ApduResponse::error(SW_WRONG_DATA), }; - let Some(object) = store.get(&obj_id) else { - return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); - }; + match object_attributes(store, &obj_id) { + Some(attributes) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)]), + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + } +} + +/// Attribute bytes of an object (AN12543 tables 3 and 4), for +/// ReadObjectAttributes and attested reads. +pub fn object_attributes(store: &ObjectStore, obj_id: &[u8; 4]) -> Option> { + let obj_id = *obj_id; + let object = store.get(&obj_id)?; let mut attributes = Vec::new(); attributes.extend_from_slice(&obj_id); @@ -444,7 +490,9 @@ fn handle_read_attributes( // Authentication Object layout (AN12543 table 3): indicator SET, // failed attempts, owner, maximum attempts. attributes.push(0x02); // kSE05x_SetIndicator_SET - attributes.extend_from_slice(&auth.attempts.to_be_bytes()); + // UserID attempts are counted but not reported (AN12543 3.2.4.4). + let shown = if matches!(object, SecureObject::UserID { .. }) { 0 } else { auth.attempts }; + attributes.extend_from_slice(&shown.to_be_bytes()); attributes.extend_from_slice(&[0x00; 4]); attributes.extend_from_slice(&auth.max_attempts.to_be_bytes()); } else { @@ -462,8 +510,7 @@ fn handle_read_attributes( attributes.push(0x01); // legacy store: treat as externally created } attributes.extend_from_slice(&[0x00; 4]); // object version - - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)]) + Some(attributes) } fn handle_check_exists(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 85b4237..94aa43f 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -94,6 +94,10 @@ pub struct ObjectStore { /// own NIST P-256 key pair used for ECKey session key agreement. pub const RESERVED_ID_ECKEY_SESSION: [u8; 4] = [0x7F, 0xFF, 0x02, 0x01]; +/// The SE's ECC attestation key pair (SSS_SE05X_RESID_ATTESTATION_KEY in Plug +/// & Trust): signs attested reads; hosts read its public key from the SE. +pub const RESERVED_ID_ATTESTATION_KEY: [u8; 4] = [0xF0, 0x00, 0x00, 0x12]; + fn default_curves() -> HashMap { // P-192, P-224, P-256, P-384, P-521 fully parameterized. (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() @@ -238,22 +242,58 @@ impl ObjectStore { self.session_owner = owner; } - /// The SE's ECKey session key pair, created on first use and persisted. - pub fn ensure_eckey_session_key(&mut self) { - if self.objects.contains_key(&RESERVED_ID_ECKEY_SESSION) { - return; + /// SE050_SIM_DEMO_AUTH=1: the demo Authentication Objects that NXP Plug & + /// Trust's examples use (sss/ex/inc/ex_sss_auth.h, ex_sss_objid.h), so its + /// UserID, AESKey and ECKey session modes can run against the simulator: + /// 0x7DA00001 UserID C0 01 02 03 04, 0x7DA00002 AESKey 40..4F, 0x7DA00003 + /// ECKey with the public key of EX_SSS_AUTH_SE05X_KEY_HOST_ECDSA_KEY. No + /// policy, unlimited attempts. Created once, like trust provisioning. + pub fn ensure_demo_auth_objects(&mut self) { + const USERID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + const AESKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x02]; + const ECKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x03]; + const ECKEY_PUB: &str = "043c9e47edf051a3589f67302d22567c2e17229e8883338ec3b7d527f9ee71d0a81aae7fe21caa6677783aa88da6d6a8ad5ec53b10bc0b11094482f04d24b5bec4"; + if !self.objects.contains_key(&USERID) { + self.objects.insert(USERID, SecureObject::UserID { value: vec![0xC0, 0x01, 0x02, 0x03, 0x04] }); + self.metadata.insert(USERID, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); + } + if !self.objects.contains_key(&AESKEY) { + self.objects.insert(AESKEY, SecureObject::AESKey { key: (0x40..=0x4F).collect() }); + self.metadata.insert(AESKEY, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); + } + if !self.objects.contains_key(&ECKEY) { + self.objects.insert(ECKEY, SecureObject::ECPublicKey { + curve: crate::object_store::types::ECCurve::NistP256, + public_key: hex::decode(ECKEY_PUB).unwrap_or_default(), + }); + self.metadata.insert(ECKEY, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); } - use p256::elliptic_curve::sec1::ToEncodedPoint; - let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); - let pk = sk.public_key().to_encoded_point(false).as_bytes().to_vec(); - self.objects.insert(RESERVED_ID_ECKEY_SESSION, SecureObject::ECKeyPair { - curve: crate::object_store::types::ECCurve::NistP256, - private_key: sk.to_bytes().to_vec(), - public_key: pk, - }); self.persist(); } + /// The SE's own key pairs (ECKey session key agreement, attestation), + /// created on first use and persisted. + pub fn ensure_eckey_session_key(&mut self) { + let mut changed = false; + for id in [RESERVED_ID_ECKEY_SESSION, RESERVED_ID_ATTESTATION_KEY] { + if self.objects.contains_key(&id) { + continue; + } + use p256::elliptic_curve::sec1::ToEncodedPoint; + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let pk = sk.public_key().to_encoded_point(false).as_bytes().to_vec(); + self.objects.insert(id, SecureObject::ECKeyPair { + curve: crate::object_store::types::ECCurve::NistP256, + private_key: sk.to_bytes().to_vec(), + public_key: pk, + }); + changed = true; + } + if changed { + self.persist(); + } + } + pub fn metadata(&self, id: &[u8; 4]) -> Option<&ObjectMetadata> { self.metadata.get(id) } diff --git a/SE050Sim/se050-sim/src/scp03/mod.rs b/SE050Sim/se050-sim/src/scp03/mod.rs index 6dfc124..b8dfbf2 100644 --- a/SE050Sim/se050-sim/src/scp03/mod.rs +++ b/SE050Sim/se050-sim/src/scp03/mod.rs @@ -210,6 +210,14 @@ impl Scp03State { /// EXTERNAL AUTHENTICATE (CLA 0x84 INS 0x82). `raw` is the full APDU: /// 84 82 00 10 . Any failure drops /// the channel to Idle. + /// The established channel, once EXTERNAL AUTHENTICATE has succeeded. + pub fn into_session(self) -> Option { + match self { + Scp03State::Active(s) => Some(s), + _ => None, + } + } + pub fn external_authenticate(&mut self, raw: &[u8]) -> ApduResponse { // Take the pending state out; every failure path leaves us Idle. let p = match std::mem::replace(self, Scp03State::Idle) { From 6149ca88e7992cca9ff0c14283dea42083f6cad2 Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:09:49 +0100 Subject: [PATCH 4/6] SE050Sim: address review - reserved session key, crypto policies, session lifetime Fixes from review feedback on the ECKey applet sessions. The SE's ECKey session key pair (0x7FFF0201) was an ordinary object: a plain caller could run ECDH with it against a host's ephemeral key and derive that session's keys, or sign, replace or delete it. Applet-reserved objects are now read only at dispatch: reads, attributes and existence checks work (hosts read the public key during session setup), any other command naming one of them gets 0x6985. Only the applet's own session flow uses the private key. EC signing and ECDH ignored object policies, so a key-pair Authentication Object could sign the authentication challenge for a caller without the private key. They now require POLICY_OBJ_ALLOW_SIGN and POLICY_OBJ_ALLOW_KA for the current session owner, as MAC generation already did. Objects without a policy keep the applet default (everything for ordinary objects, read only for Authentication Objects). A session only noticed that its Authentication Object was gone after running its next command, and a new object created under the same ID kept old sessions alive. Authentication Objects now carry a random instance tag; a session is tied to it and is refused (and dropped) before authentication or dispatch once the object is deleted or replaced, by whichever session. A malformed TAG_MAX_ATTEMPTS (wrong length, or above 0x7FFF) silently became "unlimited". It is now refused with 0x6A80 before anything is stored; an absent tag still means unlimited. ECKey Authentication Objects are also restricted to NIST P-256, the only curve sessions accept, so a P-384 one is refused instead of being created unusable. Applet sessions are now cleared by T=1 interface soft reset and resync, not only by SELECT, as a chip reset ends them. Tests: reserved key (ECDH, ECDH into it, sign, delete, replace; public read still works; also inside a session), key-pair Authentication Object refused for sign and ECDH, sessions ending on delete and recreate of their object, Authentication Object parameter validation, and a T=1 transport test for both resets. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/applet_session.rs | 130 ++++++++++++++++++- SE050Sim/se050-sim/src/dispatch.rs | 11 ++ SE050Sim/se050-sim/src/handlers/ec.rs | 39 ++++-- SE050Sim/se050-sim/src/handlers/mac.rs | 4 +- SE050Sim/se050-sim/src/object_store/mod.rs | 13 +- SE050Sim/se050-sim/src/object_store/types.rs | 5 + SE050Sim/se050-sim/src/policy.rs | 2 + SE050Sim/se050-sim/src/t1.rs | 52 ++++++++ 8 files changed, 241 insertions(+), 15 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs index 1ad4003..0862d72 100644 --- a/SE050Sim/se050-sim/src/applet_session.rs +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -106,9 +106,13 @@ struct AppletSession { auth_id: [u8; 4], kind: Kind, state: State, + /// AuthState::instance of the Authentication Object when the session was + /// created; the session is void once the object is gone or replaced. + auth_instance: u64, } -/// Per-connection applet sessions. Reset by SELECT. +/// Per-connection applet sessions. Reset by SELECT and by a T=1 interface +/// soft reset or resync, as a chip reset ends them. #[derive(Default)] pub struct AppletSessions { sessions: HashMap<[u8; 8], AppletSession>, @@ -167,12 +171,12 @@ impl AppletSessions { | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) => Kind::EcKey, _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; - if store.auth_state(&auth_id).is_none() { + let Some(auth) = store.auth_state(&auth_id) else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); - } + }; let mut id = [0u8; 8]; rand::rngs::OsRng.fill_bytes(&mut id); - self.sessions.insert(id, AppletSession { auth_id, kind, state: State::Created }); + self.sessions.insert(id, AppletSession { auth_id, kind, state: State::Created, auth_instance: auth.instance }); ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &id)]) } @@ -190,6 +194,12 @@ impl AppletSessions { let Some(session) = self.sessions.get_mut(&sid) else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); }; + // A session ends with its Authentication Object (AN12543 3.6.4), + // whoever deleted it; a new object under the same ID does not revive it. + if store.auth_state(&session.auth_id).map(|a| a.instance) != Some(session.auth_instance) { + self.sessions.remove(&sid); + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } if raw.len() < 4 { return ApduResponse::error(SW_WRONG_DATA); } @@ -879,4 +889,116 @@ mod tests { assert_eq!(sw, SW_NO_ERROR); } } + + fn ec_auth_write(id: &[u8; 4], curve: u8, max: Option<&[u8]>, pubkey: &[u8]) -> Vec { + let mut data = Vec::new(); + if let Some(m) = max { + data.extend(tlv(crate::tlv::TAG_MAX_ATTEMPTS, m)); + } + data.extend(tlv(TAG_1, id)); + data.extend(tlv(crate::tlv::TAG_2, &[curve])); + data.extend(tlv(crate::tlv::TAG_4, pubkey)); + cmd(0x80, INS_WRITE | 0x40, P1_EC | P1_PUBLIC_KEY, 0x00, &data) + } + + #[test] + fn reserved_session_key_is_read_only() { + let good = p256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let (mut store, mut s) = setup(&good); + let r = run(&mut s, &mut store, &cmd(0x80, INS_READ, 0, 0, &tlv(TAG_1, &RESERVED_ID_ECKEY_SESSION))); + assert_eq!(r.sw, SW_NO_ERROR, "public key read"); + let peer = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key().to_encoded_point(false).as_bytes().to_vec(); + let mut ecdh = tlv(TAG_1, &RESERVED_ID_ECKEY_SESSION); + ecdh.extend(tlv(crate::tlv::TAG_2, &peer)); + let mut sign = tlv(TAG_1, &RESERVED_ID_ECKEY_SESSION); + sign.extend(tlv(crate::tlv::TAG_2, &[0x21])); + sign.extend(tlv(crate::tlv::TAG_3, &[0x11; 32])); + let mut ecdh_into = tlv(TAG_1, &HMAC_ID); + ecdh_into.extend(tlv(crate::tlv::TAG_2, &peer)); + ecdh_into.extend(tlv(crate::tlv::TAG_7, &RESERVED_ID_ECKEY_SESSION)); + for (what, raw) in [ + ("ECDH", cmd(0x80, INS_CRYPTO, P1_EC, P2_DH, &ecdh)), + ("sign", cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)), + ("ECDH into", cmd(0x80, INS_CRYPTO, P1_EC, P2_DH, &ecdh_into)), + ("delete", cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &RESERVED_ID_ECKEY_SESSION))), + ("replace", ec_auth_write(&RESERVED_ID_ECKEY_SESSION, 0x03, None, &peer)), + ] { + assert_eq!(run(&mut s, &mut store, &raw).sw, SW_CONDITIONS_NOT_SATISFIED, "{what}"); + } + // Nor inside an authenticated session. + let sid = create(&mut s, &mut store); + let mut host = authenticate(&mut s, &mut store, &sid, &good).unwrap(); + let (_, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_CRYPTO, P1_EC, P2_DH, &ecdh); + assert_eq!(sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(matches!(store.get(&RESERVED_ID_ECKEY_SESSION), Some(SecureObject::ECKeyPair { .. }))); + } + + #[test] + fn key_pair_authentication_object_does_not_sign_for_others() { + // A key-pair Authentication Object with no policy is read only + // (AN12543 table 17): it cannot sign the authentication challenge + // for a caller that lacks the private key. + let mut store = ObjectStore::new(); + let mut s = AppletSessions::default(); + let mut data = tlv(TAG_1, &AUTH_ID); + data.extend(tlv(crate::tlv::TAG_2, &[0x03])); + let r = run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_EC | P1_KEY_PAIR, 0x00, &data)); + assert_eq!(r.sw, SW_NO_ERROR); + let mut sign = tlv(TAG_1, &AUTH_ID); + sign.extend(tlv(crate::tlv::TAG_2, &[0x21])); // ECDSA SHA-256 + sign.extend(tlv(crate::tlv::TAG_3, &[0x11; 32])); + let r = run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)); + assert_eq!(r.sw, SW_COMMAND_NOT_ALLOWED); + let peer = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key().to_encoded_point(false).as_bytes().to_vec(); + let mut ecdh = tlv(TAG_1, &AUTH_ID); + ecdh.extend(tlv(crate::tlv::TAG_2, &peer)); + let r = run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_EC, P2_DH, &ecdh)); + assert_eq!(r.sw, SW_COMMAND_NOT_ALLOWED); + } + + #[test] + fn sessions_end_when_their_authentication_object_is_deleted() { + let good = p256::ecdsa::SigningKey::random(&mut rand::rngs::OsRng); + let (mut store, mut s) = setup(&good); + let pubkey = good.verifying_key().to_encoded_point(false).as_bytes().to_vec(); + let sid_a = create(&mut s, &mut store); + let mut a = authenticate(&mut s, &mut store, &sid_a, &good).unwrap(); + let sid_b = create(&mut s, &mut store); + let mut b = authenticate(&mut s, &mut store, &sid_b, &good).unwrap(); + let sid_pending = create(&mut s, &mut store); + // Session B deletes the Authentication Object (its own policy allows + // it), then the object is created again under the same ID. + let (_, sw) = b.call(&mut s, &mut store, &sid_b, 0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &AUTH_ID)); + assert_eq!(sw, SW_NO_ERROR); + let r = run(&mut s, &mut store, &ec_auth_write(&AUTH_ID, 0x03, None, &pubkey)); + assert_eq!(r.sw, SW_NO_ERROR); + // Neither session A nor the half-open one survive, though the ID exists again. + let (_, sw) = a.call(&mut s, &mut store, &sid_a, 0x80, INS_CRYPTO, P1_MAC, P2_GENERATE_ONESHOT, &hmac_cmd()); + assert_eq!(sw, SW_CONDITIONS_NOT_SATISFIED); + assert_eq!(authenticate(&mut s, &mut store, &sid_pending, &good).err(), Some(SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().attempts, 0, "a stale session costs no attempt"); + // A new session on the new object works. + let sid = create(&mut s, &mut store); + assert!(authenticate(&mut s, &mut store, &sid, &good).is_ok()); + } + + #[test] + fn authentication_object_parameters_are_validated() { + let mut store = ObjectStore::new(); + let mut s = AppletSessions::default(); + let p256_pub = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key().to_encoded_point(false).as_bytes().to_vec(); + let p384_pub = p384::SecretKey::random(&mut rand::rngs::OsRng).public_key().to_encoded_point(false).as_bytes().to_vec(); + for (what, raw) in [ + ("max attempts, one byte", ec_auth_write(&AUTH_ID, 0x03, Some(&[0x03]), &p256_pub)), + ("max attempts, three bytes", ec_auth_write(&AUTH_ID, 0x03, Some(&[0, 0, 3]), &p256_pub)), + ("max attempts above 0x7FFF", ec_auth_write(&AUTH_ID, 0x03, Some(&[0x80, 0x00]), &p256_pub)), + ("P-384", ec_auth_write(&AUTH_ID, 0x04, None, &p384_pub)), + ] { + assert_eq!(run(&mut s, &mut store, &raw).sw, SW_WRONG_DATA, "{what}"); + assert!(!store.exists(&AUTH_ID), "{what}: nothing stored"); + } + let r = run(&mut s, &mut store, &ec_auth_write(&AUTH_ID, 0x03, Some(&[0x7F, 0xFF]), &p256_pub)); + assert_eq!(r.sw, SW_NO_ERROR); + assert_eq!(store.auth_state(&AUTH_ID).unwrap().max_attempts, 0x7FFF); + } } diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 2661110..0c9d9f6 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -52,6 +52,17 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> let base_ins = apdu.base_ins(); let cred_type = apdu.cred_type(); + // Applet-reserved objects (e.g. the ECKey session key pair) are only + // readable: any other command naming one of them is refused. + let read_only = base_ins == INS_READ || (base_ins == INS_MGMT && apdu.p2 == P2_EXIST); + if !read_only { + if let Ok(tlvs) = apdu.parse_tlvs() { + if tlvs.iter().any(|t| crate::object_store::is_reserved_id(&t.value)) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + } + } + match base_ins { INS_WRITE => match cred_type { P1_EC => handlers::ec::handle_write_ec_key(apdu, store, version), diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index 0020d2c..78a33d4 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -106,6 +106,27 @@ pub fn handle_write_ec_key( return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } + // INS_AUTH_OBJECT: an ECKey Authentication Object, NIST P-256 only (the + // curve of ECKey sessions), with an optional maximum number of failed + // attempts (two bytes, 0 = unlimited, at most 0x7FFF). Checked before + // anything is stored; an existing Authentication Object keeps the curve. + let auth_object = apdu.ins & 0x40 != 0; + let max_attempts = if auth_object { + match tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) { + None => 0, + Some(t) if t.value.len() == 2 => match u16::from_be_bytes([t.value[0], t.value[1]]) { + v @ 0..=0x7FFF => v, + _ => return ApduResponse::error(SW_WRONG_DATA), + }, + Some(_) => return ApduResponse::error(SW_WRONG_DATA), + } + } else { + 0 + }; + if (auth_object || store.auth_state(&obj_id).is_some()) && !matches!(curve, ECCurve::NistP256) { + return ApduResponse::error(SW_WRONG_DATA); + } + // Check what key data is provided let private_key_data = tlv::find_tlv(&tlvs, TAG_3).map(|t| t.value.clone()); let public_key_data = tlv::find_tlv(&tlvs, TAG_4) @@ -147,14 +168,8 @@ pub fn handle_write_ec_key( creation_policy, if generated { 0x02 } else { 0x01 }, ); - // INS_AUTH_OBJECT: an ECKey Authentication Object, with its maximum - // number of failed attempts (0 = unlimited, at most 0x7FFF). - if apdu.ins & 0x40 != 0 { - let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) - .filter(|t| t.value.len() == 2) - .map(|t| u16::from_be_bytes([t.value[0], t.value[1]]) & 0x7FFF) - .unwrap_or(0); - store.set_auth_object(obj_id, max); + if auth_object { + store.set_auth_object(obj_id, max_attempts); } } response @@ -511,6 +526,10 @@ pub fn handle_sign(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { // on 3.1.1 and 7.2.0), not 0x6A82. None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + // Signing needs POLICY_OBJ_ALLOW_SIGN for the current session owner. + if !store.policy_allows(&key_id, crate::policy::POLICY_OBJ_ALLOW_SIGN) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } // Weierstrass ECDSA enforces input length == algo digest length. if let SecureObject::ECKeyPair { curve, .. } = &key_obj { @@ -759,6 +778,10 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> Some(obj) => obj.clone(), None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + // Key agreement needs POLICY_OBJ_ALLOW_KA for the current session owner. + if !store.policy_allows(&key_id, crate::policy::POLICY_OBJ_ALLOW_KA) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } // 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 diff --git a/SE050Sim/se050-sim/src/handlers/mac.rs b/SE050Sim/se050-sim/src/handlers/mac.rs index fd63107..f0e779a 100644 --- a/SE050Sim/se050-sim/src/handlers/mac.rs +++ b/SE050Sim/se050-sim/src/handlers/mac.rs @@ -152,7 +152,7 @@ pub fn handle_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool }; // Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session // owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table). - let needed = if validate { 0x0800_0000 } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; + let needed = if validate { crate::policy::POLICY_OBJ_ALLOW_VERIFY } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; if !store.policy_allows(&key_id, needed) { return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); } @@ -268,7 +268,7 @@ pub fn handle_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse }; // Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session // owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table). - let needed = if validate { 0x0800_0000 } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; + let needed = if validate { crate::policy::POLICY_OBJ_ALLOW_VERIFY } else { crate::policy::POLICY_OBJ_ALLOW_SIGN }; if !store.policy_allows(&key_id, needed) { return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); } diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 94aa43f..d548d24 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -98,6 +98,16 @@ pub const RESERVED_ID_ECKEY_SESSION: [u8; 4] = [0x7F, 0xFF, 0x02, 0x01]; /// & Trust): signs attested reads; hosts read its public key from the SE. pub const RESERVED_ID_ATTESTATION_KEY: [u8; 4] = [0xF0, 0x00, 0x00, 0x12]; +/// Applet-reserved objects the simulator manages itself. Hosts may read them +/// (public part, attributes, existence) but not create, replace, delete or +/// use them in generic crypto commands; only the applet's own flows use the +/// private keys. +pub const RESERVED_IDS: &[[u8; 4]] = &[RESERVED_ID_ECKEY_SESSION, RESERVED_ID_ATTESTATION_KEY]; + +pub fn is_reserved_id(id: &[u8]) -> bool { + RESERVED_IDS.iter().any(|r| r[..] == *id) +} + fn default_curves() -> HashMap { // P-192, P-224, P-256, P-384, P-521 fully parameterized. (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() @@ -200,7 +210,8 @@ impl ObjectStore { /// Mark a freshly created object as an Authentication Object. pub fn set_auth_object(&mut self, id: [u8; 4], max_attempts: u16) { if let Some(m) = self.metadata.get_mut(&id) { - m.auth = Some(crate::object_store::types::AuthState { max_attempts, attempts: 0 }); + let instance = rand::RngCore::next_u64(&mut rand::rngs::OsRng); + m.auth = Some(crate::object_store::types::AuthState { max_attempts, attempts: 0, instance }); self.persist(); } } diff --git a/SE050Sim/se050-sim/src/object_store/types.rs b/SE050Sim/se050-sim/src/object_store/types.rs index 414fb7c..baf1336 100644 --- a/SE050Sim/se050-sim/src/object_store/types.rs +++ b/SE050Sim/se050-sim/src/object_store/types.rs @@ -45,6 +45,11 @@ pub struct ObjectMetadata { pub struct AuthState { pub max_attempts: u16, pub attempts: u16, + /// Random tag given when the object is created. A session remembers it, + /// so a session outlives neither its Authentication Object nor a new + /// object later created under the same ID. + #[serde(default)] + pub instance: u64, } impl Default for ObjectMetadata { diff --git a/SE050Sim/se050-sim/src/policy.rs b/SE050Sim/se050-sim/src/policy.rs index 9ca13ed..c1a8e4e 100644 --- a/SE050Sim/se050-sim/src/policy.rs +++ b/SE050Sim/se050-sim/src/policy.rs @@ -34,6 +34,8 @@ pub const POLICY_OBJ_ALLOW_READ: u32 = 0x0020_0000; pub const POLICY_OBJ_ALLOW_WRITE: u32 = 0x0010_0000; pub const POLICY_OBJ_ALLOW_DELETE: u32 = 0x0004_0000; pub const POLICY_OBJ_ALLOW_SIGN: u32 = 0x1000_0000; +pub const POLICY_OBJ_ALLOW_VERIFY: u32 = 0x0800_0000; +pub const POLICY_OBJ_ALLOW_KA: u32 = 0x0400_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidPolicy; diff --git a/SE050Sim/se050-sim/src/t1.rs b/SE050Sim/se050-sim/src/t1.rs index 90f5315..f638deb 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -236,6 +236,7 @@ impl T1Responder { self.apdu_reassembly.clear(); self.scp03.reset(); self.selected_ssd = false; + self.applet_sessions.reset(); let ft = FrameType::SFrame { code: T1_S_INTERFACE_SOFT_RESET, @@ -251,6 +252,7 @@ impl T1Responder { self.apdu_reassembly.clear(); self.scp03.reset(); self.selected_ssd = false; + self.applet_sessions.reset(); let ft = FrameType::SFrame { code: 0x00, is_response: true }; let (header, payload_crc) = build_frame(self.nad_se2hd, ft, &[]); @@ -475,6 +477,56 @@ impl T1Responder { mod tests { use super::*; + /// Sends one APDU in an I-block (host sequence `seq`) and returns the + /// status word of the single-frame response. + fn apdu_sw(t1: &mut T1Responder, store: &mut ObjectStore, seq: u8, apdu: &[u8]) -> u16 { + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq, multi: false }, apdu); + let out = t1.process_frame(&[h, p].concat(), store); + let payload = &out[1]; + let body = &payload[..payload.len() - 2]; + u16::from_be_bytes([body[body.len() - 2], body[body.len() - 1]]) + } + + #[test] + fn test_reset_and_resync_end_applet_sessions() { + use crate::object_store::types::{ECCurve, SecureObject}; + use p256::elliptic_curve::sec1::ToEncodedPoint; + let auth_id = [0x7D, 0xA0, 0x00, 0x01]; + let pk = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key(); + // None = control (no reset), else the S-block request code. + for reset in [None, Some(T1_S_INTERFACE_SOFT_RESET), Some(0x00)] { + let mut store = ObjectStore::new(); + store.insert(auth_id, SecureObject::ECPublicKey { + curve: ECCurve::NistP256, + public_key: pk.to_encoded_point(false).as_bytes().to_vec(), + }); + store.set_creation_metadata(auth_id, None, 0x01); + store.set_auth_object(auth_id, 0); + let mut t1 = T1Responder::new(0x5A); + // CreateSession (plain): the response carries the session ID. + let mut create = vec![0x80, 0x04, 0x00, 0x1B, 0x06, 0x41, 0x04]; + create.extend_from_slice(&auth_id); + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &create); + let out = t1.process_frame(&[h, p].concat(), &mut store); + let body = &out[1][..out[1].len() - 2]; + assert_eq!(&body[body.len() - 2..], &[0x90, 0x00]); + let sid = body[2..10].to_vec(); + let mut seq = 1; + if let Some(code) = reset { + let (h, p) = build_frame(0x5A, FrameType::SFrame { code, is_response: false }, &[]); + t1.process_frame(&[h, p].concat(), &mut store); + seq = 0; + } + // CloseSession for that session ID, inside ProcessSessionCmd. + let mut process = vec![0x80, 0x05, 0x00, 0x00, 0x10, 0x10, 0x08]; + process.extend_from_slice(&sid); + process.extend_from_slice(&[0x41, 0x04, 0x80, 0x04, 0x00, 0x1C]); + let sw = apdu_sw(&mut t1, &mut store, seq, &process); + let expected = if reset.is_some() { 0x6985 } else { 0x9000 }; + assert_eq!(sw, expected, "reset {reset:?}"); + } + } + #[test] fn test_r_block_with_error_repeats_last_response() { let mut t1 = T1Responder::new(0x5A); From 2dd21eb381a004e1b827735756a9e389b7789b1b Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:06:29 +0100 Subject: [PATCH 5/6] SE050Sim: address review - store writer lifecycle and permissions, T=1 reset Fixes from review feedback on the T=1 retransmission and background store writes. The background writer kept only the last queued snapshot, whatever its destination, so two persistent stores in one process could lose each other's updates. Snapshots are now coalesced per path. Writes returned before reaching the disk and nothing waited for them, so a store dropped and reopened on the same file could load stale state: tests/scp03.rs set_platform_scp_request_flow failed on the reload of scp_required. There is now a flush barrier per path: loading a store file waits for its pending write, and dropping a persistent store waits for its own. APDU handling still never waits for the disk. The replacement file was created with the umask, so a store chmodded to 0600 became world-readable on its next update, and the temporary file was readable while being written. The store holds private keys and SCP03 keys: the temporary file is now created with mode 0600 and takes the mode of the file it replaces, if any. The cached response for an error R-block survived InterfaceSoftReset and resync, so a host could get a response from the previous link replayed. Both resets now clear it. Tests: per-path coalescing with two stores, reopening a store while the first is still open, file modes (new file 0600, existing mode kept), and an error R-block after each reset. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/object_store/mod.rs | 195 ++++++++++++++++++--- SE050Sim/se050-sim/src/t1.rs | 20 +++ 2 files changed, 188 insertions(+), 27 deletions(-) diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index d548d24..e50298a 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -114,39 +114,125 @@ fn default_curves() -> HashMap { } /// Writes the persisted store without holding up the APDU being processed: -/// a background thread writes the latest snapshot (to a temporary file, then -/// renamed). A synchronous write in the command path took up to a second on a -/// busy disk, longer than hosts wait for an answer (NXP's T=1 stack then asks -/// for a retransmission). Unit tests write synchronously. -#[cfg(not(test))] -fn write_store(path: &std::path::Path, json: String) { - use std::sync::mpsc::{channel, Sender}; - use std::sync::{Mutex, OnceLock}; - static WRITER: OnceLock>> = OnceLock::new(); - let tx = WRITER.get_or_init(|| { - let (tx, rx) = channel::<(PathBuf, String)>(); - std::thread::spawn(move || { - while let Ok((mut p, mut j)) = rx.recv() { - while let Ok((p2, j2)) = rx.try_recv() { - p = p2; - j = j2; +/// a background thread writes the latest snapshot of each store file. A +/// synchronous write in the command path took up to a second on a busy disk, +/// longer than hosts wait for an answer (NXP's T=1 stack then asks for a +/// retransmission). +/// +/// - Snapshots are coalesced per destination path, so several stores in one +/// process never discard each other's updates. +/// - `flush_store` waits until a path has no queued or in-flight write; it runs +/// before a store file is loaded and when a persistent store is dropped, so +/// reopening a store never reads stale state. +/// - The file holds private keys and SCP03 keys: it is written to a temporary +/// file created with mode 0600 (or the existing file's mode), then renamed. +mod writer { + use std::collections::{HashMap, HashSet}; + use std::path::{Path, PathBuf}; + use std::sync::{Condvar, Mutex, OnceLock}; + + #[derive(Default)] + struct State { + pending: HashMap, + in_flight: HashSet, + } + + struct Writer { + state: Mutex, + cv: Condvar, + } + + static WRITER: OnceLock<&'static Writer> = OnceLock::new(); + + fn writer() -> &'static Writer { + WRITER.get_or_init(|| { + let w: &'static Writer = Box::leak(Box::new(Writer { + state: Mutex::new(State::default()), + cv: Condvar::new(), + })); + std::thread::spawn(move || run(w)); + w + }) + } + + fn run(w: &'static Writer) { + loop { + let batch: Vec<(PathBuf, String)> = { + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + while st.pending.is_empty() { + st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner()); } - let tmp = p.with_extension("json.tmp"); - if std::fs::write(&tmp, &j).is_ok() { - let _ = std::fs::rename(&tmp, &p); + let batch: Vec<_> = st.pending.drain().collect(); + for (p, _) in &batch { + st.in_flight.insert(p.clone()); } + batch + }; + for (path, json) in &batch { + write_file(path, json); } - }); - Mutex::new(tx) - }); - if let Ok(tx) = tx.lock() { - let _ = tx.send((path.to_path_buf(), json)); + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + for (p, _) in &batch { + st.in_flight.remove(p); + } + w.cv.notify_all(); + } + } + + fn write_file(path: &Path, json: &str) { + use std::io::Write; + let mut tmp_name = path.as_os_str().to_owned(); + tmp_name.push(".tmp"); + let tmp = PathBuf::from(tmp_name); + let _ = std::fs::remove_file(&tmp); + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let Ok(mut f) = opts.open(&tmp) else { return }; + // Keep the access restrictions of the file being replaced. + if let Ok(meta) = std::fs::metadata(path) { + let _ = f.set_permissions(meta.permissions()); + } + if f.write_all(json.as_bytes()).is_ok() && f.flush().is_ok() { + drop(f); + let _ = std::fs::rename(&tmp, path); + } else { + drop(f); + let _ = std::fs::remove_file(&tmp); + } + } + + pub fn write_store(path: &Path, json: String) { + let w = writer(); + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + st.pending.insert(path.to_path_buf(), json); + w.cv.notify_all(); + } + + /// Blocks until `path` has no queued or in-flight write. + pub fn flush_store(path: &Path) { + let Some(w) = WRITER.get() else { return }; + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + while st.pending.contains_key(path) || st.in_flight.contains(path) { + st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner()); + } } } -#[cfg(test)] -fn write_store(path: &std::path::Path, json: String) { - let _ = std::fs::write(path, json); +use writer::{flush_store, write_store}; + +/// A persistent store finishes writing its file before it goes away, so a +/// store reopened on the same file (or an orderly shutdown) sees every update. +impl Drop for ObjectStore { + fn drop(&mut self) { + if let Some(path) = &self.persist_path { + flush_store(path); + } + } } impl ObjectStore { @@ -456,6 +542,7 @@ impl ObjectStore { fn load(&mut self) { let Some(path) = &self.persist_path else { return }; + flush_store(path); let Ok(json) = std::fs::read_to_string(path) else { return }; let Ok(value): Result = serde_json::from_str(&json) else { return; @@ -630,4 +717,58 @@ mod persistence_tests { assert_eq!(loaded.dek, config.dek); let _ = std::fs::remove_file(&path); } + + #[test] + fn test_stores_on_different_paths_keep_their_latest_snapshot() { + let a = unique_store_path("writer_path_a"); + let b = unique_store_path("writer_path_b"); + { + let mut sa = ObjectStore::with_persistence(a.clone()); + let mut sb = ObjectStore::with_persistence(b.clone()); + for i in 0..50u8 { + sa.insert([0, 0, 0xA0, i], SecureObject::Binary { data: vec![i] }); + sb.insert([0, 0, 0xB0, i], SecureObject::Binary { data: vec![i] }); + } + } + let sa = ObjectStore::with_persistence(a.clone()); + let sb = ObjectStore::with_persistence(b.clone()); + for i in 0..50u8 { + assert!(sa.exists(&[0, 0, 0xA0, i]) && !sa.exists(&[0, 0, 0xB0, i])); + assert!(sb.exists(&[0, 0, 0xB0, i]) && !sb.exists(&[0, 0, 0xA0, i])); + } + let _ = std::fs::remove_file(&a); + let _ = std::fs::remove_file(&b); + } + + #[test] + fn test_reopen_sees_updates_of_a_store_still_open() { + let path = unique_store_path("writer_reopen"); + let mut first = ObjectStore::with_persistence(path.clone()); + first.set_scp_required(true); + let second = ObjectStore::with_persistence(path.clone()); + assert!(second.scp_required()); + drop(first); + let _ = std::fs::remove_file(&path); + } + + #[cfg(unix)] + #[test] + fn test_store_file_is_private_and_keeps_its_mode() { + use std::os::unix::fs::PermissionsExt; + let path = unique_store_path("writer_mode"); + let mode = |p: &PathBuf| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + { + let mut store = ObjectStore::with_persistence(path.clone()); + store.insert([0, 0, 0, 1], SecureObject::Binary { data: vec![1] }); + } + assert_eq!(mode(&path), 0o600); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).unwrap(); + { + let mut store = ObjectStore::with_persistence(path.clone()); + store.insert([0, 0, 0, 2], SecureObject::Binary { data: vec![2] }); + } + assert_eq!(mode(&path), 0o640); + assert!(ObjectStore::with_persistence(path.clone()).exists(&[0, 0, 0, 2])); + let _ = std::fs::remove_file(&path); + } } diff --git a/SE050Sim/se050-sim/src/t1.rs b/SE050Sim/se050-sim/src/t1.rs index f638deb..edc37f7 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -237,6 +237,8 @@ impl T1Responder { self.scp03.reset(); self.selected_ssd = false; self.applet_sessions.reset(); + // Nothing is pending on a fresh link. + self.last_response.clear(); let ft = FrameType::SFrame { code: T1_S_INTERFACE_SOFT_RESET, @@ -253,6 +255,8 @@ impl T1Responder { self.scp03.reset(); self.selected_ssd = false; self.applet_sessions.reset(); + // Nothing is pending on a fresh link. + self.last_response.clear(); let ft = FrameType::SFrame { code: 0x00, is_response: true }; let (header, payload_crc) = build_frame(self.nad_se2hd, ft, &[]); @@ -544,6 +548,22 @@ mod tests { assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty()); } + #[test] + fn test_reset_and_resync_forget_last_response() { + let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00]; + for code in [T1_S_INTERFACE_SOFT_RESET, 0x00] { + let mut t1 = T1Responder::new(0x5A); + let mut store = ObjectStore::new(); + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2); + let (h, p) = build_frame(0x5A, FrameType::SFrame { code, is_response: false }, &[]); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2); + // An error R-block after the reset has no previous response to replay. + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 0, err: 0x01 }, &[]); + assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty(), "S-block {code:#04x}"); + } + } + #[test] fn test_crc16() { // From driver test: assert_eq!(0x78a1, Se050CRC::calculate(&[0,48,95,111,242])); From 922a22d795b576f1873bca42cb2581ca471af65d Mon Sep 17 00:00:00 2001 From: Guybrush <48859004+mcamposv@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:16:19 +0100 Subject: [PATCH 6/6] SE050Sim: address review - AES handshake blocking, cipher policies, attested reads in sessions Fixes from review feedback specific to the UserID / AESKey sessions (the ones shared with the ECKey sessions and the store writer are in the two previous commits, also applied to #23 and #24). The attempt limit was checked at INITIALIZE UPDATE only. With a maximum of one, a host could start two AESKey handshakes, fail the first to block the object, then complete the second, which also reset the counter. A pending handshake now rechecks the block at EXTERNAL AUTHENTICATE and is refused without touching the counter. AES encryption and decryption ignored object policies, so a new AESKey Authentication Object without a policy (read only by default) still encrypted and decrypted in the default session. One-shot and CipherInit now require POLICY_OBJ_ALLOW_ENC / POLICY_OBJ_ALLOW_DEC for the current session owner; update and final use the key bound at init. The SE's attestation key pair (0xF0000012) is now applet-reserved like the ECKey session key: readable, but not usable for generic signing, nor replaceable or deletable. An attested ReadObject inside an authenticated session went to plain dispatch, which masks INS_ATTEST, and came back without attestation fields. It now goes through the attestation handler, with the session's Authentication Object as the policy owner. Updating an existing AESKey Authentication Object with a 24 or 32 byte key succeeded and left it unusable for sessions. Authentication Objects stay AES-128: the length is checked before anything is stored, on creation and on update. TAG_MAX_ATTEMPTS is parsed by one helper for all three types: a malformed or out-of-range value is refused with 0x6A80 instead of meaning "unlimited" (UserID: at most 255). Tests: two pending AESKey handshakes against a one-attempt object, cipher policies (one-shot and init, both directions), AES-128 on update, malformed maximum for AESKey and UserID, attested read in a UserID session, and the reserved attestation key. The NXP Plug & Trust SDK test passes 32/32 in all seven session modes. Co-Authored-By: Claude Opus 5.5 --- SE050Sim/se050-sim/src/applet_session.rs | 140 +++++++++++++++++- SE050Sim/se050-sim/src/handlers/aes.rs | 59 +++++--- SE050Sim/se050-sim/src/handlers/ec.rs | 16 +- .../se050-sim/src/handlers/object_mgmt.rs | 8 +- SE050Sim/se050-sim/src/policy.rs | 2 + SE050Sim/se050-sim/src/tlv.rs | 14 ++ 6 files changed, 200 insertions(+), 39 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs index 0862d72..ee2a1f1 100644 --- a/SE050Sim/se050-sim/src/applet_session.rs +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -251,6 +251,12 @@ impl AppletSessions { self.sessions.remove(&sid); return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } + // The object may have been blocked by another handshake since + // INITIALIZE UPDATE: a pending one neither passes nor unblocks it. + if store.auth_blocked(&auth_id) { + self.sessions.remove(&sid); + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } let resp = pending.external_authenticate(&raw); if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { eprintln!("[trace] extauth 84 82 {:02x} {:02x} -> {:04x}", raw[2], raw[3], resp.sw); @@ -401,10 +407,16 @@ fn read_attested(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { /// Authentication Object as the policy owner. fn run_in_session(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore, scp_active: bool) -> ApduResponse { store.set_session_owner(auth_id); - if cmd.base_ins() == INS_READ { + let r = if cmd.base_ins() == INS_READ { store.ensure_eckey_session_key(); - } - let r = crate::dispatch::dispatch(cmd, store, scp_active); + if cmd.ins & INS_ATTEST != 0 { + read_attested(cmd, store) + } else { + crate::dispatch::dispatch(cmd, store, scp_active) + } + } else { + crate::dispatch::dispatch(cmd, store, scp_active) + }; store.set_session_owner([0; 4]); r } @@ -1001,4 +1013,126 @@ mod tests { assert_eq!(r.sw, SW_NO_ERROR); assert_eq!(store.auth_state(&AUTH_ID).unwrap().max_attempts, 0x7FFF); } + + /// AESKey handshake up to (not including) EXTERNAL AUTHENTICATE: the + /// command authenticating with `key`. + fn aes_init(s: &mut AppletSessions, store: &mut ObjectStore, sid: &[u8; 8], key: &[u8]) -> Vec { + let host_ch = [7u8; 8]; + let r = process(s, store, sid, &cmd(0x80, INS_INITIALIZE_UPDATE, 0x00, 0x00, &host_ch)); + assert_eq!(r.sw, SW_NO_ERROR); + let mut card_ch = [0u8; 8]; + card_ch.copy_from_slice(&r.data[13..21]); + let (_, s_mac, _) = crypto::derive_session_keys(key, key, &host_ch, &card_ch).unwrap(); + let host_crypto = crypto::cryptogram(&s_mac, &host_ch, &card_ch, 0x01).unwrap(); + let mut ea = vec![0x84, INS_EXTERNAL_AUTHENTICATE, 0x33, 0x00, 0x10]; + ea.extend_from_slice(&host_crypto); + let mcv = crypto::cmac_chain(&s_mac, &[0u8; 16], &ea).unwrap(); + ea.extend_from_slice(&mcv[..8]); + ea + } + + fn aes_auth_write(id: &[u8; 4], key: &[u8], extra: &[u8]) -> Vec { + let mut d = extra.to_vec(); + d.extend(tlv(TAG_1, id)); + d.extend(tlv(crate::tlv::TAG_3, key)); + cmd(0x80, INS_WRITE | 0x40, P1_AES, 0x00, &d) + } + + #[test] + fn pending_aes_handshake_neither_passes_nor_clears_a_block() { + let (mut store, mut s) = demo_store(); + const AES1: [u8; 4] = [0x7D, 0xA0, 0x00, 0x20]; + let key = [0x33u8; 16]; + let r = run(&mut s, &mut store, &aes_auth_write(&AES1, &key, &tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 1]))); + assert_eq!(r.sw, SW_NO_ERROR); + let sid1 = create_for(&mut s, &mut store, AES1); + let sid2 = create_for(&mut s, &mut store, AES1); + let bad = aes_init(&mut s, &mut store, &sid1, &[0u8; 16]); + let good = aes_init(&mut s, &mut store, &sid2, &key); + assert_eq!(process(&mut s, &mut store, &sid1, &bad).sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.auth_blocked(&AES1)); + assert_eq!(process(&mut s, &mut store, &sid2, &good).sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.auth_blocked(&AES1)); + assert_eq!(store.auth_state(&AES1).unwrap().attempts, 1); + } + + #[test] + fn aes_authentication_object_policies_and_key_length() { + let (mut store, mut s) = demo_store(); + const AES1: [u8; 4] = [0x7D, 0xA0, 0x00, 0x20]; + const AES2: [u8; 4] = [0x7D, 0xA0, 0x00, 0x21]; + // No policy: read only, so it neither encrypts nor decrypts in the + // default session, one-shot or multi-part. + assert_eq!(run(&mut s, &mut store, &aes_auth_write(&AES1, &[0x33; 16], &[])).sw, SW_NO_ERROR); + for p2 in [P2_ENCRYPT_ONESHOT, P2_DECRYPT_ONESHOT] { + let mut d = tlv(TAG_1, &AES1); + d.extend(tlv(crate::tlv::TAG_2, &[0x0D])); // AES ECB + d.extend(tlv(crate::tlv::TAG_3, &[0u8; 16])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_CIPHER, p2, &d)).sw, SW_COMMAND_NOT_ALLOWED); + } + let mut co = tlv(TAG_1, &[0x00, 0x10]); + co.extend(tlv(crate::tlv::TAG_2, &[0x02])); + co.extend(tlv(crate::tlv::TAG_3, &[0x0D])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_CRYPTO_OBJ, 0, &co)).sw, SW_NO_ERROR); + for p2 in [P2_ENCRYPT_INIT, P2_DECRYPT_INIT] { + let mut d = tlv(TAG_1, &AES1); + d.extend(tlv(crate::tlv::TAG_2, &[0x00, 0x10])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_CIPHER, p2, &d)).sw, SW_COMMAND_NOT_ALLOWED); + } + // Writable (policy for the default session): updates must stay AES-128. + let mut pol = vec![8]; + pol.extend_from_slice(&[0; 4]); + pol.extend_from_slice(&(crate::policy::POLICY_OBJ_ALLOW_WRITE | crate::policy::POLICY_OBJ_ALLOW_READ).to_be_bytes()); + let pol = tlv(crate::tlv::TAG_POLICY, &pol); + assert_eq!(run(&mut s, &mut store, &aes_auth_write(&AES2, &[0x44; 16], &pol)).sw, SW_NO_ERROR); + let mut upd = tlv(TAG_1, &AES2); + upd.extend(tlv(crate::tlv::TAG_3, &[0x55; 32])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_AES, 0, &upd)).sw, SW_WRONG_DATA); + assert!(matches!(store.get(&AES2), Some(SecureObject::AESKey { key }) if key == &[0x44; 16])); + let mut upd = tlv(TAG_1, &AES2); + upd.extend(tlv(crate::tlv::TAG_3, &[0x55; 16])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_AES, 0, &upd)).sw, SW_NO_ERROR); + assert!(store.auth_state(&AES2).is_some()); + // Malformed maximum attempts, AESKey and UserID. + const AES3: [u8; 4] = [0x7D, 0xA0, 0x00, 0x22]; + let r = run(&mut s, &mut store, &aes_auth_write(&AES3, &[0x33; 16], &tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[1]))); + assert_eq!(r.sw, SW_WRONG_DATA); + assert!(!store.exists(&AES3)); + let mut d = tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 0, 1]); + d.extend(tlv(TAG_1, &AES3)); + d.extend(tlv(crate::tlv::TAG_2, b"abcd")); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_WRONG_DATA); + assert!(!store.exists(&AES3)); + } + + #[test] + fn attested_read_inside_a_session_and_reserved_attestation_key() { + let (mut store, mut s) = demo_store(); + const UID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + const ECKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x03]; + let att = crate::object_store::RESERVED_ID_ATTESTATION_KEY; + let sid = create_for(&mut s, &mut store, UID); + let right = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 4])); + assert_eq!(process(&mut s, &mut store, &sid, &right).sw, SW_NO_ERROR); + let mut d = tlv(TAG_1, &ECKEY); + d.extend(tlv(crate::tlv::TAG_5, &att)); + d.extend(tlv(crate::tlv::TAG_6, &[0x21])); + d.extend(tlv(crate::tlv::TAG_7, &[0x5A; 16])); + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_READ | INS_ATTEST, 0, 0, &d)); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + let Some(SecureObject::ECPublicKey { public_key, .. }) = store.get(&ECKEY) else { panic!() }; + assert_eq!(&tlv::find_tlv(&t, TAG_1).unwrap().value, public_key); + assert!(tlv::find_tlv(&t, 0x52).is_some(), "signature"); + // The attestation key itself only reads: no generic signing with it. + let mut sign = tlv(TAG_1, &att); + sign.extend(tlv(crate::tlv::TAG_2, &[0x21])); + sign.extend(tlv(crate::tlv::TAG_3, &[0x11; 32])); + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &att))); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + } } diff --git a/SE050Sim/se050-sim/src/handlers/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index a93d816..a8ab812 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -216,7 +216,7 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR // Check if this is key generation (P2=Generate) or has a key size tag let generated = apdu.p2 == P2_GENERATE || key_data.as_ref().is_some_and(|d| d.len() <= 2); - let response = if generated { + let key = if generated { // Key generation: Tag3 contains 2-byte key size let key_len = key_data .as_ref() @@ -234,38 +234,42 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR let mut key = vec![0u8; key_len_bytes]; rand::thread_rng().fill_bytes(&mut key); - store.insert(obj_id, SecureObject::AESKey { key }); - ApduResponse::success() + key } else if let Some(key) = key_data { // Import key data if key.len() != 16 && key.len() != 24 && key.len() != 32 { return ApduResponse::error(SW_WRONG_DATA); } - store.insert(obj_id, SecureObject::AESKey { key }); - ApduResponse::success() + key } else { - ApduResponse::error(SW_WRONG_DATA) + return ApduResponse::error(SW_WRONG_DATA); + }; + + // INS_AUTH_OBJECT: an AESKey Authentication Object, AES-128 only, with an + // optional maximum number of failed attempts. Checked before anything is + // stored; an existing Authentication Object stays AES-128 on update. + let auth_object = apdu.ins & 0x40 != 0; + let max_attempts = match crate::tlv::max_attempts(&tlvs, 0x7FFF) { + Ok(v) if auth_object => v, + Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA), + _ => 0, }; - if response.sw == SW_NO_ERROR && !object_existed { + if (auth_object || store.auth_state(&obj_id).is_some()) && key.len() != 16 { + return ApduResponse::error(SW_WRONG_DATA); + } + + store.insert(obj_id, SecureObject::AESKey { key }); + if !object_existed { store.set_creation_metadata( obj_id, creation_policy, if generated { 0x02 } else { 0x01 }, ); - // INS_AUTH_OBJECT: an AESKey Authentication Object (AES-128 only). - if apdu.ins & 0x40 != 0 { - if !matches!(store.get(&obj_id), Some(SecureObject::AESKey { key }) if key.len() == 16) { - store.remove(&obj_id); - return ApduResponse::error(SW_WRONG_DATA); - } - let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) - .filter(|t| t.value.len() == 2) - .map(|t| u16::from_be_bytes([t.value[0], t.value[1]]) & 0x7FFF) - .unwrap_or(0); - store.set_auth_object(obj_id, max); + if auth_object { + store.set_auth_object(obj_id, max_attempts); } } - response + ApduResponse::success() } /// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC). @@ -322,6 +326,16 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu } } +/// Encrypting needs POLICY_OBJ_ALLOW_ENC, decrypting POLICY_OBJ_ALLOW_DEC, +/// for the current session owner. +fn cipher_permission(encrypting: bool) -> u32 { + if encrypting { + crate::policy::POLICY_OBJ_ALLOW_ENC + } else { + crate::policy::POLICY_OBJ_ALLOW_DEC + } +} + /// Shared body of the encrypt/decrypt one-shot handlers. /// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=input, Tag4=IV(opt) fn handle_cipher_oneshot( @@ -366,6 +380,9 @@ fn handle_cipher_oneshot( Some(obj) => obj.clone(), None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + if !store.policy_allows(&key_id, cipher_permission(encrypting)) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } let key_data = match &key_obj { SecureObject::AESKey { key } => key, @@ -428,6 +445,10 @@ pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes if !store.exists(&key_id) { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } + // The key is bound to the crypto object here; update and final reuse it. + if !store.policy_allows(&key_id, cipher_permission(encrypting)) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } let mut chain = vec![0u8; 16]; if let Some(t) = tlv::find_tlv(&tlvs, TAG_4) { diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index 78a33d4..9307ce0 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -111,17 +111,11 @@ pub fn handle_write_ec_key( // attempts (two bytes, 0 = unlimited, at most 0x7FFF). Checked before // anything is stored; an existing Authentication Object keeps the curve. let auth_object = apdu.ins & 0x40 != 0; - let max_attempts = if auth_object { - match tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) { - None => 0, - Some(t) if t.value.len() == 2 => match u16::from_be_bytes([t.value[0], t.value[1]]) { - v @ 0..=0x7FFF => v, - _ => return ApduResponse::error(SW_WRONG_DATA), - }, - Some(_) => return ApduResponse::error(SW_WRONG_DATA), - } - } else { - 0 + let max_attempts = match crate::tlv::max_attempts(&tlvs, 0x7FFF) { + Ok(v) if auth_object => v, + Ok(_) => 0, + Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA), + Err(()) => 0, }; if (auth_object || store.auth_state(&obj_id).is_some()) && !matches!(curve, ECCurve::NistP256) { return ApduResponse::error(SW_WRONG_DATA); diff --git a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs index ff7cd67..2b91b76 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -43,13 +43,9 @@ fn handle_write_userid(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespon if store.exists(&obj_id) { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } - let max = tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) - .filter(|t| t.value.len() == 2) - .map(|t| u16::from_be_bytes([t.value[0], t.value[1]])) - .unwrap_or(0); - if max > 255 { + let Ok(max) = crate::tlv::max_attempts(&tlvs, 255) else { return ApduResponse::error(SW_WRONG_DATA); - } + }; let policy = match crate::policy::creation_policy(&tlvs) { Ok(p) => p, Err(_) => return ApduResponse::error(SW_WRONG_DATA), diff --git a/SE050Sim/se050-sim/src/policy.rs b/SE050Sim/se050-sim/src/policy.rs index c1a8e4e..4260601 100644 --- a/SE050Sim/se050-sim/src/policy.rs +++ b/SE050Sim/se050-sim/src/policy.rs @@ -36,6 +36,8 @@ pub const POLICY_OBJ_ALLOW_DELETE: u32 = 0x0004_0000; pub const POLICY_OBJ_ALLOW_SIGN: u32 = 0x1000_0000; pub const POLICY_OBJ_ALLOW_VERIFY: u32 = 0x0800_0000; pub const POLICY_OBJ_ALLOW_KA: u32 = 0x0400_0000; +pub const POLICY_OBJ_ALLOW_ENC: u32 = 0x0200_0000; +pub const POLICY_OBJ_ALLOW_DEC: u32 = 0x0100_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidPolicy; diff --git a/SE050Sim/se050-sim/src/tlv.rs b/SE050Sim/se050-sim/src/tlv.rs index ce6f1b6..579fa66 100644 --- a/SE050Sim/se050-sim/src/tlv.rs +++ b/SE050Sim/se050-sim/src/tlv.rs @@ -118,6 +118,20 @@ pub enum TlvError { pub const TAG_SESSION_ID: u8 = 0x10; pub const TAG_POLICY: u8 = 0x11; pub const TAG_MAX_ATTEMPTS: u8 = 0x12; + +/// Maximum number of failed attempts of an Authentication Object: two bytes, +/// 0 (= unlimited) to `limit`; 0 when the tag is absent. Err when the tag is +/// present but malformed or out of range. +pub fn max_attempts(tlvs: &[Tlv], limit: u16) -> Result { + match find_tlv(tlvs, TAG_MAX_ATTEMPTS) { + None => Ok(0), + Some(t) if t.value.len() == 2 => { + let v = u16::from_be_bytes([t.value[0], t.value[1]]); + if v <= limit { Ok(v) } else { Err(()) } + } + Some(_) => Err(()), + } +} pub const TAG_1: u8 = 0x41; pub const TAG_2: u8 = 0x42; pub const TAG_3: u8 = 0x43;