diff --git a/SE050Sim/sdk-test/run_test.sh b/SE050Sim/sdk-test/run_test.sh index e50844d..875db0e 100644 --- a/SE050Sim/sdk-test/run_test.sh +++ b/SE050Sim/sdk-test/run_test.sh @@ -16,7 +16,8 @@ fi # Platform SCP03 variant: provision the simulator and the SDK from the same # key file so the two sides cannot drift. -if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then +case "$SE05X_AUTH" in *PlatfSCP03) PLATFSCP=1 ;; *) PLATFSCP=0 ;; esac +if [ "$PLATFSCP" = 1 ]; then echo "=== Platform SCP03 variant: provisioning keys from $KEYFILE ===" export SE050_SIM_SCP03_ENC=$(awk '/^ENC /{print $2}' "$KEYFILE") export SE050_SIM_SCP03_MAC=$(awk '/^MAC /{print $2}' "$KEYFILE") @@ -25,6 +26,15 @@ if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then echo " simulator MAC=$SE050_SIM_SCP03_MAC" fi +# Applet session variants: the SDK opens a UserID / AESKey / ECKey session with +# its demo Authentication Objects (ex_sss_auth.h); the simulator provisions them. +case "$SE05X_AUTH" in + UserID*|AESKey*|ECKey*) + echo "=== Applet session variant ($SE05X_AUTH): demo Authentication Objects ===" + export SE050_SIM_DEMO_AUTH=1 + ;; +esac + start_sim() { /app/se050-sim-server & SIM_PID=$! diff --git a/SE050Sim/se050-sim/src/applet_session.rs b/SE050Sim/se050-sim/src/applet_session.rs new file mode 100644 index 0000000..ee2a1f1 --- /dev/null +++ b/SE050Sim/se050-sim/src/applet_session.rs @@ -0,0 +1,1138 @@ +/* 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 the three kinds of Authentication Object: +//! +//! * UserID (3.6.3.1, 4.5.2.1): VerifySessionUserID compares the value; the +//! session then runs without secure messaging. +//! * AESKey (3.6.3.2, 4.5.3): an SCP03 handshake (INITIALIZE UPDATE with +//! KVN 0, EXTERNAL AUTHENTICATE) with the object's AES-128 key as the static +//! ENC/MAC/DEK keys; the session is then SCP03-wrapped. +//! * ECKey (3.6.3.3, 4.5.4.1), described below. +//! +//! In every kind a failed authentication counts as a failed attempt of the +//! Authentication Object, and once the maximum is reached it no longer +//! authenticates. +//! +//! An ECKey session is opened against an Authentication Object holding a NIST +//! P-256 public key. The host proves possession of the private key with +//! 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; +pub const P2_SESSION_USERID: u8 = 0x2C; +const INS_INITIALIZE_UPDATE: u8 = 0x50; +const INS_EXTERNAL_AUTHENTICATE: u8 = 0x82; +const INS_AUTH_OBJECT: u8 = 0x40; +const INS_ATTEST: u8 = 0x20; +const INS_INTERNAL_AUTHENTICATE: u8 = 0x88; +const DDC_INITIAL_MCV: u8 = 0x08; + +/// 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 + ); + } +} + +#[derive(Clone, Copy, PartialEq)] +enum Kind { + UserId, + AesKey, + EcKey, +} + +enum State { + /// Created, not authenticated yet. + Created, + /// AESKey: INITIALIZE UPDATE answered, waiting for EXTERNAL AUTHENTICATE. + AesPending(crate::scp03::Scp03State), + /// Authenticated UserID session: commands travel plain. + Plain, + /// Authenticated AESKey / ECKey session: commands are SCP03-wrapped. + Scp(ScpSession), +} + +struct AppletSession { + auth_id: [u8; 4], + kind: Kind, + state: State, + /// AuthState::instance of the Authentication Object when the session was + /// created; the session is void once the object is gone or replaced. + auth_instance: u64, +} + +/// Per-connection applet sessions. Reset by SELECT 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>, + demo_checked: bool, +} + +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 !self.demo_checked { + self.demo_checked = true; + if std::env::var("SE050_SIM_DEMO_AUTH").map(|v| v == "1").unwrap_or(false) { + store.ensure_demo_auth_objects(); + } + } + if apdu.cla != 0x80 && apdu.cla != 0x84 { + return None; + } + 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(); + if apdu.ins & INS_ATTEST != 0 { + return Some(read_attested(apdu, store)); + } + } + 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); + }; + let kind = match store.get(&auth_id) { + Some(SecureObject::UserID { .. }) => Kind::UserId, + Some(SecureObject::AESKey { key }) if key.len() == 16 => Kind::AesKey, + Some(SecureObject::ECPublicKey { curve: ECCurve::NistP256, .. }) + | Some(SecureObject::ECKeyPair { curve: ECCurve::NistP256, .. }) => Kind::EcKey, + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let Some(auth) = store.auth_state(&auth_id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let mut id = [0u8; 8]; + rand::rngs::OsRng.fill_bytes(&mut id); + self.sessions.insert(id, AppletSession { auth_id, kind, state: State::Created, auth_instance: auth.instance }); + 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 auth_id = session.auth_id; + match std::mem::replace(&mut session.state, State::Created) { + State::Created => { + let Ok(cmd) = ParsedApdu::parse(&raw) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE { + self.sessions.remove(&sid); + return ApduResponse::success(); + } + let kind = session.kind; + let (result, ctx) = match kind { + Kind::EcKey if cmd.ins == INS_INTERNAL_AUTHENTICATE => { + let r = internal_authenticate(&cmd, auth_id, store); + trace("eckauth", &cmd, r.as_ref().map(|_| SW_NO_ERROR).unwrap_or_else(|e| *e)); + (r.map(|(resp, scp)| (resp, State::Scp(scp))), "eckauth") + } + Kind::UserId if cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_USERID => { + (verify_user_id(&cmd, auth_id, store).map(|resp| (resp, State::Plain)), "userid") + } + Kind::AesKey if cmd.cla == 0x80 && cmd.ins == INS_INITIALIZE_UPDATE => { + (initialize_update(&cmd, auth_id, store), "initupd") + } + _ => (Err(SW_CONDITIONS_NOT_SATISFIED), "auth"), + }; + if ctx != "eckauth" { + trace(ctx, &cmd, result.as_ref().map(|r| r.0.sw).unwrap_or_else(|e| *e)); + } + match result { + Ok((resp, state)) => { + if let Some(s) = self.sessions.get_mut(&sid) { + s.state = state; + } + resp + } + Err(sw) => { + self.sessions.remove(&sid); + ApduResponse::error(sw) + } + } + } + State::AesPending(mut pending) => { + // EXTERNAL AUTHENTICATE completes the AESKey handshake. + if raw.len() < 4 || raw[0] != 0x84 || raw[1] != INS_EXTERNAL_AUTHENTICATE { + self.sessions.remove(&sid); + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + // The object may have been blocked by another handshake since + // INITIALIZE UPDATE: a pending one neither passes nor unblocks it. + if store.auth_blocked(&auth_id) { + self.sessions.remove(&sid); + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let resp = pending.external_authenticate(&raw); + if std::env::var("SE050_SIM_TRACE").map(|v| v == "1").unwrap_or(false) { + eprintln!("[trace] extauth 84 82 {:02x} {:02x} -> {:04x}", raw[2], raw[3], resp.sw); + } + match pending.into_session() { + Some(scp) if resp.sw == SW_NO_ERROR => { + store.auth_succeeded(&auth_id); + if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Scp(scp); + } + resp + } + _ => { + store.auth_failed(&auth_id); + self.sessions.remove(&sid); + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) + } + } + } + State::Plain => { + let Ok(cmd) = ParsedApdu::parse(&raw) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let closing = cmd.base_ins() == INS_MGMT && cmd.p2 == P2_SESSION_CLOSE; + let resp = if closing { + ApduResponse::success() + } else { + run_in_session(&cmd, auth_id, store, scp_active) + }; + trace("session", &cmd, resp.sw); + if closing || !store.exists(&auth_id) { + self.sessions.remove(&sid); + } else if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Plain; + } + resp + } + State::Scp(scp) => self.process_scp(sid, auth_id, scp, &raw, store, scp_active), + } + } + + fn process_scp(&mut self, sid: [u8; 8], auth_id: [u8; 4], mut scp: ScpSession, raw: &[u8], store: &mut ObjectStore, + scp_active: bool) -> ApduResponse { + if raw[0] & 0x04 == 0 { + self.sessions.remove(&sid); + return ApduResponse::error(SW_SECURITY_STATUS); + } + let cmd = match scp.unwrap_command(raw) { + 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 { + run_in_session(&cmd, auth_id, store, scp_active) + }; + trace("session", &cmd, resp.sw); + let wrapped = scp.wrap_response(resp); + // The session also ends when its own Authentication Object is gone + // (deleted from within the session, AN12543 3.6.4). + if closing || !store.exists(&auth_id) { + self.sessions.remove(&sid); + } else if let Some(s) = self.sessions.get_mut(&sid) { + s.state = State::Scp(scp); + } + wrapped + } +} + +/// ReadObject with attestation (applet 7.x layout, as read by Plug & Trust's +/// Se05x_API_ReadObject_W_Attst_V2): TAG_1 object ID, TAG_5 attestation key +/// ID, TAG_6 algorithm, TAG_7 random. Response: TAG_1 value (public key for +/// EC keys), TAG_2 chip ID, TAG_3 attributes, TAG_4 object size, TAG_TIMESTAMP, +/// TAG_SIGNATURE. The signature is ECDSA-SHA256 by the attestation key over +/// SHA-256(command) || the response TLVs with 3-byte lengths, as the host +/// rebuilds them (nxECKey_VerifyAttestation). Only EC objects and only the +/// SE's attestation key (ECDSA-SHA256) are supported. +fn read_attested(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Ok(tlvs) = apdu.parse_tlvs() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let id: Option<[u8; 4]> = tlv::find_tlv(&tlvs, TAG_1).and_then(|t| t.value.as_slice().try_into().ok()); + let att: Option<[u8; 4]> = tlv::find_tlv(&tlvs, crate::tlv::TAG_5).and_then(|t| t.value.as_slice().try_into().ok()); + let (Some(id), Some(att)) = (id, att) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if att != crate::object_store::RESERVED_ID_ATTESTATION_KEY || !store.policy_allows(&id, crate::policy::POLICY_OBJ_ALLOW_READ) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let (value, size) = match store.get(&id) { + Some(SecureObject::ECKeyPair { curve, public_key, .. }) | Some(SecureObject::ECPublicKey { curve, public_key }) => { + (public_key.clone(), curve.scalar_len() as u16) + } + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let Some(attributes) = crate::handlers::object_mgmt::object_attributes(store, &id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let att_priv = match store.get(&att) { + Some(SecureObject::ECKeyPair { private_key, .. }) => private_key.clone(), + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let chip_id = *b"SE050SIM-CHIP-0001"; + static TS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); + let mut timestamp = [0u8; 12]; + timestamp[8..].copy_from_slice(&TS.fetch_add(1, std::sync::atomic::Ordering::Relaxed).to_be_bytes()); + let size = size.to_be_bytes(); + + // The command as the host hashes it: header, 00 00 Lc (Lc < 256), data. + let mut cmd = vec![apdu.cla & !0x04, apdu.ins, apdu.p1, apdu.p2, 0x00, 0x00, apdu.data.len() as u8]; + cmd.extend_from_slice(&apdu.data); + let mut signed = Sha256::digest(&cmd).to_vec(); + for (tag, v) in [ + (TAG_1, &value[..]), + (crate::tlv::TAG_2, &chip_id[..]), + (crate::tlv::TAG_3, &attributes[..]), + (crate::tlv::TAG_4, &size[..]), + (0x4F, ×tamp[..]), + ] { + signed.extend_from_slice(&[tag, 0x82, (v.len() >> 8) as u8, v.len() as u8]); + signed.extend_from_slice(v); + } + use p256::ecdsa::signature::Signer; + let Ok(sk) = p256::ecdsa::SigningKey::from_slice(&att_priv) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let sig: p256::ecdsa::Signature = sk.sign(&signed); + ApduResponse::success_with_tlvs(&[ + Tlv::new(TAG_1, &value), + Tlv::new(crate::tlv::TAG_2, &chip_id), + Tlv::new(crate::tlv::TAG_3, &attributes), + Tlv::new(crate::tlv::TAG_4, &size), + Tlv::new(0x4F, ×tamp), + Tlv::new(0x52, sig.to_der().as_bytes()), + ]) +} + +/// Dispatches a command of an authenticated session, with the session's +/// Authentication Object as the policy owner. +fn run_in_session(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore, scp_active: bool) -> ApduResponse { + store.set_session_owner(auth_id); + let r = if cmd.base_ins() == INS_READ { + store.ensure_eckey_session_key(); + if cmd.ins & INS_ATTEST != 0 { + read_attested(cmd, store) + } else { + crate::dispatch::dispatch(cmd, store, scp_active) + } + } else { + crate::dispatch::dispatch(cmd, store, scp_active) + }; + store.set_session_owner([0; 4]); + r +} + +/// VerifySessionUserID: TLV[TAG_1] = the UserID value. +fn verify_user_id(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore) -> Result { + if store.auth_blocked(&auth_id) { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + let tlvs = cmd.parse_tlvs().map_err(|_| SW_WRONG_DATA)?; + let given = tlv::find_tlv(&tlvs, TAG_1).ok_or(SW_WRONG_DATA)?; + let ok = matches!(store.get(&auth_id), Some(SecureObject::UserID { value }) if *value == given.value); + if !ok { + store.auth_failed(&auth_id); + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + store.auth_succeeded(&auth_id); + Ok(ApduResponse::success()) +} + +/// AESKey session, INITIALIZE UPDATE: the object's key is the static ENC, MAC +/// and DEK key; the key version is 0 (AN12543 4.5.3.1). +fn initialize_update(cmd: &ParsedApdu, auth_id: [u8; 4], store: &mut ObjectStore) -> Result<(ApduResponse, State), u16> { + if store.auth_blocked(&auth_id) { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + let key = match store.get(&auth_id) { + Some(SecureObject::AESKey { key }) if key.len() == 16 => key.clone(), + _ => return Err(SW_CONDITIONS_NOT_SATISFIED), + }; + let cfg = crate::scp03::keys::Scp03Config { kvn: 0x00, enc: key.clone(), mac: key.clone(), dek: key }; + let mut st = crate::scp03::Scp03State::new(); + let resp = st.initialize_update_with_config(cmd.p1, &cmd.data, crate::applet::AppletVersion::from_env(), cfg); + if resp.sw != SW_NO_ERROR { + return Err(resp.sw); + } + Ok((resp, State::AesPending(st))) +} + +fn object_id(apdu: &ParsedApdu) -> Option<[u8; 4]> { + let tlvs = apdu.parse_tlvs().ok()?; + tlv::find_tlv(&tlvs, TAG_1)?.value.as_slice().try_into().ok() +} + +/// 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 demo_store() -> (ObjectStore, AppletSessions) { + let mut store = ObjectStore::new(); + store.ensure_demo_auth_objects(); + (store, AppletSessions::default()) + } + + fn create_for(s: &mut AppletSessions, store: &mut ObjectStore, id: [u8; 4]) -> [u8; 8] { + let r = run(s, store, &cmd(0x80, INS_MGMT, 0x00, P2_SESSION_CREATE, &tlv(TAG_1, &id))); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + tlv::find_tlv(&t, TAG_1).unwrap().value.as_slice().try_into().unwrap() + } + + #[test] + fn userid_session_plain_commands_and_counter() { + let (mut store, mut s) = demo_store(); + const UID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + // a UserID with a maximum, created over the (assumed) authenticated channel + let mut d = tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 2]); + d.extend(tlv(TAG_1, &[0x7D, 0xA0, 0x00, 0x10])); + d.extend(tlv(crate::tlv::TAG_2, b"abcd")); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_NO_ERROR); + // not updatable + assert_ne!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_NO_ERROR); + + let sid = create_for(&mut s, &mut store, UID); + let wrong = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 5])); + assert_eq!(process(&mut s, &mut store, &sid, &wrong).sw, SW_CONDITIONS_NOT_SATISFIED); + let sid = create_for(&mut s, &mut store, UID); + let right = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 4])); + assert_eq!(process(&mut s, &mut store, &sid, &right).sw, SW_NO_ERROR); + // plain command inside the session: GetVersion + let r = process(&mut s, &mut store, &sid, &[0x80, 0x04, 0x00, 0x20]); + assert_eq!(r.sw, SW_NO_ERROR); + assert!(!r.data.is_empty()); + assert_eq!(process(&mut s, &mut store, &sid, &[0x80, 0x04, 0x00, P2_SESSION_CLOSE]).sw, SW_NO_ERROR); + + // the limited UserID blocks after 2 wrong values; attempts are not reported + const U2: [u8; 4] = [0x7D, 0xA0, 0x00, 0x10]; + for _ in 0..2 { + let sid = create_for(&mut s, &mut store, U2); + let bad = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, b"abce")); + assert_eq!(process(&mut s, &mut store, &sid, &bad).sw, SW_CONDITIONS_NOT_SATISFIED); + } + let sid = create_for(&mut s, &mut store, U2); + let good = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, b"abcd")); + assert_eq!(process(&mut s, &mut store, &sid, &good).sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_READ, 0, P2_ATTRIBUTES, &tlv(TAG_1, &U2))); + let a = &crate::tlv::parse_tlvs(&r.data).unwrap()[0].value; + assert_eq!((a[5], &a[6..8], &a[12..14]), (0x02, &[0, 0][..], &[0, 2][..])); + } + + #[test] + fn aeskey_session_handshake_and_wrapped_commands() { + let (mut store, mut s) = demo_store(); + const AID_AES: [u8; 4] = [0x7D, 0xA0, 0x00, 0x02]; + let key: Vec = (0x40..=0x4F).collect(); + + // wrong key: host cryptogram does not verify -> counted (max 0 = unlimited, so no block) + for (k, expect_ok) in [(vec![0u8; 16], false), (key.clone(), true)] { + let sid = create_for(&mut s, &mut store, AID_AES); + let host_ch = [7u8; 8]; + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_INITIALIZE_UPDATE, 0x00, 0x00, &host_ch)); + assert_eq!(r.sw, SW_NO_ERROR); + assert_eq!(r.data.len(), 29); + let mut card_ch = [0u8; 8]; + card_ch.copy_from_slice(&r.data[13..21]); + let (s_enc, s_mac, s_rmac) = crypto::derive_session_keys(&k, &k, &host_ch, &card_ch).unwrap(); + let card_ok = crypto::cryptogram(&s_mac, &host_ch, &card_ch, crypto::DDC_CARD_CRYPTOGRAM).unwrap()[..] == r.data[21..29]; + assert_eq!(card_ok, expect_ok); + let host_crypto = crypto::cryptogram(&s_mac, &host_ch, &card_ch, 0x01).unwrap(); + let mut ea = vec![0x84, INS_EXTERNAL_AUTHENTICATE, 0x33, 0x00, 0x10]; + ea.extend_from_slice(&host_crypto); + let mcv = crypto::cmac_chain(&s_mac, &[0u8; 16], &ea).unwrap(); + ea.extend_from_slice(&mcv[..8]); + let r = process(&mut s, &mut store, &sid, &ea); + if !expect_ok { + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + assert_eq!(store.auth_state(&AID_AES).unwrap().attempts, 0); // unlimited: not counted + continue; + } + assert_eq!(r.sw, SW_NO_ERROR); + let mut host = Host { s_enc, s_mac, s_rmac, mcv, counter: 1 }; + let (v, sw) = host.call(&mut s, &mut store, &sid, 0x80, 0x04, 0x00, 0x20, &[]); + assert_eq!(sw, SW_NO_ERROR); + assert!(!v.is_empty()); + let (_, sw) = host.call(&mut s, &mut store, &sid, 0x80, INS_MGMT, 0, P2_SESSION_CLOSE, &[]); + assert_eq!(sw, SW_NO_ERROR); + } + } + + 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); + } + + /// AESKey handshake up to (not including) EXTERNAL AUTHENTICATE: the + /// command authenticating with `key`. + fn aes_init(s: &mut AppletSessions, store: &mut ObjectStore, sid: &[u8; 8], key: &[u8]) -> Vec { + let host_ch = [7u8; 8]; + let r = process(s, store, sid, &cmd(0x80, INS_INITIALIZE_UPDATE, 0x00, 0x00, &host_ch)); + assert_eq!(r.sw, SW_NO_ERROR); + let mut card_ch = [0u8; 8]; + card_ch.copy_from_slice(&r.data[13..21]); + let (_, s_mac, _) = crypto::derive_session_keys(key, key, &host_ch, &card_ch).unwrap(); + let host_crypto = crypto::cryptogram(&s_mac, &host_ch, &card_ch, 0x01).unwrap(); + let mut ea = vec![0x84, INS_EXTERNAL_AUTHENTICATE, 0x33, 0x00, 0x10]; + ea.extend_from_slice(&host_crypto); + let mcv = crypto::cmac_chain(&s_mac, &[0u8; 16], &ea).unwrap(); + ea.extend_from_slice(&mcv[..8]); + ea + } + + fn aes_auth_write(id: &[u8; 4], key: &[u8], extra: &[u8]) -> Vec { + let mut d = extra.to_vec(); + d.extend(tlv(TAG_1, id)); + d.extend(tlv(crate::tlv::TAG_3, key)); + cmd(0x80, INS_WRITE | 0x40, P1_AES, 0x00, &d) + } + + #[test] + fn pending_aes_handshake_neither_passes_nor_clears_a_block() { + let (mut store, mut s) = demo_store(); + const AES1: [u8; 4] = [0x7D, 0xA0, 0x00, 0x20]; + let key = [0x33u8; 16]; + let r = run(&mut s, &mut store, &aes_auth_write(&AES1, &key, &tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 1]))); + assert_eq!(r.sw, SW_NO_ERROR); + let sid1 = create_for(&mut s, &mut store, AES1); + let sid2 = create_for(&mut s, &mut store, AES1); + let bad = aes_init(&mut s, &mut store, &sid1, &[0u8; 16]); + let good = aes_init(&mut s, &mut store, &sid2, &key); + assert_eq!(process(&mut s, &mut store, &sid1, &bad).sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.auth_blocked(&AES1)); + assert_eq!(process(&mut s, &mut store, &sid2, &good).sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.auth_blocked(&AES1)); + assert_eq!(store.auth_state(&AES1).unwrap().attempts, 1); + } + + #[test] + fn aes_authentication_object_policies_and_key_length() { + let (mut store, mut s) = demo_store(); + const AES1: [u8; 4] = [0x7D, 0xA0, 0x00, 0x20]; + const AES2: [u8; 4] = [0x7D, 0xA0, 0x00, 0x21]; + // No policy: read only, so it neither encrypts nor decrypts in the + // default session, one-shot or multi-part. + assert_eq!(run(&mut s, &mut store, &aes_auth_write(&AES1, &[0x33; 16], &[])).sw, SW_NO_ERROR); + for p2 in [P2_ENCRYPT_ONESHOT, P2_DECRYPT_ONESHOT] { + let mut d = tlv(TAG_1, &AES1); + d.extend(tlv(crate::tlv::TAG_2, &[0x0D])); // AES ECB + d.extend(tlv(crate::tlv::TAG_3, &[0u8; 16])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_CIPHER, p2, &d)).sw, SW_COMMAND_NOT_ALLOWED); + } + let mut co = tlv(TAG_1, &[0x00, 0x10]); + co.extend(tlv(crate::tlv::TAG_2, &[0x02])); + co.extend(tlv(crate::tlv::TAG_3, &[0x0D])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_CRYPTO_OBJ, 0, &co)).sw, SW_NO_ERROR); + for p2 in [P2_ENCRYPT_INIT, P2_DECRYPT_INIT] { + let mut d = tlv(TAG_1, &AES1); + d.extend(tlv(crate::tlv::TAG_2, &[0x00, 0x10])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_CIPHER, p2, &d)).sw, SW_COMMAND_NOT_ALLOWED); + } + // Writable (policy for the default session): updates must stay AES-128. + let mut pol = vec![8]; + pol.extend_from_slice(&[0; 4]); + pol.extend_from_slice(&(crate::policy::POLICY_OBJ_ALLOW_WRITE | crate::policy::POLICY_OBJ_ALLOW_READ).to_be_bytes()); + let pol = tlv(crate::tlv::TAG_POLICY, &pol); + assert_eq!(run(&mut s, &mut store, &aes_auth_write(&AES2, &[0x44; 16], &pol)).sw, SW_NO_ERROR); + let mut upd = tlv(TAG_1, &AES2); + upd.extend(tlv(crate::tlv::TAG_3, &[0x55; 32])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_AES, 0, &upd)).sw, SW_WRONG_DATA); + assert!(matches!(store.get(&AES2), Some(SecureObject::AESKey { key }) if key == &[0x44; 16])); + let mut upd = tlv(TAG_1, &AES2); + upd.extend(tlv(crate::tlv::TAG_3, &[0x55; 16])); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE, P1_AES, 0, &upd)).sw, SW_NO_ERROR); + assert!(store.auth_state(&AES2).is_some()); + // Malformed maximum attempts, AESKey and UserID. + const AES3: [u8; 4] = [0x7D, 0xA0, 0x00, 0x22]; + let r = run(&mut s, &mut store, &aes_auth_write(&AES3, &[0x33; 16], &tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[1]))); + assert_eq!(r.sw, SW_WRONG_DATA); + assert!(!store.exists(&AES3)); + let mut d = tlv(crate::tlv::TAG_MAX_ATTEMPTS, &[0, 0, 1]); + d.extend(tlv(TAG_1, &AES3)); + d.extend(tlv(crate::tlv::TAG_2, b"abcd")); + assert_eq!(run(&mut s, &mut store, &cmd(0x80, INS_WRITE | 0x40, P1_USERID, 0, &d)).sw, SW_WRONG_DATA); + assert!(!store.exists(&AES3)); + } + + #[test] + fn attested_read_inside_a_session_and_reserved_attestation_key() { + let (mut store, mut s) = demo_store(); + const UID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + const ECKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x03]; + let att = crate::object_store::RESERVED_ID_ATTESTATION_KEY; + let sid = create_for(&mut s, &mut store, UID); + let right = cmd(0x80, INS_MGMT, 0, P2_SESSION_USERID, &tlv(TAG_1, &[0xC0, 1, 2, 3, 4])); + assert_eq!(process(&mut s, &mut store, &sid, &right).sw, SW_NO_ERROR); + let mut d = tlv(TAG_1, &ECKEY); + d.extend(tlv(crate::tlv::TAG_5, &att)); + d.extend(tlv(crate::tlv::TAG_6, &[0x21])); + d.extend(tlv(crate::tlv::TAG_7, &[0x5A; 16])); + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_READ | INS_ATTEST, 0, 0, &d)); + assert_eq!(r.sw, SW_NO_ERROR); + let t = crate::tlv::parse_tlvs(&r.data).unwrap(); + let Some(SecureObject::ECPublicKey { public_key, .. }) = store.get(&ECKEY) else { panic!() }; + assert_eq!(&tlv::find_tlv(&t, TAG_1).unwrap().value, public_key); + assert!(tlv::find_tlv(&t, 0x52).is_some(), "signature"); + // The attestation key itself only reads: no generic signing with it. + let mut sign = tlv(TAG_1, &att); + sign.extend(tlv(crate::tlv::TAG_2, &[0x21])); + sign.extend(tlv(crate::tlv::TAG_3, &[0x11; 32])); + let r = process(&mut s, &mut store, &sid, &cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_CRYPTO, P1_SIGNATURE, P2_SIGN, &sign)); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + let r = run(&mut s, &mut store, &cmd(0x80, INS_MGMT, 0, P2_DELETE_OBJECT, &tlv(TAG_1, &att))); + assert_eq!(r.sw, SW_CONDITIONS_NOT_SATISFIED); + } +} diff --git a/SE050Sim/se050-sim/src/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/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index c188faf..a8ab812 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -216,7 +216,7 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR // Check if this is key generation (P2=Generate) or has a key size tag let generated = apdu.p2 == P2_GENERATE || key_data.as_ref().is_some_and(|d| d.len() <= 2); - let response = if generated { + let key = if generated { // Key generation: Tag3 contains 2-byte key size let key_len = key_data .as_ref() @@ -234,26 +234,42 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR let mut key = vec![0u8; key_len_bytes]; rand::thread_rng().fill_bytes(&mut key); - store.insert(obj_id, SecureObject::AESKey { key }); - ApduResponse::success() + key } else if let Some(key) = key_data { // Import key data if key.len() != 16 && key.len() != 24 && key.len() != 32 { return ApduResponse::error(SW_WRONG_DATA); } - store.insert(obj_id, SecureObject::AESKey { key }); - ApduResponse::success() + key } else { - ApduResponse::error(SW_WRONG_DATA) + return ApduResponse::error(SW_WRONG_DATA); + }; + + // INS_AUTH_OBJECT: an AESKey Authentication Object, AES-128 only, with an + // optional maximum number of failed attempts. Checked before anything is + // stored; an existing Authentication Object stays AES-128 on update. + let auth_object = apdu.ins & 0x40 != 0; + let max_attempts = match crate::tlv::max_attempts(&tlvs, 0x7FFF) { + Ok(v) if auth_object => v, + Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA), + _ => 0, }; - if response.sw == SW_NO_ERROR && !object_existed { + if (auth_object || store.auth_state(&obj_id).is_some()) && key.len() != 16 { + return ApduResponse::error(SW_WRONG_DATA); + } + + store.insert(obj_id, SecureObject::AESKey { key }); + if !object_existed { store.set_creation_metadata( obj_id, creation_policy, if generated { 0x02 } else { 0x01 }, ); + if auth_object { + store.set_auth_object(obj_id, max_attempts); + } } - response + ApduResponse::success() } /// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC). @@ -310,6 +326,16 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu } } +/// Encrypting needs POLICY_OBJ_ALLOW_ENC, decrypting POLICY_OBJ_ALLOW_DEC, +/// for the current session owner. +fn cipher_permission(encrypting: bool) -> u32 { + if encrypting { + crate::policy::POLICY_OBJ_ALLOW_ENC + } else { + crate::policy::POLICY_OBJ_ALLOW_DEC + } +} + /// Shared body of the encrypt/decrypt one-shot handlers. /// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=input, Tag4=IV(opt) fn handle_cipher_oneshot( @@ -354,6 +380,9 @@ fn handle_cipher_oneshot( Some(obj) => obj.clone(), None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + if !store.policy_allows(&key_id, cipher_permission(encrypting)) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } let key_data = match &key_obj { SecureObject::AESKey { key } => key, @@ -416,6 +445,10 @@ pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes if !store.exists(&key_id) { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } + // The key is bound to the crypto object here; update and final reuse it. + if !store.policy_allows(&key_id, cipher_permission(encrypting)) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } let mut chain = vec![0u8; 16]; if let Some(t) = tlv::find_tlv(&tlvs, TAG_4) { diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index bff0a7d..9307ce0 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -106,6 +106,21 @@ 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 = match crate::tlv::max_attempts(&tlvs, 0x7FFF) { + Ok(v) if auth_object => v, + Ok(_) => 0, + Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA), + Err(()) => 0, + }; + if (auth_object || store.auth_state(&obj_id).is_some()) && !matches!(curve, ECCurve::NistP256) { + return ApduResponse::error(SW_WRONG_DATA); + } + // 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 +162,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 +520,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 +772,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..2b91b76 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -24,15 +24,49 @@ use crate::object_store::types::SecureObject; use crate::object_store::ObjectStore; use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY}; +/// WriteUserID (AN12543 4.7.1.5): policy and maximum attempts (< 256) only on +/// creation, TAG_1 object ID, TAG_2 value of 4 to 16 bytes. An existing UserID +/// cannot be updated. +fn handle_write_userid(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Ok(tlvs) = apdu.parse_tlvs() else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let Some(obj_id) = extract_object_id(&tlvs) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let Some(value) = tlv::find_tlv(&tlvs, TAG_2).map(|t| t.value.clone()) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if !(4..=16).contains(&value.len()) { + return ApduResponse::error(SW_WRONG_DATA); + } + if store.exists(&obj_id) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + let Ok(max) = crate::tlv::max_attempts(&tlvs, 255) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + let policy = match crate::policy::creation_policy(&tlvs) { + Ok(p) => p, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + store.insert(obj_id, SecureObject::UserID { value }); + store.set_creation_metadata(obj_id, policy, 0x01); + store.set_auth_object(obj_id, max); + ApduResponse::success() +} + /// Handle WRITE commands for Binary, UserID, and Counter objects. pub fn handle_write(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { match apdu.cred_type() { P1_BINARY => handle_write_binary(apdu, store), // WriteUserID is refused in plain (unauthenticated) sessions: // bench-verified 0x6985 on SE050C applet 3.1.1 and SE051 - // applet 7.2.0 alike, via both the raw APDU and the sss layer. - // The simulator only models plain sessions, so the write is - // always refused. + // applet 7.2.0 alike, via both the raw APDU and the sss layer + // (the transport refuses Authentication Object writes outside an + // authenticated channel, see applet_session::intercept). UserIDs + // exist only as Authentication Objects. + P1_USERID if apdu.ins & 0x40 != 0 => handle_write_userid(apdu, store), P1_USERID => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), P1_COUNTER => handle_write_counter(apdu, store), _ => ApduResponse::error(SW_WRONG_P1P2), @@ -433,17 +467,36 @@ fn handle_read_attributes( Some(id) => id, None => return ApduResponse::error(SW_WRONG_DATA), }; - let Some(object) = store.get(&obj_id) else { - return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); - }; + match object_attributes(store, &obj_id) { + Some(attributes) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)]), + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + } +} + +/// Attribute bytes of an object (AN12543 tables 3 and 4), for +/// ReadObjectAttributes and attested reads. +pub fn object_attributes(store: &ObjectStore, obj_id: &[u8; 4]) -> Option> { + let obj_id = *obj_id; + let object = store.get(&obj_id)?; let mut attributes = Vec::new(); attributes.extend_from_slice(&obj_id); 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 + // UserID attempts are counted but not reported (AN12543 3.2.4.4). + let shown = if matches!(object, SecureObject::UserID { .. }) { 0 } else { auth.attempts }; + attributes.extend_from_slice(&shown.to_be_bytes()); + attributes.extend_from_slice(&[0x00; 4]); + attributes.extend_from_slice(&auth.max_attempts.to_be_bytes()); + } else { + 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); @@ -453,8 +506,7 @@ fn handle_read_attributes( attributes.push(0x01); // legacy store: treat as externally created } attributes.extend_from_slice(&[0x00; 4]); // object version - - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)]) + Some(attributes) } fn handle_check_exists(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { diff --git a/SE050Sim/se050-sim/src/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..e50298a 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -84,6 +84,28 @@ 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]; + +/// The SE's ECC attestation key pair (SSS_SE05X_RESID_ATTESTATION_KEY in Plug +/// & Trust): signs attested reads; hosts read its public key from the SE. +pub const RESERVED_ID_ATTESTATION_KEY: [u8; 4] = [0xF0, 0x00, 0x00, 0x12]; + +/// Applet-reserved objects the simulator manages itself. Hosts may read them +/// (public part, attributes, existence) but not create, replace, delete or +/// use them in generic crypto commands; only the applet's own flows use the +/// private keys. +pub const RESERVED_IDS: &[[u8; 4]] = &[RESERVED_ID_ECKEY_SESSION, RESERVED_ID_ATTESTATION_KEY]; + +pub fn is_reserved_id(id: &[u8]) -> bool { + RESERVED_IDS.iter().any(|r| r[..] == *id) } fn default_curves() -> HashMap { @@ -91,6 +113,128 @@ fn default_curves() -> HashMap { (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() } +/// Writes the persisted store without holding up the APDU being processed: +/// a background thread writes the latest snapshot of each store file. A +/// synchronous write in the command path took up to a second on a busy disk, +/// longer than hosts wait for an answer (NXP's T=1 stack then asks for a +/// retransmission). +/// +/// - Snapshots are coalesced per destination path, so several stores in one +/// process never discard each other's updates. +/// - `flush_store` waits until a path has no queued or in-flight write; it runs +/// before a store file is loaded and when a persistent store is dropped, so +/// reopening a store never reads stale state. +/// - The file holds private keys and SCP03 keys: it is written to a temporary +/// file created with mode 0600 (or the existing file's mode), then renamed. +mod writer { + use std::collections::{HashMap, HashSet}; + use std::path::{Path, PathBuf}; + use std::sync::{Condvar, Mutex, OnceLock}; + + #[derive(Default)] + struct State { + pending: HashMap, + in_flight: HashSet, + } + + struct Writer { + state: Mutex, + cv: Condvar, + } + + static WRITER: OnceLock<&'static Writer> = OnceLock::new(); + + fn writer() -> &'static Writer { + WRITER.get_or_init(|| { + let w: &'static Writer = Box::leak(Box::new(Writer { + state: Mutex::new(State::default()), + cv: Condvar::new(), + })); + std::thread::spawn(move || run(w)); + w + }) + } + + fn run(w: &'static Writer) { + loop { + let batch: Vec<(PathBuf, String)> = { + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + while st.pending.is_empty() { + st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner()); + } + let batch: Vec<_> = st.pending.drain().collect(); + for (p, _) in &batch { + st.in_flight.insert(p.clone()); + } + batch + }; + for (path, json) in &batch { + write_file(path, json); + } + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + for (p, _) in &batch { + st.in_flight.remove(p); + } + w.cv.notify_all(); + } + } + + fn write_file(path: &Path, json: &str) { + use std::io::Write; + let mut tmp_name = path.as_os_str().to_owned(); + tmp_name.push(".tmp"); + let tmp = PathBuf::from(tmp_name); + let _ = std::fs::remove_file(&tmp); + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let Ok(mut f) = opts.open(&tmp) else { return }; + // Keep the access restrictions of the file being replaced. + if let Ok(meta) = std::fs::metadata(path) { + let _ = f.set_permissions(meta.permissions()); + } + if f.write_all(json.as_bytes()).is_ok() && f.flush().is_ok() { + drop(f); + let _ = std::fs::rename(&tmp, path); + } else { + drop(f); + let _ = std::fs::remove_file(&tmp); + } + } + + pub fn write_store(path: &Path, json: String) { + let w = writer(); + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + st.pending.insert(path.to_path_buf(), json); + w.cv.notify_all(); + } + + /// Blocks until `path` has no queued or in-flight write. + pub fn flush_store(path: &Path) { + let Some(w) = WRITER.get() else { return }; + let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner()); + while st.pending.contains_key(path) || st.in_flight.contains(path) { + st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner()); + } + } +} + +use writer::{flush_store, write_store}; + +/// A persistent store finishes writing its file before it goes away, so a +/// store reopened on the same file (or an orderly shutdown) sees every update. +impl Drop for ObjectStore { + fn drop(&mut self) { + if let Some(path) = &self.persist_path { + flush_store(path); + } + } +} + impl ObjectStore { pub fn new() -> Self { Self { @@ -102,6 +246,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], } } @@ -115,6 +260,7 @@ impl ObjectStore { crypto_object_types: HashMap::new(), scp_required: false, platform_scp: None, + session_owner: [0; 4], }; store.load(); store @@ -143,21 +289,124 @@ 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; + } + + /// SE050_SIM_DEMO_AUTH=1: the demo Authentication Objects that NXP Plug & + /// Trust's examples use (sss/ex/inc/ex_sss_auth.h, ex_sss_objid.h), so its + /// UserID, AESKey and ECKey session modes can run against the simulator: + /// 0x7DA00001 UserID C0 01 02 03 04, 0x7DA00002 AESKey 40..4F, 0x7DA00003 + /// ECKey with the public key of EX_SSS_AUTH_SE05X_KEY_HOST_ECDSA_KEY. No + /// policy, unlimited attempts. Created once, like trust provisioning. + pub fn ensure_demo_auth_objects(&mut self) { + const USERID: [u8; 4] = [0x7D, 0xA0, 0x00, 0x01]; + const AESKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x02]; + const ECKEY: [u8; 4] = [0x7D, 0xA0, 0x00, 0x03]; + const ECKEY_PUB: &str = "043c9e47edf051a3589f67302d22567c2e17229e8883338ec3b7d527f9ee71d0a81aae7fe21caa6677783aa88da6d6a8ad5ec53b10bc0b11094482f04d24b5bec4"; + if !self.objects.contains_key(&USERID) { + self.objects.insert(USERID, SecureObject::UserID { value: vec![0xC0, 0x01, 0x02, 0x03, 0x04] }); + self.metadata.insert(USERID, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); + } + if !self.objects.contains_key(&AESKEY) { + self.objects.insert(AESKEY, SecureObject::AESKey { key: (0x40..=0x4F).collect() }); + self.metadata.insert(AESKEY, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); + } + if !self.objects.contains_key(&ECKEY) { + self.objects.insert(ECKEY, SecureObject::ECPublicKey { + curve: crate::object_store::types::ECCurve::NistP256, + public_key: hex::decode(ECKEY_PUB).unwrap_or_default(), + }); + self.metadata.insert(ECKEY, ObjectMetadata { policy: None, origin: 0x03, auth: Some(Default::default()) }); + } + self.persist(); + } + + /// The SE's own key pairs (ECKey session key agreement, attestation), + /// created on first use and persisted. + pub fn ensure_eckey_session_key(&mut self) { + let mut changed = false; + for id in [RESERVED_ID_ECKEY_SESSION, RESERVED_ID_ATTESTATION_KEY] { + if self.objects.contains_key(&id) { + continue; + } + use p256::elliptic_curve::sec1::ToEncodedPoint; + let sk = p256::SecretKey::random(&mut rand::rngs::OsRng); + let pk = sk.public_key().to_encoded_point(false).as_bytes().to_vec(); + self.objects.insert(id, SecureObject::ECKeyPair { + curve: crate::object_store::types::ECCurve::NistP256, + private_key: sk.to_bytes().to_vec(), + public_key: pk, + }); + changed = true; + } + if changed { + self.persist(); + } + } + pub fn metadata(&self, id: &[u8; 4]) -> Option<&ObjectMetadata> { self.metadata.get(id) } - /// 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, } } @@ -287,12 +536,13 @@ impl ObjectStore { "platform_scp": self.platform_scp, }); if let Ok(json) = serde_json::to_string_pretty(&doc) { - let _ = std::fs::write(path, json); + write_store(path, json); } } fn load(&mut self) { let Some(path) = &self.persist_path else { return }; + flush_store(path); let Ok(json) = std::fs::read_to_string(path) else { return }; let Ok(value): Result = serde_json::from_str(&json) else { return; @@ -467,4 +717,58 @@ mod persistence_tests { assert_eq!(loaded.dek, config.dek); let _ = std::fs::remove_file(&path); } + + #[test] + fn test_stores_on_different_paths_keep_their_latest_snapshot() { + let a = unique_store_path("writer_path_a"); + let b = unique_store_path("writer_path_b"); + { + let mut sa = ObjectStore::with_persistence(a.clone()); + let mut sb = ObjectStore::with_persistence(b.clone()); + for i in 0..50u8 { + sa.insert([0, 0, 0xA0, i], SecureObject::Binary { data: vec![i] }); + sb.insert([0, 0, 0xB0, i], SecureObject::Binary { data: vec![i] }); + } + } + let sa = ObjectStore::with_persistence(a.clone()); + let sb = ObjectStore::with_persistence(b.clone()); + for i in 0..50u8 { + assert!(sa.exists(&[0, 0, 0xA0, i]) && !sa.exists(&[0, 0, 0xB0, i])); + assert!(sb.exists(&[0, 0, 0xB0, i]) && !sb.exists(&[0, 0, 0xA0, i])); + } + let _ = std::fs::remove_file(&a); + let _ = std::fs::remove_file(&b); + } + + #[test] + fn test_reopen_sees_updates_of_a_store_still_open() { + let path = unique_store_path("writer_reopen"); + let mut first = ObjectStore::with_persistence(path.clone()); + first.set_scp_required(true); + let second = ObjectStore::with_persistence(path.clone()); + assert!(second.scp_required()); + drop(first); + let _ = std::fs::remove_file(&path); + } + + #[cfg(unix)] + #[test] + fn test_store_file_is_private_and_keeps_its_mode() { + use std::os::unix::fs::PermissionsExt; + let path = unique_store_path("writer_mode"); + let mode = |p: &PathBuf| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + { + let mut store = ObjectStore::with_persistence(path.clone()); + store.insert([0, 0, 0, 1], SecureObject::Binary { data: vec![1] }); + } + assert_eq!(mode(&path), 0o600); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).unwrap(); + { + let mut store = ObjectStore::with_persistence(path.clone()); + store.insert([0, 0, 0, 2], SecureObject::Binary { data: vec![2] }); + } + assert_eq!(mode(&path), 0o640); + assert!(ObjectStore::with_persistence(path.clone()).exists(&[0, 0, 0, 2])); + let _ = std::fs::remove_file(&path); + } } diff --git a/SE050Sim/se050-sim/src/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..4260601 100644 --- a/SE050Sim/se050-sim/src/policy.rs +++ b/SE050Sim/se050-sim/src/policy.rs @@ -33,6 +33,11 @@ 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; +pub const POLICY_OBJ_ALLOW_ENC: u32 = 0x0200_0000; +pub const POLICY_OBJ_ALLOW_DEC: u32 = 0x0100_0000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InvalidPolicy; @@ -77,6 +82,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..b8dfbf2 100644 --- a/SE050Sim/se050-sim/src/scp03/mod.rs +++ b/SE050Sim/se050-sim/src/scp03/mod.rs @@ -210,6 +210,14 @@ impl Scp03State { /// EXTERNAL AUTHENTICATE (CLA 0x84 INS 0x82). `raw` is the full APDU: /// 84 82 00 10 . Any failure drops /// the channel to Idle. + /// The established channel, once EXTERNAL AUTHENTICATE has succeeded. + pub fn into_session(self) -> Option { + match self { + Scp03State::Active(s) => Some(s), + _ => None, + } + } + pub fn external_authenticate(&mut self, raw: &[u8]) -> ApduResponse { // Take the pending state out; every failure path leaves us Idle. let p = match std::mem::replace(self, Scp03State::Idle) { @@ -261,6 +269,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..edc37f7 100644 --- a/SE050Sim/se050-sim/src/t1.rs +++ b/SE050Sim/se050-sim/src/t1.rs @@ -166,6 +166,10 @@ 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, + /// Last single-frame response, for an R-block asking for a retransmission. + last_response: Vec>, } impl T1Responder { @@ -179,6 +183,8 @@ impl T1Responder { apdu_reassembly: Vec::new(), scp03: Scp03State::new(), selected_ssd: false, + applet_sessions: Default::default(), + last_response: Vec::new(), } } @@ -204,6 +210,13 @@ impl T1Responder { FrameType::IFrame { seq, multi } => { self.handle_i_frame(seq, multi, &frame.payload, store) } + FrameType::RFrame { err, .. } if err != 0 => { + // R-block with an error indication: the host did not get our + // last block (e.g. it gave up waiting) and asks for it again. + // Without an answer the host and a bridge in between would + // wait forever. + self.last_response.clone() + } FrameType::RFrame { .. } => { // R-frames during multi-frame response sending // Currently we pre-queue all response frames, so we just ignore R-frames @@ -223,6 +236,9 @@ impl T1Responder { self.apdu_reassembly.clear(); self.scp03.reset(); self.selected_ssd = false; + self.applet_sessions.reset(); + // Nothing is pending on a fresh link. + self.last_response.clear(); let ft = FrameType::SFrame { code: T1_S_INTERFACE_SOFT_RESET, @@ -238,6 +254,9 @@ impl T1Responder { self.apdu_reassembly.clear(); self.scp03.reset(); self.selected_ssd = false; + self.applet_sessions.reset(); + // Nothing is pending on a fresh link. + self.last_response.clear(); let ft = FrameType::SFrame { code: 0x00, is_response: true }; let (header, payload_crc) = build_frame(self.nad_se2hd, ft, &[]); @@ -299,6 +318,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 +363,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 +390,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 +419,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 @@ -413,7 +448,9 @@ impl T1Responder { let (header, payload_crc) = build_frame(self.nad_se2hd, ft, response_bytes); chunks.push(header); chunks.push(payload_crc); + self.last_response = chunks.clone(); } else { + self.last_response.clear(); // Multi-frame response let mut offset = 0; while offset < response_bytes.len() { @@ -444,6 +481,89 @@ impl T1Responder { mod tests { use super::*; + /// Sends one APDU in an I-block (host sequence `seq`) and returns the + /// status word of the single-frame response. + fn apdu_sw(t1: &mut T1Responder, store: &mut ObjectStore, seq: u8, apdu: &[u8]) -> u16 { + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq, multi: false }, apdu); + let out = t1.process_frame(&[h, p].concat(), store); + let payload = &out[1]; + let body = &payload[..payload.len() - 2]; + u16::from_be_bytes([body[body.len() - 2], body[body.len() - 1]]) + } + + #[test] + fn test_reset_and_resync_end_applet_sessions() { + use crate::object_store::types::{ECCurve, SecureObject}; + use p256::elliptic_curve::sec1::ToEncodedPoint; + let auth_id = [0x7D, 0xA0, 0x00, 0x01]; + let pk = p256::SecretKey::random(&mut rand::rngs::OsRng).public_key(); + // None = control (no reset), else the S-block request code. + for reset in [None, Some(T1_S_INTERFACE_SOFT_RESET), Some(0x00)] { + let mut store = ObjectStore::new(); + store.insert(auth_id, SecureObject::ECPublicKey { + curve: ECCurve::NistP256, + public_key: pk.to_encoded_point(false).as_bytes().to_vec(), + }); + store.set_creation_metadata(auth_id, None, 0x01); + store.set_auth_object(auth_id, 0); + let mut t1 = T1Responder::new(0x5A); + // CreateSession (plain): the response carries the session ID. + let mut create = vec![0x80, 0x04, 0x00, 0x1B, 0x06, 0x41, 0x04]; + create.extend_from_slice(&auth_id); + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &create); + let out = t1.process_frame(&[h, p].concat(), &mut store); + let body = &out[1][..out[1].len() - 2]; + assert_eq!(&body[body.len() - 2..], &[0x90, 0x00]); + let sid = body[2..10].to_vec(); + let mut seq = 1; + if let Some(code) = reset { + let (h, p) = build_frame(0x5A, FrameType::SFrame { code, is_response: false }, &[]); + t1.process_frame(&[h, p].concat(), &mut store); + seq = 0; + } + // CloseSession for that session ID, inside ProcessSessionCmd. + let mut process = vec![0x80, 0x05, 0x00, 0x00, 0x10, 0x10, 0x08]; + process.extend_from_slice(&sid); + process.extend_from_slice(&[0x41, 0x04, 0x80, 0x04, 0x00, 0x1C]); + let sw = apdu_sw(&mut t1, &mut store, seq, &process); + let expected = if reset.is_some() { 0x6985 } else { 0x9000 }; + assert_eq!(sw, expected, "reset {reset:?}"); + } + } + + #[test] + fn test_r_block_with_error_repeats_last_response() { + let mut t1 = T1Responder::new(0x5A); + let mut store = ObjectStore::new(); + // I-block 0 carrying SELECT of the IoT applet + let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00]; + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select); + let first = t1.process_frame(&[h, p].concat(), &mut store); + assert_eq!(first.len(), 2); + // R-block, N(R) = 1, error bits set: the same response again + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x01 }, &[]); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store), first); + // without error bits (chaining acknowledgement) nothing is sent + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x00 }, &[]); + assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty()); + } + + #[test] + fn test_reset_and_resync_forget_last_response() { + let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00]; + for code in [T1_S_INTERFACE_SOFT_RESET, 0x00] { + let mut t1 = T1Responder::new(0x5A); + let mut store = ObjectStore::new(); + let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2); + let (h, p) = build_frame(0x5A, FrameType::SFrame { code, is_response: false }, &[]); + assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2); + // An error R-block after the reset has no previous response to replay. + let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 0, err: 0x01 }, &[]); + assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty(), "S-block {code:#04x}"); + } + } + #[test] fn test_crc16() { // From driver test: assert_eq!(0x78a1, Se050CRC::calculate(&[0,48,95,111,242])); diff --git a/SE050Sim/se050-sim/src/tlv.rs b/SE050Sim/se050-sim/src/tlv.rs index 822108e..579fa66 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 { @@ -112,6 +118,20 @@ pub enum TlvError { pub const TAG_SESSION_ID: u8 = 0x10; pub const TAG_POLICY: u8 = 0x11; pub const TAG_MAX_ATTEMPTS: u8 = 0x12; + +/// Maximum number of failed attempts of an Authentication Object: two bytes, +/// 0 (= unlimited) to `limit`; 0 when the tag is absent. Err when the tag is +/// present but malformed or out of range. +pub fn max_attempts(tlvs: &[Tlv], limit: u16) -> Result { + match find_tlv(tlvs, TAG_MAX_ATTEMPTS) { + None => Ok(0), + Some(t) if t.value.len() == 2 => { + let v = u16::from_be_bytes([t.value[0], t.value[1]]); + if v <= limit { Ok(v) } else { Err(()) } + } + Some(_) => Err(()), + } +} pub const TAG_1: u8 = 0x41; pub const TAG_2: u8 = 0x42; pub const TAG_3: u8 = 0x43;