diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs new file mode 100644 index 0000000..5d03aee --- /dev/null +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -0,0 +1,695 @@ +/* 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], + /// 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 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>, +} + +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, .. }) + ); + 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, auth_instance: auth.instance, 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); + }; + // 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); + } + + 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); + } + + 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 bff0a7d..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,6 +168,9 @@ pub fn handle_write_ec_key( creation_policy, if generated { 0x02 } else { 0x01 }, ); + if auth_object { + store.set_auth_object(obj_id, max_attempts); + } } response } @@ -502,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 { @@ -750,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 ecc4000..f0e779a 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 { 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); + } 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 { 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); + } 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..29b91f9 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -84,6 +84,24 @@ 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]; + +/// 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 { @@ -102,6 +120,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], } } @@ -115,6 +134,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], }; store.load(); store @@ -143,7 +163,69 @@ 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) { + 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(); + } + } + + 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 +233,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..baf1336 100644 --- a/SE050Sim/se050-sim/src/object_store/types.rs +++ b/SE050Sim/se050-sim/src/object_store/types.rs @@ -32,6 +32,24 @@ 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, + /// 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 { @@ -39,6 +57,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..c1a8e4e 100644 --- a/SE050Sim/se050-sim/src/policy.rs +++ b/SE050Sim/se050-sim/src/policy.rs @@ -33,6 +33,9 @@ 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; @@ -77,6 +80,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..dd3f214 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(), } } @@ -223,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, @@ -238,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, &[]); @@ -299,6 +304,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 +349,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 +376,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 +405,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 @@ -444,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])); 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 {