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/2] 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 6c2bd02a7287e4c9cf0f2cd91fd050b2e96d018d 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 2/2] 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 4e1e6c0..5d03aee 100644 --- a/SE050Sim/se050-sim/src/applet_session.rs +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -71,11 +71,15 @@ pub fn trace(context: &str, apdu: &ParsedApdu, sw: u16) { struct AppletSession { auth_id: [u8; 4], + /// 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, /// None until ECKeySessionInternalAuthenticate succeeds. scp: Option, } -/// 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>, @@ -123,12 +127,12 @@ impl AppletSessions { Some(SecureObject::ECPublicKey { curve: ECCurve::NistP256, .. }) | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) ); - if store.auth_state(&auth_id).is_none() || !is_eckey { + let Some(auth) = store.auth_state(&auth_id).filter(|_| is_eckey) 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, scp: None }); + self.sessions.insert(id, AppletSession { auth_id, auth_instance: auth.instance, scp: None }); ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &id)]) } @@ -146,6 +150,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); } @@ -570,4 +580,116 @@ 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 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 1485d0d..29b91f9 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -94,6 +94,16 @@ 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]; +/// 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]; + +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() @@ -160,7 +170,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 e9871da..dd3f214 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -226,6 +226,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, @@ -241,6 +242,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, &[]); @@ -463,6 +465,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_crc16() { // From driver test: assert_eq!(0x78a1, Se050CRC::calculate(&[0,48,95,111,242]));