From ced25f78ef4922f6d6179cabf3448939d213579f Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 3 Aug 2026 16:47:02 +0100 Subject: [PATCH 1/2] SE050Sim: model applet 3.1.1/7.2.0 personalities, fix hardware-verified gaps Every behavior below was cross-checked against real silicon in the August 2026 bench sessions: an SE051 (applet 7.2.0) and an SE050C (applet 3.1.1) driven over raw Se05x_API_* APDUs on the Raspberry Pi bench, with NIST/RFC vectors where applicable. SE050_SIM_APPLET selects the personality at runtime (default 7.2.0; 3/3.1.1 for the SE050C). Modelled differences: SELECT/GetVersion blobs, GetFreeMemory width (U16 vs U32) and values, GetRandom cap (880 vs 1018 bytes), EC ReadType codes (generic vs curve-specific), and duplicate CreateECCurve (7.2 refuses 0x6985; 3.x accepts and silently wipes the curve params). Fixes applying to both generations: - MAC surface: HMAC-SHA1/256/384/512 + AES-CMAC, one-shot and multi-step, generate and validate (was entirely missing, 0x6A86). - ReadObject on AESKey objects now refused 0x6986 like HMACKey, regardless of an attached ALLOW_READ policy. - Cipher mode byte honored: ECB_NOPAD/CBC_NOPAD/CTR implemented and verified against SP800-38A vectors; unaligned NOPAD input 0x6985. - Streaming cipher emits output per CipherUpdate with proper chaining and returns only the remainder at Final. - Crypto-object lifecycle enforced: CreateCryptoObject required before Init (0x6985), duplicate create 0x6986; DigestInit now takes the real wire format (TAG_2 only, algo from the create subtype). - EC Weierstrass curves are stateful objects: create/set-param/delete/ list with SET=0x02 semantics, keygen requires a fully parameterized curve. The sim pre-provisions P-192..P-521 so existing hosts keep working; DeleteAll re-provisions. - P-521 and P-192 supported end to end (keygen/import/sign/verify/ ECDH); p192 0.13 lacks a sign primitive so signing is implemented over its arithmetic and tested against its VerifyingKey. - ECDSA sign/verify enforce input length == algo digest length. - Missing-object and delete-nonexistent SWs corrected to 0x6985; binary file bounds enforced (read past end 0x6985, write past end 0x6A80, size immutable); ReadSize reports EC scalar sizes; counter size fixed at creation with Set/Inc semantics; ReadIDList emits the TAG_1 more-indicator + TAG_2 ID-list format with type filtering; GetRandom(0) refused; DigestOneShot hashes the empty message; WriteUserID refused 0x6985 in plain sessions. Object store schema extended to {objects, ec_curves} with a fallback loader for legacy flat-map files. --- SE050Sim/se050-sim/Cargo.toml | 5 + SE050Sim/se050-sim/src/apdu.rs | 4 + SE050Sim/se050-sim/src/applet.rs | 104 +++ SE050Sim/se050-sim/src/dispatch.rs | 52 +- SE050Sim/se050-sim/src/handlers/aes.rs | 608 +++++++++++++----- SE050Sim/se050-sim/src/handlers/crypto_obj.rs | 6 + SE050Sim/se050-sim/src/handlers/curve.rs | 244 +++++++ SE050Sim/se050-sim/src/handlers/digest.rs | 32 +- SE050Sim/se050-sim/src/handlers/ec.rs | 344 +++++++++- SE050Sim/se050-sim/src/handlers/mac.rs | 419 ++++++++++++ SE050Sim/se050-sim/src/handlers/management.rs | 127 +++- SE050Sim/se050-sim/src/handlers/mod.rs | 2 + .../se050-sim/src/handlers/object_mgmt.rs | 472 ++++++++++---- SE050Sim/se050-sim/src/handlers/rsa.rs | 4 +- SE050Sim/se050-sim/src/handlers/session.rs | 17 +- SE050Sim/se050-sim/src/lib.rs | 1 + SE050Sim/se050-sim/src/object_store/mod.rs | 167 ++++- SE050Sim/se050-sim/src/object_store/types.rs | 103 ++- 18 files changed, 2329 insertions(+), 382 deletions(-) create mode 100644 SE050Sim/se050-sim/src/applet.rs create mode 100644 SE050Sim/se050-sim/src/handlers/curve.rs create mode 100644 SE050Sim/se050-sim/src/handlers/mac.rs diff --git a/SE050Sim/se050-sim/Cargo.toml b/SE050Sim/se050-sim/Cargo.toml index 0a0a7e7..c81c9b6 100644 --- a/SE050Sim/se050-sim/Cargo.toml +++ b/SE050Sim/se050-sim/Cargo.toml @@ -10,9 +10,12 @@ embedded-hal = "0.2" crc16 = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" +p192 = { version = "0.13", features = ["ecdsa", "arithmetic"] } +elliptic-curve = { version = "0.13", features = ["ecdh"] } p224 = { version = "0.13", features = ["ecdsa", "ecdh"] } p256 = { version = "0.13", features = ["ecdsa", "ecdh"] } p384 = { version = "0.13", features = ["ecdsa", "ecdh"] } +p521 = { version = "0.13", features = ["ecdsa", "ecdh"] } ecdsa = { version = "0.16", features = ["signing", "verifying", "der"] } ed25519-dalek = { version = "2", features = ["rand_core"] } x25519-dalek = { version = "2", features = ["static_secrets"] } @@ -21,6 +24,8 @@ cbc = { version = "0.1", features = ["alloc"] } rsa = { version = "0.9", features = ["sha2", "hazmat"] } sha1 = "0.10" sha2 = "0.10" +hmac = "0.12" +cmac = "0.7" signature = "2.2" rand = "0.8" hex = "0.4" diff --git a/SE050Sim/se050-sim/src/apdu.rs b/SE050Sim/se050-sim/src/apdu.rs index ab636dd..c0a267a 100644 --- a/SE050Sim/se050-sim/src/apdu.rs +++ b/SE050Sim/se050-sim/src/apdu.rs @@ -219,8 +219,12 @@ pub const P2_DELETE_ALL: u8 = 0x2A; pub const P2_ID: u8 = 0x36; pub const P2_ENCRYPT_ONESHOT: u8 = 0x37; pub const P2_DECRYPT_ONESHOT: u8 = 0x38; +pub const P2_PARAM: u8 = 0x40; pub const P2_ENCRYPT_INIT: u8 = 0x42; pub const P2_DECRYPT_INIT: u8 = 0x43; +pub const P2_MAC_VALIDATE: u8 = 0x44; +pub const P2_GENERATE_ONESHOT: u8 = 0x45; +pub const P2_VALIDATE_ONESHOT: u8 = 0x46; pub const P2_CRYPTO_LIST: u8 = 0x47; pub const P2_RAW: u8 = 0x4F; pub const P2_RANDOM: u8 = 0x49; diff --git a/SE050Sim/se050-sim/src/applet.rs b/SE050Sim/se050-sim/src/applet.rs new file mode 100644 index 0000000..f5e59cb --- /dev/null +++ b/SE050Sim/se050-sim/src/applet.rs @@ -0,0 +1,104 @@ +/* applet.rs + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of SE050Sim. + * + * SE050Sim is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * SE050Sim is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/// Applet personality selection. +/// +/// The simulator can present itself as either of the two applet +/// generations that were bench-characterized on real silicon (August +/// 2026, see SE050Sim/HARDWARE_VALIDATION.md): an SE050C running applet +/// 3.1.1 or an SE051 running applet 7.2.0. Almost all behavior is +/// identical between the two; the differences the simulator models are: +/// +/// * SELECT / GetVersion version bytes. +/// * GetFreeMemory response width (2 bytes on 3.x, 4 bytes on 7.2) and +/// the reported per-type values. +/// * GetRandom maximum request size (880 bytes on the SE050C, 1018 on +/// the SE051). +/// * ReadType secure-object type codes for EC keys (generic 0x01/0x03 +/// on 3.x, curve-specific on 7.2). +/// * CreateECCurve on an already existing curve: applet 7.2 refuses +/// with SW 0x6985; applet 3.1.1 returns 0x9000 and silently resets +/// the curve to a parameter-less state (subsequent key generation on +/// it fails 0x6985 until the parameters are uploaded again). + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppletVersion { + /// SE050C, applet 3.1.1 (ATR historical bytes "JCOP4"). + V3_1_1, + /// SE051, applet 7.2.0 (ATR historical bytes "eSE051"). Default. + V7_2_0, +} + +impl AppletVersion { + /// Read the personality from the SE050_SIM_APPLET environment + /// variable. Accepts "3", "3.1.1" (SE050C) and "7", "7.2", "7.2.0" + /// (SE051). Unset or unrecognized values select 7.2.0, matching the + /// version the simulator has always advertised. + pub fn from_env() -> Self { + match std::env::var("SE050_SIM_APPLET") { + Ok(v) if v.starts_with('3') => AppletVersion::V3_1_1, + _ => AppletVersion::V7_2_0, + } + } + + /// 7-byte version blob returned by SELECT and GetVersion: + /// major, minor, patch, appletConfig (2B), secureBox (2B). + /// Captured from real parts: SE050C applet 3.1.1 returns + /// 03 01 01 6f ff 01 0b, SE051 applet 7.2.0 returns + /// 07 02 00 3f ff ff ff. + pub fn version_bytes(self) -> [u8; 7] { + match self { + AppletVersion::V3_1_1 => [0x03, 0x01, 0x01, 0x6F, 0xFF, 0x01, 0x0B], + AppletVersion::V7_2_0 => [0x07, 0x02, 0x00, 0x3F, 0xFF, 0xFF, 0xFF], + } + } + + /// Largest GetRandom request the applet serves; one byte more + /// returns SW 0x6985 (bench-measured: 880 on SE050C 3.1.1, 1018 on + /// SE051 7.2.0). + pub fn get_random_max(self) -> usize { + match self { + AppletVersion::V3_1_1 => 880, + AppletVersion::V7_2_0 => 1018, + } + } + + /// GetFreeMemory reply for a memory type, as measured on the bench + /// parts. Applet 3.x replies with a 2-byte value, 7.2 with 4 bytes + /// (the v04.07.01 middleware parses U16 vs U32 accordingly). + pub fn free_memory_bytes(self, memory_type: u8) -> Option> { + let (persistent, transient_reset, transient_deselect): (u32, u32, u32) = + match self { + AppletVersion::V3_1_1 => (31304, 575, 560), + AppletVersion::V7_2_0 => (21000, 605, 592), + }; + let value = match memory_type { + 0x01 => persistent, + 0x02 => transient_reset, + 0x03 => transient_deselect, + _ => return None, + }; + Some(match self { + AppletVersion::V3_1_1 => (value as u16).to_be_bytes().to_vec(), + AppletVersion::V7_2_0 => value.to_be_bytes().to_vec(), + }) + } +} diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 4dd0429..425d0ea 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -23,13 +23,19 @@ /// based on CLA, INS (masked with 0x1F), P1, and P2. use crate::apdu::*; +use crate::applet::AppletVersion; use crate::handlers; use crate::object_store::ObjectStore; pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + // Applet personality (SE050_SIM_APPLET env var; defaults to the + // SE051 / applet 7.2.0 the simulator has always advertised). + let version = AppletVersion::from_env(); + let v7 = version == AppletVersion::V7_2_0; + // SELECT command (CLA=0x00, INS=0xA4) if apdu.cla == 0x00 && apdu.ins == 0xA4 { - return handlers::session::handle_select(apdu, store); + return handlers::session::handle_select(apdu, store, version); } // All other SE050 proprietary commands use CLA=0x80 or 0x84 @@ -47,10 +53,11 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { P1_AES => handlers::aes::handle_write_aes_key(apdu, store), P1_HMAC => handlers::aes::handle_write_hmac_key(apdu, store), P1_CRYPTO_OBJ => handlers::crypto_obj::handle_create(apdu, store), - P1_CURVE => { - // CreateECCurve / SetECCurveParam: our crypto libs have curves built-in - ApduResponse::success() - } + P1_CURVE => match apdu.p2 { + P2_CREATE => handlers::curve::handle_create(apdu, store, version), + P2_PARAM => handlers::curve::handle_set_param(apdu, store), + _ => ApduResponse::error(SW_WRONG_P1P2), + }, P1_BINARY | P1_USERID | P1_COUNTER => { handlers::object_mgmt::handle_write(apdu, store) } @@ -89,7 +96,7 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) } } - _ => ApduResponse::error(SW_FILE_NOT_FOUND), + _ => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), } } (P1_CRYPTO_OBJ, _) => handlers::crypto_obj::handle_list(apdu, store), @@ -105,22 +112,12 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { &[crate::tlv::Tlv::new(crate::tlv::TAG_1, &[cid])]), None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }, - None => ApduResponse::error(SW_FILE_NOT_FOUND), + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), } } - (P1_CURVE, _) => { - // ReadECCurveList: return 17-byte list marking all NIST curves as SET. - // Index = curve_id - 1, value 0x01 = SET, 0x00 = NOT_SET. - let mut curve_list = [0u8; 0x11]; // kSE05x_ECCurve_Total_Weierstrass_Curves - curve_list[0x00] = 0x01; // NIST_P192 - curve_list[0x01] = 0x01; // NIST_P224 - curve_list[0x02] = 0x01; // NIST_P256 - curve_list[0x03] = 0x01; // NIST_P384 - curve_list[0x04] = 0x01; // NIST_P521 - ApduResponse::success_with_tlvs( - &[crate::tlv::Tlv::new(crate::tlv::TAG_1, &curve_list)]) - } - _ => handlers::object_mgmt::handle_read(apdu, store), + (P1_CURVE, P2_LIST) => handlers::curve::handle_list(store), + (P1_CURVE, _) => ApduResponse::error(SW_WRONG_P1P2), + _ => handlers::object_mgmt::handle_read(apdu, store, v7), }, INS_CRYPTO => match (cred_type, apdu.p2) { @@ -163,6 +160,15 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { (P1_DEFAULT, P2_UPDATE) => handlers::digest::handle_digest_update(apdu, store), (P1_DEFAULT, P2_FINAL) => handlers::digest::handle_digest_final(apdu, store), + // MAC (HMAC / AES-CMAC): one-shot and multi-step. + // MACInit uses P2 = Generate (0x03) / Validate (0x44). + (P1_MAC, P2_GENERATE_ONESHOT) => handlers::mac::handle_oneshot(apdu, store, false), + (P1_MAC, P2_VALIDATE_ONESHOT) => handlers::mac::handle_oneshot(apdu, store, true), + (P1_MAC, P2_GENERATE) => handlers::mac::handle_init(apdu, store, false), + (P1_MAC, P2_MAC_VALIDATE) => handlers::mac::handle_init(apdu, store, true), + (P1_MAC, P2_UPDATE) => handlers::mac::handle_update(apdu, store), + (P1_MAC, P2_FINAL) => handlers::mac::handle_final(apdu, store), + _ => ApduResponse::error(SW_WRONG_P1P2), }, @@ -172,9 +178,13 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { (P1_CRYPTO_OBJ, P2_DELETE_OBJECT) => { handlers::crypto_obj::handle_delete(apdu, store) } + // EC curve deletion + (P1_CURVE, P2_DELETE_OBJECT) => { + handlers::curve::handle_delete(apdu, store) + } // General management (_, P2_VERSION) | (_, P2_MEMORY) | (_, P2_RANDOM) | (_, P2_DELETE_ALL) => { - handlers::management::handle(apdu, store) + handlers::management::handle(apdu, store, version) } (_, P2_EXIST) | (_, P2_DELETE_OBJECT) => { handlers::object_mgmt::handle_mgmt(apdu, store) diff --git a/SE050Sim/se050-sim/src/handlers/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index 958a737..eaeb2c3 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -19,15 +19,170 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ +/// AES key management and cipher operations. +/// +/// The cipher mode byte (Tag2 on one-shots, CreateCryptoObject subtype +/// for the multi-step flow) is honored: AES_CBC_NOPAD (0x0D), +/// AES_ECB_NOPAD (0x0E) and AES_CTR (0xF0) are implemented and +/// bench-verified against NIST SP800-38A vectors on SE050C applet +/// 3.1.1 and SE051 applet 7.2.0 (August 2026). Non-block-aligned +/// input to a NOPAD mode fails 0x6985 as on hardware. The padded CBC +/// variants (ISO9797, PKCS5) are not implemented. +/// +/// Multi-step ciphering returns output incrementally: each +/// CipherUpdate emits the ciphertext/plaintext for the block-aligned +/// prefix it can process (bench-verified: 16 bytes in, 16 bytes out), +/// and CipherFinal emits only what remained. + use crate::apdu::*; use crate::object_store::types::SecureObject; use crate::object_store::{CryptoObjectState, ObjectStore}; use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY}; -use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit}; use aes::cipher::generic_array::GenericArray; +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; use rand::RngCore; +pub const CIPHER_MODE_CBC_NOPAD: u8 = 0x0D; // kSE05x_CipherMode_AES_CBC_NOPAD +pub const CIPHER_MODE_ECB_NOPAD: u8 = 0x0E; // kSE05x_CipherMode_AES_ECB_NOPAD +pub const CIPHER_MODE_CTR: u8 = 0xF0; // kSE05x_CipherMode_AES_CTR + +/// AES block cipher over any of the three key sizes. +enum AnyAes { + A128(aes::Aes128), + A192(aes::Aes192), + A256(aes::Aes256), +} + +impl AnyAes { + fn new(key: &[u8]) -> Option { + match key.len() { + 16 => aes::Aes128::new_from_slice(key).ok().map(AnyAes::A128), + 24 => aes::Aes192::new_from_slice(key).ok().map(AnyAes::A192), + 32 => aes::Aes256::new_from_slice(key).ok().map(AnyAes::A256), + _ => None, + } + } + + fn encrypt_block(&self, block: &mut [u8; 16]) { + let ga = GenericArray::from_mut_slice(block); + match self { + AnyAes::A128(c) => c.encrypt_block(ga), + AnyAes::A192(c) => c.encrypt_block(ga), + AnyAes::A256(c) => c.encrypt_block(ga), + } + } + + fn decrypt_block(&self, block: &mut [u8; 16]) { + let ga = GenericArray::from_mut_slice(block); + match self { + AnyAes::A128(c) => c.decrypt_block(ga), + AnyAes::A192(c) => c.decrypt_block(ga), + AnyAes::A256(c) => c.decrypt_block(ga), + } + } +} + +/// Process block-aligned data in ECB mode. +fn ecb_process(cipher: &AnyAes, data: &[u8], encrypting: bool) -> Vec { + let mut out = Vec::with_capacity(data.len()); + for chunk in data.chunks(16) { + let mut block = [0u8; 16]; + block.copy_from_slice(chunk); + if encrypting { + cipher.encrypt_block(&mut block); + } else { + cipher.decrypt_block(&mut block); + } + out.extend_from_slice(&block); + } + out +} + +/// Process block-aligned data in CBC mode, advancing the chain vector. +fn cbc_process(cipher: &AnyAes, chain: &mut [u8; 16], data: &[u8], encrypting: bool) -> Vec { + let mut out = Vec::with_capacity(data.len()); + for chunk in data.chunks(16) { + let mut block = [0u8; 16]; + block.copy_from_slice(chunk); + if encrypting { + for i in 0..16 { + block[i] ^= chain[i]; + } + cipher.encrypt_block(&mut block); + chain.copy_from_slice(&block); + out.extend_from_slice(&block); + } else { + let saved_ct = block; + let mut pt = block; + cipher.decrypt_block(&mut pt); + for i in 0..16 { + pt[i] ^= chain[i]; + } + chain.copy_from_slice(&saved_ct); + out.extend_from_slice(&pt); + } + } + out +} + +/// Process data (any length) in CTR mode, advancing the counter one +/// step per started block. Only the final chunk of a streaming +/// operation may be partial. +fn ctr_process(cipher: &AnyAes, counter: &mut [u8; 16], data: &[u8]) -> Vec { + let mut out = Vec::with_capacity(data.len()); + for chunk in data.chunks(16) { + let mut keystream = *counter; + cipher.encrypt_block(&mut keystream); + for (i, b) in chunk.iter().enumerate() { + out.push(b ^ keystream[i]); + } + // Big-endian increment over the full 16-byte counter block. + for i in (0..16).rev() { + counter[i] = counter[i].wrapping_add(1); + if counter[i] != 0 { + break; + } + } + } + out +} + +/// Apply a cipher mode to `data` in one pass. `chain` is the IV / +/// initial counter and is advanced in place. Returns Err(SW) on an +/// unsupported mode or misaligned NOPAD input. +fn apply_mode( + mode: u8, + key: &[u8], + chain: &mut [u8; 16], + data: &[u8], + encrypting: bool, + is_final: bool, +) -> Result, u16> { + let cipher = AnyAes::new(key).ok_or(SW_CONDITIONS_NOT_SATISFIED)?; + match mode { + CIPHER_MODE_ECB_NOPAD => { + if data.len() % 16 != 0 { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + Ok(ecb_process(&cipher, data, encrypting)) + } + CIPHER_MODE_CBC_NOPAD => { + if data.len() % 16 != 0 { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + Ok(cbc_process(&cipher, chain, data, encrypting)) + } + CIPHER_MODE_CTR => { + if !is_final && data.len() % 16 != 0 { + return Err(SW_CONDITIONS_NOT_SATISFIED); + } + Ok(ctr_process(&cipher, chain, data)) + } + _ => Err(SW_WRONG_DATA), + } +} + /// Handle WRITE AES key command. /// Tag1=obj_id(4B), Tag3=key_data (or Tag3=key_size for generation) pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { @@ -122,10 +277,13 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu } } -/// Handle AES Encrypt Oneshot. -/// INS=Crypto, P1=Cipher, P2=EncryptOneshot -/// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=plaintext, Tag4=IV(opt) -pub fn handle_encrypt_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// 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( + apdu: &ParsedApdu, + store: &mut ObjectStore, + encrypting: bool, +) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), @@ -140,23 +298,28 @@ pub fn handle_encrypt_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apd _ => return ApduResponse::error(SW_WRONG_DATA), }; - let _cipher_mode = match tlv::find_tlv(&tlvs, TAG_2) { + let cipher_mode = match tlv::find_tlv(&tlvs, TAG_2) { Some(t) if !t.value.is_empty() => t.value[0], _ => return ApduResponse::error(SW_WRONG_DATA), }; - let plaintext = match tlv::find_tlv(&tlvs, TAG_3) { + let input = match tlv::find_tlv(&tlvs, TAG_3) { Some(t) => t.value.clone(), None => return ApduResponse::error(SW_WRONG_DATA), }; - let iv = tlv::find_tlv(&tlvs, TAG_4) - .map(|t| t.value.clone()) - .unwrap_or_else(|| vec![0u8; 16]); // Zero IV if not provided + let mut chain = [0u8; 16]; + if let Some(t) = tlv::find_tlv(&tlvs, TAG_4) { + if t.value.len() == 16 { + chain.copy_from_slice(&t.value); + } else if !t.value.is_empty() { + return ApduResponse::error(SW_WRONG_DATA); + } + } let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; let key_data = match &key_obj { @@ -164,78 +327,31 @@ pub fn handle_encrypt_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apd _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; - // AES-CBC encryption - let ciphertext = match key_data.len() { - 16 => aes_cbc_encrypt::(key_data, &iv, &plaintext), - 24 => aes_cbc_encrypt::(key_data, &iv, &plaintext), - 32 => aes_cbc_encrypt::(key_data, &iv, &plaintext), - _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), - }; - - match ciphertext { - Some(ct) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &ct)]), - None => ApduResponse::error(SW_WRONG_DATA), + match apply_mode(cipher_mode, key_data, &mut chain, &input, encrypting, true) { + Ok(out) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &out)]), + Err(sw) => ApduResponse::error(sw), } } +/// Handle AES Encrypt Oneshot. +/// INS=Crypto, P1=Cipher, P2=EncryptOneshot +pub fn handle_encrypt_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + handle_cipher_oneshot(apdu, store, true) +} + /// Handle AES Decrypt Oneshot. /// INS=Crypto, P1=Cipher, P2=DecryptOneshot -/// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=ciphertext, Tag4=IV(opt) pub fn handle_decrypt_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { - let tlvs = match apdu.parse_tlvs() { - Ok(t) => t, - Err(_) => return ApduResponse::error(SW_WRONG_DATA), - }; - - let key_id = match tlv::find_tlv(&tlvs, TAG_1) { - Some(t) if t.value.len() == 4 => { - let mut id = [0u8; 4]; - id.copy_from_slice(&t.value); - id - } - _ => return ApduResponse::error(SW_WRONG_DATA), - }; - - let _cipher_mode = match tlv::find_tlv(&tlvs, TAG_2) { - Some(t) if !t.value.is_empty() => t.value[0], - _ => return ApduResponse::error(SW_WRONG_DATA), - }; - - let ciphertext = match tlv::find_tlv(&tlvs, TAG_3) { - Some(t) => t.value.clone(), - None => return ApduResponse::error(SW_WRONG_DATA), - }; - - let iv = tlv::find_tlv(&tlvs, TAG_4) - .map(|t| t.value.clone()) - .unwrap_or_else(|| vec![0u8; 16]); - - let key_obj = match store.get(&key_id) { - Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), - }; - - let key_data = match &key_obj { - SecureObject::AESKey { key } => key, - _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), - }; - - let plaintext = match key_data.len() { - 16 => aes_cbc_decrypt::(key_data, &iv, &ciphertext), - 24 => aes_cbc_decrypt::(key_data, &iv, &ciphertext), - 32 => aes_cbc_decrypt::(key_data, &iv, &ciphertext), - _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), - }; - - match plaintext { - Some(pt) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &pt)]), - None => ApduResponse::error(SW_WRONG_DATA), - } + handle_cipher_oneshot(apdu, store, false) } /// Handle CipherInit (encrypt or decrypt). /// INS=Crypto, P1=Cipher, P2=EncryptInit(0x42)/DecryptInit(0x43) /// Tag1=keyObjectID(4B), Tag2=cryptoObjectID(2B), Tag4=IV(opt) +/// +/// The crypto object must have been created with CreateCryptoObject +/// (context CIPHER); its subtype selects the cipher mode. Init on a +/// never-created crypto object fails 0x6985, as bench-verified. pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let encrypting = apdu.p2 == P2_ENCRYPT_INIT; let tlvs = match apdu.parse_tlvs() { @@ -257,17 +373,34 @@ pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes _ => return ApduResponse::error(SW_WRONG_DATA), }; - let iv = tlv::find_tlv(&tlvs, TAG_4) - .map(|t| t.value.clone()) - .unwrap_or_else(|| vec![0u8; 16]); + let Some(&(context, subtype)) = store.crypto_object_types.get(&crypto_id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + // kSE05x_CryptoContext_CIPHER + if context != 0x02 { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + if !store.exists(&key_id) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + + let mut chain = vec![0u8; 16]; + if let Some(t) = tlv::find_tlv(&tlvs, TAG_4) { + if t.value.len() == 16 { + chain.copy_from_slice(&t.value); + } else if !t.value.is_empty() { + return ApduResponse::error(SW_WRONG_DATA); + } + } store.crypto_objects.insert( crypto_id, CryptoObjectState::Cipher { encrypting, + mode: subtype, key_id, - iv, - accumulated: Vec::new(), + chain, + pending: Vec::new(), }, ); @@ -277,6 +410,10 @@ pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes /// Handle CipherUpdate. /// INS=Crypto, P1=Cipher, P2=Update(0x0C) /// Tag2=cryptoObjectID(2B), Tag3=inputData +/// +/// Emits the output for every complete block available, holding back +/// only the sub-block remainder (bench-verified: each aligned update +/// returns its ciphertext immediately). pub fn handle_cipher_update(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -293,21 +430,51 @@ pub fn handle_cipher_update(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR None => return ApduResponse::error(SW_WRONG_DATA), }; - // Accumulate data - process in final - match store.crypto_objects.get_mut(&crypto_id) { - Some(CryptoObjectState::Cipher { accumulated, .. }) => { - accumulated.extend_from_slice(&input); - // For streaming cipher, we could process block-aligned chunks here. - // For simplicity, accumulate all and process in final. - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &[])]) - } - _ => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), - } + let Some(CryptoObjectState::Cipher { encrypting, mode, key_id, chain, pending }) = + store.crypto_objects.get(&crypto_id).cloned() + else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + + let key = match store.get(&key_id) { + Some(SecureObject::AESKey { key }) => key.clone(), + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + + let mut buffered = pending; + buffered.extend_from_slice(&input); + let aligned_len = buffered.len() - (buffered.len() % 16); + let (aligned, rest) = buffered.split_at(aligned_len); + + let mut chain_arr = [0u8; 16]; + chain_arr.copy_from_slice(&chain); + let output = match apply_mode(mode, &key, &mut chain_arr, aligned, encrypting, false) { + Ok(out) => out, + Err(sw) => return ApduResponse::error(sw), + }; + + let rest = rest.to_vec(); + store.crypto_objects.insert( + crypto_id, + CryptoObjectState::Cipher { + encrypting, + mode, + key_id, + chain: chain_arr.to_vec(), + pending: rest, + }, + ); + + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &output)]) } /// Handle CipherFinal. /// INS=Crypto, P1=Cipher, P2=Final(0x0D) /// Tag2=cryptoObjectID(2B), Tag3=remainingData(opt) +/// +/// Processes what was still buffered plus the final chunk. NOPAD +/// modes require the total to be block-aligned (0x6985 otherwise); +/// a fully drained stream returns zero bytes, as on hardware. pub fn handle_cipher_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -326,105 +493,204 @@ pub fn handle_cipher_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRe None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; - match state { - CryptoObjectState::Cipher { - encrypting, - key_id, - iv, - mut accumulated, - } => { - if let Some(rem) = remaining { - accumulated.extend_from_slice(&rem); - } + let CryptoObjectState::Cipher { encrypting, mode, key_id, chain, mut pending } = state else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; - let key_obj = match store.get(&key_id) { - Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), - }; - - let key_data = match &key_obj { - SecureObject::AESKey { key } => key, - _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), - }; - - let result = if encrypting { - match key_data.len() { - 16 => aes_cbc_encrypt::(key_data, &iv, &accumulated), - 24 => aes_cbc_encrypt::(key_data, &iv, &accumulated), - 32 => aes_cbc_encrypt::(key_data, &iv, &accumulated), - _ => None, - } - } else { - match key_data.len() { - 16 => aes_cbc_decrypt::(key_data, &iv, &accumulated), - 24 => aes_cbc_decrypt::(key_data, &iv, &accumulated), - 32 => aes_cbc_decrypt::(key_data, &iv, &accumulated), - _ => None, - } - }; - - match result { - Some(output) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &output)]), - None => ApduResponse::error(SW_WRONG_DATA), - } - } - _ => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + if let Some(rem) = remaining { + pending.extend_from_slice(&rem); + } + + let key = match store.get(&key_id) { + Some(SecureObject::AESKey { key }) => key.clone(), + _ => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + + let mut chain_arr = [0u8; 16]; + chain_arr.copy_from_slice(&chain); + match apply_mode(mode, &key, &mut chain_arr, &pending, encrypting, true) { + Ok(out) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &out)]), + Err(sw) => ApduResponse::error(sw), } } -/// AES-CBC encrypt with no padding (manual CBC chaining). -pub fn aes_cbc_encrypt(key: &[u8], iv: &[u8], plaintext: &[u8]) -> Option> -where - C: BlockEncrypt + KeyInit, -{ - if plaintext.len() % 16 != 0 || iv.len() < 16 { - return None; +#[cfg(test)] +mod cipher_mode_tests { + use super::*; + + // NIST SP800-38A AES-128 vectors, bench-verified byte-for-byte on + // SE050C applet 3.1.1 and SE051 applet 7.2.0. + const KEY: [u8; 16] = [ + 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, + 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C, + ]; + const PT: [u8; 32] = [ + 0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, + 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93, 0x17, 0x2A, + 0xAE, 0x2D, 0x8A, 0x57, 0x1E, 0x03, 0xAC, 0x9C, + 0x9E, 0xB7, 0x6F, 0xAC, 0x45, 0xAF, 0x8E, 0x51, + ]; + const CBC_IV: [u8; 16] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + ]; + const CTR_IV: [u8; 16] = [ + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF, + ]; + const ECB_CT: &str = "3ad77bb40d7a3660a89ecaf32466ef97f5d3d58503b9699de785895a96fdbaaf"; + const CBC_CT: &str = "7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2"; + const CTR_CT: &str = "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff"; + + const KEY_ID: [u8; 4] = [0, 0, 0, 0x50]; + + fn store_with_key() -> ObjectStore { + let mut store = ObjectStore::new(); + store.insert(KEY_ID, SecureObject::AESKey { key: KEY.to_vec() }); + store } - let cipher = C::new_from_slice(key).ok()?; - let mut result = Vec::with_capacity(plaintext.len()); - let mut prev_block = [0u8; 16]; - prev_block.copy_from_slice(&iv[..16]); - for chunk in plaintext.chunks(16) { - let mut block = [0u8; 16]; - for i in 0..16 { - block[i] = chunk[i] ^ prev_block[i]; + fn oneshot_apdu(mode: u8, input: &[u8], iv: Option<&[u8]>, p2: u8) -> ParsedApdu { + let mut data = vec![TAG_1, 0x04]; + data.extend_from_slice(&KEY_ID); + data.extend_from_slice(&[TAG_2, 0x01, mode]); + data.push(TAG_3); + data.push(input.len() as u8); + data.extend_from_slice(input); + if let Some(iv) = iv { + data.push(TAG_4); + data.push(iv.len() as u8); + data.extend_from_slice(iv); } - let ga = GenericArray::from_mut_slice(&mut block); - cipher.encrypt_block(ga); - prev_block.copy_from_slice(&block); - result.extend_from_slice(&block); + ParsedApdu { cla: 0x80, ins: INS_CRYPTO, p1: P1_CIPHER, p2, data, le: None } } - Some(result) -} + fn output_of(resp: &ApduResponse) -> Vec { + assert_eq!(resp.sw, 0x9000, "SW {:04x}", resp.sw); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + tlv::find_tlv(&tlvs, TAG_1).unwrap().value.clone() + } -/// AES-CBC decrypt with no padding (manual CBC chaining). -fn aes_cbc_decrypt(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Option> -where - C: BlockDecrypt + KeyInit, -{ - if ciphertext.len() % 16 != 0 || iv.len() < 16 { - return None; + #[test] + fn test_oneshot_modes_match_nist_vectors() { + let mut store = store_with_key(); + let ecb = handle_encrypt_oneshot( + &oneshot_apdu(CIPHER_MODE_ECB_NOPAD, &PT, None, P2_ENCRYPT_ONESHOT), &mut store); + assert_eq!(hex::encode(output_of(&ecb)), ECB_CT); + let cbc = handle_encrypt_oneshot( + &oneshot_apdu(CIPHER_MODE_CBC_NOPAD, &PT, Some(&CBC_IV), P2_ENCRYPT_ONESHOT), + &mut store); + assert_eq!(hex::encode(output_of(&cbc)), CBC_CT); + let ctr = handle_encrypt_oneshot( + &oneshot_apdu(CIPHER_MODE_CTR, &PT, Some(&CTR_IV), P2_ENCRYPT_ONESHOT), &mut store); + assert_eq!(hex::encode(output_of(&ctr)), CTR_CT); } - let cipher = C::new_from_slice(key).ok()?; - let mut result = Vec::with_capacity(ciphertext.len()); - let mut prev_block = [0u8; 16]; - prev_block.copy_from_slice(&iv[..16]); - for chunk in ciphertext.chunks(16) { - let mut block = [0u8; 16]; - block.copy_from_slice(chunk); - let ga = GenericArray::from_mut_slice(&mut block); - cipher.decrypt_block(ga); - for i in 0..16 { - block[i] ^= prev_block[i]; + #[test] + fn test_oneshot_decrypt_round_trips() { + let mut store = store_with_key(); + for (mode, iv) in [ + (CIPHER_MODE_ECB_NOPAD, None), + (CIPHER_MODE_CBC_NOPAD, Some(&CBC_IV[..])), + (CIPHER_MODE_CTR, Some(&CTR_IV[..])), + ] { + let enc = handle_encrypt_oneshot( + &oneshot_apdu(mode, &PT, iv, P2_ENCRYPT_ONESHOT), &mut store); + let ct = output_of(&enc); + let dec = handle_decrypt_oneshot( + &oneshot_apdu(mode, &ct, iv, P2_DECRYPT_ONESHOT), &mut store); + assert_eq!(output_of(&dec), PT.to_vec(), "mode {:02x}", mode); + } + } + + #[test] + fn test_oneshot_unaligned_nopad_fails_6985() { + // Bench-verified: 20 bytes into CBC_NOPAD -> 0x6985. + let mut store = store_with_key(); + let resp = handle_encrypt_oneshot( + &oneshot_apdu(CIPHER_MODE_CBC_NOPAD, &PT[..20], Some(&CBC_IV), P2_ENCRYPT_ONESHOT), + &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_oneshot_unknown_mode_fails() { + let mut store = store_with_key(); + let resp = handle_encrypt_oneshot( + &oneshot_apdu(0x18 /* AES_CBC_PKCS5, unimplemented */, &PT, None, + P2_ENCRYPT_ONESHOT), + &mut store); + assert_eq!(resp.sw, SW_WRONG_DATA); + } + + #[test] + fn test_oneshot_missing_key_fails_6985() { + // Bench-verified SW for operations on missing objects. + let mut store = ObjectStore::new(); + let resp = handle_encrypt_oneshot( + &oneshot_apdu(CIPHER_MODE_ECB_NOPAD, &PT, None, P2_ENCRYPT_ONESHOT), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + fn init_apdu(crypto_id: u16, iv: Option<&[u8]>) -> ParsedApdu { + let mut data = vec![TAG_1, 0x04]; + data.extend_from_slice(&KEY_ID); + data.extend_from_slice(&[TAG_2, 0x02, (crypto_id >> 8) as u8, crypto_id as u8]); + if let Some(iv) = iv { + data.push(TAG_4); + data.push(iv.len() as u8); + data.extend_from_slice(iv); + } + ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_CIPHER, p2: P2_ENCRYPT_INIT, data, le: None, } - prev_block.copy_from_slice(chunk); - result.extend_from_slice(&block); } - Some(result) + fn update_apdu(crypto_id: u16, input: &[u8]) -> ParsedApdu { + let mut data = vec![TAG_2, 0x02, (crypto_id >> 8) as u8, crypto_id as u8]; + data.push(TAG_3); + data.push(input.len() as u8); + data.extend_from_slice(input); + ParsedApdu { cla: 0x80, ins: INS_CRYPTO, p1: P1_CIPHER, p2: P2_UPDATE, data, le: None } + } + + fn final_apdu(crypto_id: u16, input: &[u8]) -> ParsedApdu { + let mut data = vec![TAG_2, 0x02, (crypto_id >> 8) as u8, crypto_id as u8]; + if !input.is_empty() { + data.push(TAG_3); + data.push(input.len() as u8); + data.extend_from_slice(input); + } + ParsedApdu { cla: 0x80, ins: INS_CRYPTO, p1: P1_CIPHER, p2: P2_FINAL, data, le: None } + } + + #[test] + fn test_streaming_cbc_emits_output_per_update() { + // Bench-verified: CipherUpdate(16B) returns the 16-byte + // ciphertext block immediately, and Final(0B) returns nothing. + let crypto_id = 0x0010u16; + let mut store = store_with_key(); + store.crypto_object_types.insert(crypto_id, (0x02, CIPHER_MODE_CBC_NOPAD)); + + assert_eq!(handle_cipher_init(&init_apdu(crypto_id, Some(&CBC_IV)), &mut store).sw, + 0x9000); + let u1 = handle_cipher_update(&update_apdu(crypto_id, &PT[..16]), &mut store); + assert_eq!(hex::encode(output_of(&u1)), CBC_CT[..32]); + let u2 = handle_cipher_update(&update_apdu(crypto_id, &PT[16..]), &mut store); + assert_eq!(hex::encode(output_of(&u2)), CBC_CT[32..]); + let f = handle_cipher_final(&final_apdu(crypto_id, &[]), &mut store); + assert_eq!(output_of(&f).len(), 0); + } + + #[test] + fn test_streaming_init_without_created_crypto_object_fails() { + // Bench-verified: 0x6985 on a never-created crypto object. + let mut store = store_with_key(); + let resp = handle_cipher_init(&init_apdu(0x0777, Some(&CBC_IV)), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + let resp = handle_cipher_update(&update_apdu(0x0778, &PT[..16]), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } } #[cfg(test)] diff --git a/SE050Sim/se050-sim/src/handlers/crypto_obj.rs b/SE050Sim/se050-sim/src/handlers/crypto_obj.rs index 8e527f5..680dfcc 100644 --- a/SE050Sim/se050-sim/src/handlers/crypto_obj.rs +++ b/SE050Sim/se050-sim/src/handlers/crypto_obj.rs @@ -47,6 +47,12 @@ pub fn handle_create(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse _ => 0x00, }; + // Re-creating an existing crypto object fails 0x6986 + // (bench-verified on applet 3.1.1 and 7.2.0). + if store.crypto_object_types.contains_key(&crypto_id) { + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } + store.crypto_object_types.insert(crypto_id, (context_type, subtype)); ApduResponse::success() } diff --git a/SE050Sim/se050-sim/src/handlers/curve.rs b/SE050Sim/se050-sim/src/handlers/curve.rs new file mode 100644 index 0000000..ba0424b --- /dev/null +++ b/SE050Sim/se050-sim/src/handlers/curve.rs @@ -0,0 +1,244 @@ +/* curve.rs + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of SE050Sim. + * + * SE050Sim is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * SE050Sim is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/// EC curve object management: CreateECCurve, SetECCurveParam, +/// DeleteECCurve, ReadECCurveList. +/// +/// Weierstrass curves are dynamic objects on real applets: they must +/// be created and have all five parameters (A, B, G, N, PRIME) +/// uploaded before key operations on them succeed, and this state +/// persists across sessions. Bench-verified behaviors this module +/// models (SE050C applet 3.1.1 + SE051 applet 7.2.0, August 2026): +/// +/// * Key generation on a missing or parameter-less curve fails 0x6985. +/// * A created but parameter-less curve still shows as SET (0x02) in +/// ReadECCurveList -- the list tracks existence, not usability. +/// * CreateECCurve on an existing curve: applet 7.2 refuses 0x6985; +/// applet 3.1.1 returns 0x9000 and silently resets the curve to the +/// parameter-less state (wiping a provisioned curve!). +/// * ReadECCurveList entries are 0x02 = SET / 0x01 = NOT_SET +/// (kSE05x_SetIndicator values). + +use crate::apdu::*; +use crate::applet::AppletVersion; +use crate::object_store::ObjectStore; +use crate::tlv::{self, Tlv, TAG_1, TAG_2}; + +/// Number of entries in the ReadECCurveList response +/// (kSE05x_ECCurve_Total_Weierstrass_Curves). +const WEIERSTRASS_CURVE_COUNT: u8 = 0x11; + +fn curve_id_from_tag1(apdu: &ParsedApdu) -> Option { + let tlvs = apdu.parse_tlvs().ok()?; + let t = tlv::find_tlv(&tlvs, TAG_1)?; + if t.value.len() == 1 { + Some(t.value[0]) + } else { + None + } +} + +/// CreateECCurve: INS_WRITE, P1_CURVE, P2_CREATE, Tag1=curve id (1B). +pub fn handle_create( + apdu: &ParsedApdu, + store: &mut ObjectStore, + version: AppletVersion, +) -> ApduResponse { + let Some(curve_id) = curve_id_from_tag1(apdu) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if curve_id == 0 || curve_id > WEIERSTRASS_CURVE_COUNT { + return ApduResponse::error(SW_WRONG_DATA); + } + if store.curve_exists(curve_id) { + return match version { + // Bench-verified on the SE051: re-creating an existing + // curve is refused and the curve is left intact. + AppletVersion::V7_2_0 => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + // Bench-verified on the SE050C: the duplicate create is + // accepted and resets the curve to param-less, so key + // generation on it fails until the parameters are + // uploaded again. + AppletVersion::V3_1_1 => { + store.curve_reset(curve_id); + ApduResponse::success() + } + }; + } + store.curve_create(curve_id); + ApduResponse::success() +} + +/// SetECCurveParam: INS_WRITE, P1_CURVE, P2_PARAM, +/// Tag1=curve id (1B), Tag2=param type (1B bit), Tag3=param value. +/// The parameter values themselves are not stored -- the simulator's +/// crypto backends carry the standard NIST constants -- only the +/// completeness bitmask matters. +pub fn handle_set_param(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + let curve_id = match tlv::find_tlv(&tlvs, TAG_1) { + Some(t) if t.value.len() == 1 => t.value[0], + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let param = match tlv::find_tlv(&tlvs, TAG_2) { + Some(t) if t.value.len() == 1 => t.value[0], + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + if !matches!(param, 0x01 | 0x02 | 0x04 | 0x08 | 0x10) { + return ApduResponse::error(SW_WRONG_DATA); + } + if !store.curve_exists(curve_id) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + store.curve_add_param(curve_id, param); + ApduResponse::success() +} + +/// DeleteECCurve: INS_MGMT, P1_CURVE, P2_DELETE_OBJECT, Tag1=curve id. +pub fn handle_delete(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let Some(curve_id) = curve_id_from_tag1(apdu) else { + return ApduResponse::error(SW_WRONG_DATA); + }; + if store.curve_delete(curve_id) { + ApduResponse::success() + } else { + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) + } +} + +/// ReadECCurveList: INS_READ, P1_CURVE, P2_LIST. Response Tag1 holds +/// one byte per Weierstrass curve ID 0x01..=0x11: 0x02 if the curve +/// object exists (parameterized or not), 0x01 otherwise. +pub fn handle_list(store: &ObjectStore) -> ApduResponse { + let list: Vec = (1..=WEIERSTRASS_CURVE_COUNT) + .map(|id| if store.curve_exists(id) { 0x02 } else { 0x01 }) + .collect(); + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &list)]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_apdu(curve_id: u8) -> ParsedApdu { + ParsedApdu { + cla: 0x80, + ins: INS_WRITE, + p1: P1_CURVE, + p2: P2_CREATE, + data: vec![TAG_1, 0x01, curve_id], + le: None, + } + } + + fn param_apdu(curve_id: u8, param: u8) -> ParsedApdu { + ParsedApdu { + cla: 0x80, + ins: INS_WRITE, + p1: P1_CURVE, + p2: P2_PARAM, + data: vec![TAG_1, 0x01, curve_id, TAG_2, 0x01, param, TAG_3_LOCAL, 0x01, 0xAA], + le: None, + } + } + + const TAG_3_LOCAL: u8 = 0x43; + + #[test] + fn test_duplicate_create_is_version_dependent() { + // SE051 7.2.0 refuses; SE050C 3.1.1 accepts and wipes params + // (both bench-verified -- the 3.1.1 wipe broke a provisioned + // P-256 curve during the August 2026 session). + let mut store = ObjectStore::new(); // P-256 (0x03) provisioned + assert!(store.curve_ready(0x03)); + + let resp = handle_create(&create_apdu(0x03), &mut store, AppletVersion::V7_2_0); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.curve_ready(0x03), "7.2 dup create must not touch the curve"); + + let resp = handle_create(&create_apdu(0x03), &mut store, AppletVersion::V3_1_1); + assert_eq!(resp.sw, 0x9000); + assert!(store.curve_exists(0x03)); + assert!(!store.curve_ready(0x03), "3.1.1 dup create wipes the params"); + } + + #[test] + fn test_param_upload_completes_curve() { + let mut store = ObjectStore::new(); + store.curve_delete(0x05); + let resp = handle_create(&create_apdu(0x05), &mut store, AppletVersion::V7_2_0); + assert_eq!(resp.sw, 0x9000); + assert!(!store.curve_ready(0x05)); + for param in [0x01, 0x02, 0x04, 0x08, 0x10] { + let resp = handle_set_param(¶m_apdu(0x05, param), &mut store); + assert_eq!(resp.sw, 0x9000); + } + assert!(store.curve_ready(0x05)); + } + + #[test] + fn test_set_param_on_missing_curve_fails() { + let mut store = ObjectStore::new(); + store.curve_delete(0x05); + let resp = handle_set_param(¶m_apdu(0x05, 0x01), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_delete_missing_curve_fails() { + let mut store = ObjectStore::new(); + store.curve_delete(0x05); + let apdu = ParsedApdu { + cla: 0x80, + ins: INS_MGMT, + p1: P1_CURVE, + p2: P2_DELETE_OBJECT, + data: vec![TAG_1, 0x01, 0x05], + le: None, + }; + let resp = handle_delete(&apdu, &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_list_reflects_state_with_set_indicator_values() { + // 0x02 = SET, 0x01 = NOT_SET (kSE05x_SetIndicator). A created + // but param-less curve still lists as SET, as on hardware. + let mut store = ObjectStore::new(); + store.curve_delete(0x01); + store.curve_delete(0x05); + store.curve_create(0x05); // param-less + + let resp = handle_list(&store); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + let list = &tlv::find_tlv(&tlvs, TAG_1).unwrap().value; + assert_eq!(list.len(), 0x11); + assert_eq!(list[0], 0x01, "P-192 deleted -> NOT_SET"); + assert_eq!(list[1], 0x02, "P-224 default-provisioned -> SET"); + assert_eq!(list[2], 0x02, "P-256 default-provisioned -> SET"); + assert_eq!(list[4], 0x02, "param-less P-521 still lists as SET"); + assert_eq!(list[5], 0x01, "brainpool never created -> NOT_SET"); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/digest.rs b/SE050Sim/se050-sim/src/handlers/digest.rs index 6545166..8227a21 100644 --- a/SE050Sim/se050-sim/src/handlers/digest.rs +++ b/SE050Sim/se050-sim/src/handlers/digest.rs @@ -37,7 +37,8 @@ fn compute_hash(mode: u8, data: &[u8]) -> Option> { /// Handle Digest OneShot command. /// INS=Crypto, P1=Default, P2=Oneshot -/// Tag1=digest_mode(1B), Tag2=data_to_hash +/// Tag1=digest_mode(1B), Tag2=data_to_hash (optional/empty: real +/// applets hash the empty message, bench-verified on 3.1.1 and 7.2.0) pub fn handle_digest_oneshot(apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -49,12 +50,11 @@ pub fn handle_digest_oneshot(apdu: &ParsedApdu, _store: &mut ObjectStore) -> Apd _ => return ApduResponse::error(SW_WRONG_DATA), }; - let data = match tlv::find_tlv(&tlvs, TAG_2) { - Some(t) => &t.value, - None => return ApduResponse::error(SW_WRONG_DATA), - }; + let data = tlv::find_tlv(&tlvs, TAG_2) + .map(|t| t.value.clone()) + .unwrap_or_default(); - match compute_hash(digest_mode, data) { + match compute_hash(digest_mode, &data) { Some(hash) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &hash)]), None => ApduResponse::error(SW_WRONG_DATA), } @@ -62,27 +62,33 @@ pub fn handle_digest_oneshot(apdu: &ParsedApdu, _store: &mut ObjectStore) -> Apd /// Handle DigestInit. /// INS=Crypto, P1=Default, P2=Init(0x0B) -/// Tag1=digest_mode(1B), Tag2=cryptoObjectID(2B) +/// Tag2=cryptoObjectID(2B). The digest algorithm comes from the +/// crypto object's CreateCryptoObject subtype; the SDK sends no algo +/// TLV here. Init on a never-created crypto object fails 0x6985 +/// (bench-verified on applet 3.1.1 and 7.2.0). pub fn handle_digest_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), }; - let algo = match tlv::find_tlv(&tlvs, TAG_1) { - Some(t) if !t.value.is_empty() => t.value[0], - _ => return ApduResponse::error(SW_WRONG_DATA), - }; - let crypto_id = match tlv::find_tlv(&tlvs, TAG_2) { Some(t) if t.value.len() == 2 => ((t.value[0] as u16) << 8) | (t.value[1] as u16), _ => return ApduResponse::error(SW_WRONG_DATA), }; + let Some(&(context, subtype)) = store.crypto_object_types.get(&crypto_id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + // kSE05x_CryptoContext_DIGEST + if context != 0x01 { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + store.crypto_objects.insert( crypto_id, CryptoObjectState::Digest { - algo, + algo: subtype, data: Vec::new(), }, ); diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index dde6aef..515f395 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -24,10 +24,14 @@ use crate::object_store::types::{ECCurve, SecureObject}; use crate::object_store::ObjectStore; use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_5, TAG_7}; -use ecdsa::signature::{Signer, Verifier}; use ecdsa::signature::hazmat::{PrehashSigner, PrehashVerifier}; use rand::rngs::OsRng; -use sha2::Digest; + +// p192 0.13 ships no SecretKey/SigningKey conveniences (verification +// only); the generic elliptic-curve types work with its arithmetic. +use elliptic_curve::sec1::ToEncodedPoint; +type P192SecretKey = elliptic_curve::SecretKey; +type P192PublicKey = elliptic_curve::PublicKey; /// Pad a hash to the curve's scalar size (right-pad with zeros). /// ECDSA requires the hash to be at least as long as the curve order. @@ -61,14 +65,22 @@ pub fn handle_write_ec_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRe }; // Extract curve from Tag2 - let curve = match tlv::find_tlv(&tlvs, TAG_2) { + let (curve_byte, curve) = match tlv::find_tlv(&tlvs, TAG_2) { Some(t) if !t.value.is_empty() => match ECCurve::from_se050_byte(t.value[0]) { - Some(c) => c, + Some(c) => (t.value[0], c), None => return ApduResponse::error(SW_WRONG_DATA), }, _ => return ApduResponse::error(SW_WRONG_DATA), }; + // Weierstrass curves must exist as fully parameterized curve + // objects before any key can be created on them; key generation on + // a missing or param-less curve fails 0x6985 (bench-verified on + // applet 3.1.1 and 7.2.0). The 25519 curves are chip constants. + if curve.needs_curve_object() && !store.curve_ready(curve_byte) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + // 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) @@ -78,9 +90,11 @@ pub fn handle_write_ec_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRe if apdu.key_type() == P1_KEY_PAIR && private_key_data.is_none() { // Generate a new key pair match curve { + ECCurve::NistP192 => generate_p192_keypair(obj_id, store), ECCurve::NistP224 => generate_p224_keypair(obj_id, store), ECCurve::NistP256 => generate_p256_keypair(obj_id, store), ECCurve::NistP384 => generate_p384_keypair(obj_id, store), + ECCurve::NistP521 => generate_p521_keypair(obj_id, store), ECCurve::Ed25519 => generate_ed25519_keypair(obj_id, store), ECCurve::Curve25519 => generate_x25519_keypair(obj_id, store), } @@ -103,6 +117,28 @@ pub fn handle_write_ec_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRe } } +fn generate_p192_keypair(obj_id: [u8; 4], store: &mut ObjectStore) -> ApduResponse { + let sk = P192SecretKey::random(&mut OsRng); + let pk = sk.public_key(); + store.insert(obj_id, SecureObject::ECKeyPair { + curve: ECCurve::NistP192, + private_key: sk.to_bytes().to_vec(), + public_key: pk.to_encoded_point(false).as_bytes().to_vec(), + }); + ApduResponse::success() +} + +fn generate_p521_keypair(obj_id: [u8; 4], store: &mut ObjectStore) -> ApduResponse { + let sk = p521::ecdsa::SigningKey::random(&mut OsRng); + let pk = p521::ecdsa::VerifyingKey::from(&sk); + store.insert(obj_id, SecureObject::ECKeyPair { + curve: ECCurve::NistP521, + private_key: sk.to_bytes().to_vec(), + public_key: pk.to_encoded_point(false).as_bytes().to_vec(), + }); + ApduResponse::success() +} + fn generate_p224_keypair(obj_id: [u8; 4], store: &mut ObjectStore) -> ApduResponse { let sk = p224::ecdsa::SigningKey::random(&mut OsRng); let pk = sk.verifying_key(); @@ -197,6 +233,19 @@ fn import_ec_key( .to_bytes() .to_vec() } + ECCurve::NistP192 if private_key_data.len() == 24 => { + match P192SecretKey::from_bytes(private_key_data.into()) { + Ok(sk) => sk.public_key().to_encoded_point(false).as_bytes().to_vec(), + Err(_) => vec![], + } + } + ECCurve::NistP521 if private_key_data.len() == 66 => { + match p521::ecdsa::SigningKey::from_bytes(private_key_data.into()) { + Ok(sk) => p521::ecdsa::VerifyingKey::from(&sk) + .to_encoded_point(false).as_bytes().to_vec(), + Err(_) => vec![], + } + } ECCurve::NistP224 if private_key_data.len() == 28 => { match p224::ecdsa::SigningKey::from_bytes(private_key_data.into()) { Ok(sk) => sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(), @@ -239,6 +288,87 @@ fn import_ec_key( ApduResponse::success() } +// kSE05x_ECSignatureAlgo digest sizes. The applet requires the signed +// input to be exactly the digest length of the selected algorithm +// (bench-verified on applet 3.1.1 and 7.2.0: SHA256 with 20 bytes and +// SHA512 with 32 bytes both fail 0x6985, SHA1 with 20 and SHA512 with +// 64 succeed). PLAIN (0x09) takes a raw value at curve scalar size. +fn ecdsa_algo_digest_len(algo: u8) -> Option { + match algo { + 0x11 => Some(20), // SHA1 + 0x25 => Some(28), // SHA224 + 0x21 => Some(32), // SHA256 + 0x22 => Some(48), // SHA384 + 0x26 => Some(64), // SHA512 + _ => None, + } +} + +/// Validate the (algo, input length) pairing for a Weierstrass ECDSA +/// operation. Returns Some(error) when the request must be refused. +fn check_ecdsa_algo(algo: u8, input_len: usize, scalar_len: usize) -> Option { + match ecdsa_algo_digest_len(algo) { + Some(dlen) if input_len == dlen => None, + Some(_) => Some(ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED)), + None if algo == 0x09 /* PLAIN */ => { + if input_len == scalar_len { + None + } else { + Some(ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED)) + } + } + None => Some(ApduResponse::error(SW_WRONG_DATA)), + } +} + +/// Raw ECDSA signing over p192's arithmetic. The p192 crate only +/// implements the verify primitive, so the textbook signing equation +/// s = k^-1 (e + r d) is computed here with a random per-signature k; +/// output correctness is checked against p192's own VerifyingKey in +/// the unit tests. +fn p192_sign(private_key: &[u8], data: &[u8]) -> ApduResponse { + use elliptic_curve::ops::Reduce; + use elliptic_curve::point::AffineCoordinates; + use elliptic_curve::Field; + + let Ok(sk) = P192SecretKey::from_bytes(private_key.into()) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let d = *sk.to_nonzero_scalar(); + let hash = pad_hash(data, 24); + let e = >::reduce_bytes( + p192::FieldBytes::from_slice(&hash)); + + for _ in 0..64 { + let k = p192::Scalar::random(&mut OsRng); + let Some(k_inv) = Option::::from(k.invert()) else { continue }; + let r_point = (p192::ProjectivePoint::GENERATOR * k).to_affine(); + let r = >::reduce_bytes( + &r_point.x()); + if bool::from(r.is_zero()) { + continue; + } + let s = k_inv * (e + r * d); + if bool::from(s.is_zero()) { + continue; + } + let Ok(sig) = p192::ecdsa::Signature::from_scalars(r, s) else { continue }; + let der = sig.to_der(); + return ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, der.as_bytes())]); + } + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) +} +fn p521_sign(private_key: &[u8], data: &[u8]) -> ApduResponse { + let Ok(sk) = p521::ecdsa::SigningKey::from_bytes(private_key.into()) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + let hash = pad_hash(data, 66); + let sig: Result = sk.sign_prehash(&hash); + let Ok(sig) = sig else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) }; + let der = sig.to_der(); + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, der.as_bytes())]) +} + fn p224_sign(private_key: &[u8], data: &[u8]) -> ApduResponse { let Ok(sk) = p224::ecdsa::SigningKey::from_bytes(private_key.into()) else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); @@ -270,6 +400,18 @@ fn p384_sign(private_key: &[u8], data: &[u8]) -> ApduResponse { ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, der.as_bytes())]) } +fn p192_verify(private_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { + let Ok(sk) = P192SecretKey::from_bytes(private_key.into()) else { return false }; + let pub_point = sk.public_key().to_encoded_point(false); + p192_verify_pubkey(pub_point.as_bytes(), data, sig_data) +} +fn p521_verify(private_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { + let Ok(sk) = p521::ecdsa::SigningKey::from_bytes(private_key.into()) else { return false }; + let vk = p521::ecdsa::VerifyingKey::from(&sk); + let Ok(sig) = p521::ecdsa::Signature::from_der(sig_data) else { return false }; + let hash = pad_hash(data, 66); + vk.verify_prehash(&hash, &sig).is_ok() +} fn p224_verify(private_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { let Ok(sk) = p224::ecdsa::SigningKey::from_bytes(private_key.into()) else { return false }; let vk = sk.verifying_key(); @@ -322,10 +464,24 @@ pub fn handle_sign(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + // Missing objects fail 0x6985 on real applets (bench-verified + // on 3.1.1 and 7.2.0), not 0x6A82. + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; + // Weierstrass ECDSA enforces input length == algo digest length. + if let SecureObject::ECKeyPair { curve, .. } = &key_obj { + if curve.needs_curve_object() { + if let Some(err) = check_ecdsa_algo(algo, input_data.len(), curve.scalar_len()) { + return err; + } + } + } + match &key_obj { + SecureObject::ECKeyPair { curve: ECCurve::NistP192, private_key, .. } => { + p192_sign(private_key, &input_data) + } SecureObject::ECKeyPair { curve: ECCurve::NistP224, private_key, .. } => { p224_sign(private_key, &input_data) } @@ -335,6 +491,9 @@ pub fn handle_sign(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { SecureObject::ECKeyPair { curve: ECCurve::NistP384, private_key, .. } => { p384_sign(private_key, &input_data) } + SecureObject::ECKeyPair { curve: ECCurve::NistP521, private_key, .. } => { + p521_sign(private_key, &input_data) + } SecureObject::ECKeyPair { curve: ECCurve::Ed25519, private_key, @@ -404,10 +563,28 @@ pub fn handle_verify(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + + // Same digest-length contract as ECDSASign (the sign direction is + // bench-verified; verify is assumed to share the applet's check). + let wcurve = match &key_obj { + SecureObject::ECKeyPair { curve, .. } => Some(curve), + SecureObject::ECPublicKey { curve, .. } => Some(curve), + _ => None, }; + if let Some(curve) = wcurve { + if curve.needs_curve_object() { + if let Some(err) = check_ecdsa_algo(algo, input_data.len(), curve.scalar_len()) { + return err; + } + } + } let result = match &key_obj { + SecureObject::ECKeyPair { curve: ECCurve::NistP192, private_key, .. } => { + p192_verify(private_key, &input_data, &sig_data) + } SecureObject::ECKeyPair { curve: ECCurve::NistP224, private_key, .. } => { p224_verify(private_key, &input_data, &sig_data) } @@ -417,6 +594,12 @@ pub fn handle_verify(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse SecureObject::ECKeyPair { curve: ECCurve::NistP384, private_key, .. } => { p384_verify(private_key, &input_data, &sig_data) } + SecureObject::ECKeyPair { curve: ECCurve::NistP521, private_key, .. } => { + p521_verify(private_key, &input_data, &sig_data) + } + SecureObject::ECPublicKey { curve: ECCurve::NistP192, public_key } => { + p192_verify_pubkey(public_key, &input_data, &sig_data) + } SecureObject::ECPublicKey { curve: ECCurve::NistP224, public_key } => { p224_verify_pubkey(public_key, &input_data, &sig_data) } @@ -426,6 +609,9 @@ pub fn handle_verify(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse SecureObject::ECPublicKey { curve: ECCurve::NistP384, public_key } => { p384_verify_pubkey(public_key, &input_data, &sig_data) } + SecureObject::ECPublicKey { curve: ECCurve::NistP521, public_key } => { + p521_verify_pubkey(public_key, &input_data, &sig_data) + } SecureObject::ECKeyPair { curve: ECCurve::Ed25519, public_key, @@ -528,7 +714,7 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; // On the real applet the Tag7 target must already exist as an HMACKey @@ -549,6 +735,9 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> SecureObject::ECKeyPair { curve: ECCurve::Curve25519, .. }); let shared_secret = match &key_obj { + SecureObject::ECKeyPair { curve: ECCurve::NistP192, private_key, .. } => { + p192_ecdh(private_key, peer_pubkey) + } SecureObject::ECKeyPair { curve: ECCurve::NistP224, private_key, .. } => { p224_ecdh(private_key, peer_pubkey) } @@ -558,6 +747,9 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> SecureObject::ECKeyPair { curve: ECCurve::NistP384, private_key, .. } => { p384_ecdh(private_key, peer_pubkey) } + SecureObject::ECKeyPair { curve: ECCurve::NistP521, private_key, .. } => { + p521_ecdh(private_key, peer_pubkey) + } SecureObject::ECKeyPair { curve: ECCurve::Curve25519, private_key, .. } => { x25519_ecdh(private_key, peer_pubkey) } @@ -604,6 +796,18 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> } // Verify using raw public key bytes (for ECPublicKey objects) +fn p192_verify_pubkey(public_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { + let Ok(vk) = p192::ecdsa::VerifyingKey::from_sec1_bytes(public_key) else { return false }; + let Ok(sig) = p192::ecdsa::Signature::from_der(sig_data) else { return false }; + let hash = pad_hash(data, 24); + vk.verify_prehash(&hash, &sig).is_ok() +} +fn p521_verify_pubkey(public_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { + let Ok(vk) = p521::ecdsa::VerifyingKey::from_sec1_bytes(public_key) else { return false }; + let Ok(sig) = p521::ecdsa::Signature::from_der(sig_data) else { return false }; + let hash = pad_hash(data, 66); + vk.verify_prehash(&hash, &sig).is_ok() +} fn p224_verify_pubkey(public_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { let Ok(vk) = p224::ecdsa::VerifyingKey::from_sec1_bytes(public_key) else { return false }; let Ok(sig) = p224::ecdsa::Signature::from_der(sig_data) else { return false }; @@ -623,6 +827,22 @@ fn p384_verify_pubkey(public_key: &[u8], data: &[u8], sig_data: &[u8]) -> bool { vk.verify_prehash(&hash, &sig).is_ok() } +fn p192_ecdh(private_key: &[u8], peer_pubkey: &[u8]) -> Option> { + // p192 0.13 has no ecdh cargo feature; the generic elliptic-curve + // diffie_hellman works with its arithmetic implementation. + let sk = P192SecretKey::from_bytes(private_key.into()).ok()?; + let peer_pk = P192PublicKey::from_sec1_bytes(peer_pubkey).ok()?; + let shared = elliptic_curve::ecdh::diffie_hellman(sk.to_nonzero_scalar(), peer_pk.as_affine()); + Some(shared.raw_secret_bytes().to_vec()) +} + +fn p521_ecdh(private_key: &[u8], peer_pubkey: &[u8]) -> Option> { + let sk = p521::SecretKey::from_bytes(private_key.into()).ok()?; + let peer_pk = p521::PublicKey::from_sec1_bytes(peer_pubkey).ok()?; + let shared = p521::ecdh::diffie_hellman(sk.to_nonzero_scalar(), peer_pk.as_affine()); + Some(shared.raw_secret_bytes().to_vec()) +} + fn p224_ecdh(private_key: &[u8], peer_pubkey: &[u8]) -> Option> { let sk = p224::SecretKey::from_bytes(private_key.into()).ok()?; let peer_pk = p224::PublicKey::from_sec1_bytes(peer_pubkey).ok()?; @@ -908,6 +1128,116 @@ mod tests { } } + fn sign_apdu(key_id: [u8; 4], algo: u8, data: &[u8]) -> ParsedApdu { + let mut body = tlv_bytes(TAG_1, &key_id); + body.extend(tlv_bytes(TAG_2, &[algo])); + body.extend(tlv_bytes(TAG_3, data)); + ParsedApdu { + cla: 0x80, + ins: 0x03, + p1: crate::apdu::P1_SIGNATURE, + p2: crate::apdu::P2_SIGN, + data: body, + le: None, + } + } + + #[test] + fn test_ecdsa_sign_enforces_algo_digest_length() { + // Bench-verified on applet 3.1.1 and 7.2.0: the input must be + // exactly the digest length of the ECSignatureAlgo. SHA256 + // with 20 bytes and SHA512 with 32 bytes fail 0x6985; SHA1 + // with 20 bytes and SHA512 with 64 bytes (truncated to the + // leftmost 32 by the chip, host-verified) succeed. + let key_id = [0, 0, 0, 0x80]; + let mut store = ObjectStore::new(); + let sk = p256::ecdsa::SigningKey::random(&mut OsRng); + store.insert(key_id, SecureObject::ECKeyPair { + curve: ECCurve::NistP256, + private_key: sk.to_bytes().to_vec(), + public_key: sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(), + }); + + let resp = handle_sign(&sign_apdu(key_id, 0x21, &[0x11; 20]), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "SHA256 algo + 20B input"); + let resp = handle_sign(&sign_apdu(key_id, 0x26, &[0x22; 32]), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "SHA512 algo + 32B input"); + let resp = handle_sign(&sign_apdu(key_id, 0x11, &[0x33; 20]), &mut store); + assert_eq!(resp.sw, 0x9000, "SHA1 algo + 20B input"); + let resp = handle_sign(&sign_apdu(key_id, 0x26, &[0x33; 64]), &mut store); + assert_eq!(resp.sw, 0x9000, "SHA512 algo + 64B input"); + // Unknown algo byte is a request error, not a state error. + let resp = handle_sign(&sign_apdu(key_id, 0x7E, &[0x33; 32]), &mut store); + assert_eq!(resp.sw, SW_WRONG_DATA); + // Missing key object: 0x6985 as on hardware. + let resp = handle_sign(&sign_apdu([9, 9, 9, 9], 0x21, &[0x33; 32]), &mut store); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_p521_full_flow_with_curve_provisioning() { + // Bench-verified on both parts: keygen on a param-less curve + // fails 0x6985; after SetECCurveParam uploads all five + // parameters it succeeds, ReadSize reports 66, and a SHA-512 + // sign works (P-521 uses the whole 64-byte digest). + use crate::dispatch::dispatch; + let mut store = ObjectStore::new(); + store.curve_delete(0x05); + store.curve_create(0x05); // param-less + + let key_id = [0, 0, 0, 0x81]; + let mut body = tlv_bytes(TAG_1, &key_id); + body.extend(tlv_bytes(TAG_2, &[0x05])); + let keygen = ParsedApdu { + cla: 0x80, + ins: 0x01, + p1: crate::apdu::P1_EC | crate::apdu::P1_KEY_PAIR, + p2: crate::apdu::P2_DEFAULT, + data: body, + le: None, + }; + assert_eq!(dispatch(&keygen, &mut store).sw, SW_CONDITIONS_NOT_SATISFIED); + + for param in [0x01, 0x02, 0x04, 0x08, 0x10] { + store.curve_add_param(0x05, param); + } + assert_eq!(dispatch(&keygen, &mut store).sw, 0x9000); + match store.get(&key_id) { + Some(SecureObject::ECKeyPair { curve: ECCurve::NistP521, public_key, .. }) => { + assert_eq!(public_key.len(), 133, "uncompressed P-521 point"); + } + other => panic!("expected P-521 pair, got {:?}", other.is_some()), + } + + let resp = handle_sign(&sign_apdu(key_id, 0x26, &[0x44; 64]), &mut store); + assert_eq!(resp.sw, 0x9000); + } + + #[test] + fn test_p192_sign_verifies_with_p192_crate() { + // The hand-rolled P-192 signing (p192 0.13 implements only the + // verify primitive) must produce signatures the crate's own + // VerifyingKey accepts. + let sk = P192SecretKey::random(&mut OsRng); + let priv_bytes = sk.to_bytes().to_vec(); + let pub_bytes = sk.public_key().to_encoded_point(false).as_bytes().to_vec(); + let digest = [0x55u8; 20]; + + let resp = p192_sign(&priv_bytes, &digest); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + let der = &tlv::find_tlv(&tlvs, TAG_1).unwrap().value; + assert!(p192_verify_pubkey(&pub_bytes, &digest, der)); + assert!(p192_verify(&priv_bytes, &digest, der)); + // ECDH round-trip between two P-192 keys. + let sk2 = P192SecretKey::random(&mut OsRng); + let pub2 = sk2.public_key().to_encoded_point(false).as_bytes().to_vec(); + let s1 = p192_ecdh(&priv_bytes, &pub2).unwrap(); + let s2 = p192_ecdh(&sk2.to_bytes().to_vec(), &pub_bytes).unwrap(); + assert_eq!(s1, s2); + assert_eq!(s1.len(), 24); + } + #[test] fn test_ecdh_lenient_tag7_target_missing_creates_binary() { // Legacy behavior for hosts that predate the applet 7.2 contract: diff --git a/SE050Sim/se050-sim/src/handlers/mac.rs b/SE050Sim/se050-sim/src/handlers/mac.rs new file mode 100644 index 0000000..4a8549c --- /dev/null +++ b/SE050Sim/se050-sim/src/handlers/mac.rs @@ -0,0 +1,419 @@ +/* mac.rs + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of SE050Sim. + * + * SE050Sim is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * SE050Sim is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/// MAC operations (HMAC + AES-CMAC), one-shot and multi-step. +/// +/// Bench-verified against SE050C applet 3.1.1 and SE051 applet 7.2.0 +/// (August 2026): MACOneShot generate returns the exact RFC 4231 +/// HMAC-SHA256 and NIST SP800-38B CMAC-AES-128 vectors. HMAC algos +/// operate on HMACKey objects, CMAC on AESKey objects. +/// +/// Wire format (Plug & Trust v04.07.01): +/// * One-shot: INS_CRYPTO, P1_MAC, P2 = GenerateOneshot(0x45) / +/// ValidateOneshot(0x46). Tag1=keyID(4B), Tag2=algo(1B), +/// Tag3=data(opt), Tag5=MAC to validate (validate only). +/// Response Tag1 = MAC (generate) or result byte (validate). +/// * Multi-step: MACInit is P2 = Generate(0x03) / Validate(0x44) with +/// Tag1=keyID, Tag2=cryptoObjectID(2B); MACUpdate/MACFinal carry the +/// data in Tag1 and the crypto object ID in Tag2 (unlike digest and +/// cipher, which use Tag3/Tag2). The crypto object must have been +/// created with CreateCryptoObject first (ops on a never-created +/// object fail 0x6985 on real applets). + +use crate::apdu::*; +use crate::object_store::types::SecureObject; +use crate::object_store::{CryptoObjectState, ObjectStore}; +use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_5}; + +use cmac::Cmac; +use hmac::{Mac, SimpleHmac}; + +// kSE05x_MACAlgo values. +const MAC_HMAC_SHA1: u8 = 0x18; +const MAC_HMAC_SHA256: u8 = 0x19; +const MAC_HMAC_SHA384: u8 = 0x1A; +const MAC_HMAC_SHA512: u8 = 0x1B; +const MAC_CMAC_AES: u8 = 0x31; + +fn hmac_compute(key: &[u8], data: &[u8]) -> Vec +where + D: hmac::digest::Digest + hmac::digest::core_api::BlockSizeUser, +{ + let mut mac = as hmac::digest::KeyInit>::new_from_slice(key) + .expect("HMAC accepts any key length"); + Mac::update(&mut mac, data); + mac.finalize().into_bytes().to_vec() +} + +fn cmac_aes(key: &[u8], data: &[u8]) -> Option> { + // A macro sidesteps the deep trait bounds Cmac would need in a + // generic helper; the three AES key sizes are concrete here. + macro_rules! do_cmac { + ($cipher:ty) => {{ + let mut mac = as cmac::digest::KeyInit>::new_from_slice(key).ok()?; + Mac::update(&mut mac, data); + Some(mac.finalize().into_bytes().to_vec()) + }}; + } + match key.len() { + 16 => do_cmac!(aes::Aes128), + 24 => do_cmac!(aes::Aes192), + 32 => do_cmac!(aes::Aes256), + _ => None, + } +} + +/// Compute a MAC. Returns Err(SW) when the algo/key-object pairing is +/// invalid: HMAC algos need an HMACKey, CMAC needs an AESKey. +fn compute_mac(algo: u8, key_obj: &SecureObject, data: &[u8]) -> Result, u16> { + match algo { + MAC_HMAC_SHA1 | MAC_HMAC_SHA256 | MAC_HMAC_SHA384 | MAC_HMAC_SHA512 => { + let SecureObject::HMACKey { key, .. } = key_obj else { + return Err(SW_CONDITIONS_NOT_SATISFIED); + }; + Ok(match algo { + MAC_HMAC_SHA1 => hmac_compute::(key, data), + MAC_HMAC_SHA256 => hmac_compute::(key, data), + MAC_HMAC_SHA384 => hmac_compute::(key, data), + _ => hmac_compute::(key, data), + }) + } + MAC_CMAC_AES => { + let SecureObject::AESKey { key } = key_obj else { + return Err(SW_CONDITIONS_NOT_SATISFIED); + }; + cmac_aes(key, data).ok_or(SW_CONDITIONS_NOT_SATISFIED) + } + _ => Err(SW_WRONG_DATA), + } +} + +fn mac_response(mac: Vec, expected: Option<&[u8]>) -> ApduResponse { + match expected { + None => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &mac)]), + Some(exp) => { + let result = if exp == mac.as_slice() { 0x01 } else { 0x02 }; + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &[result])]) + } + } +} + +/// MACOneShot (generate or validate). +pub fn handle_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + let key_id = match tlv::find_tlv(&tlvs, TAG_1) { + Some(t) if t.value.len() == 4 => { + let mut id = [0u8; 4]; + id.copy_from_slice(&t.value); + id + } + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let algo = match tlv::find_tlv(&tlvs, TAG_2) { + Some(t) if !t.value.is_empty() => t.value[0], + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let data = tlv::find_tlv(&tlvs, TAG_3) + .map(|t| t.value.clone()) + .unwrap_or_default(); + let expected = if validate { + match tlv::find_tlv(&tlvs, TAG_5) { + Some(t) => Some(t.value.clone()), + None => return ApduResponse::error(SW_WRONG_DATA), + } + } else { + None + }; + + let key_obj = match store.get(&key_id) { + Some(obj) => obj.clone(), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + match compute_mac(algo, &key_obj, &data) { + Ok(mac) => mac_response(mac, expected.as_deref()), + Err(sw) => ApduResponse::error(sw), + } +} + +/// MACInit: Tag1=keyID(4B), Tag2=cryptoObjectID(2B). The MAC algo +/// comes from the crypto object's CreateCryptoObject subtype. +pub fn handle_init(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + let key_id = match tlv::find_tlv(&tlvs, TAG_1) { + Some(t) if t.value.len() == 4 => { + let mut id = [0u8; 4]; + id.copy_from_slice(&t.value); + id + } + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let crypto_id = match tlv::find_tlv(&tlvs, TAG_2) { + Some(t) if t.value.len() == 2 => ((t.value[0] as u16) << 8) | (t.value[1] as u16), + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + // The crypto object must have been created first; ops on a + // never-created crypto object fail 0x6985 on real applets. + let Some(&(_, subtype)) = store.crypto_object_types.get(&crypto_id) else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + if !store.exists(&key_id) { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + store.crypto_objects.insert( + crypto_id, + CryptoObjectState::Mac { + algo: subtype, + validate, + key_id, + data: Vec::new(), + }, + ); + ApduResponse::success() +} + +/// MACUpdate: Tag1=data(opt), Tag2=cryptoObjectID. +pub fn handle_update(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + let crypto_id = match tlv::find_tlv(&tlvs, TAG_2) { + Some(t) if t.value.len() == 2 => ((t.value[0] as u16) << 8) | (t.value[1] as u16), + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let input = tlv::find_tlv(&tlvs, TAG_1) + .map(|t| t.value.clone()) + .unwrap_or_default(); + match store.crypto_objects.get_mut(&crypto_id) { + Some(CryptoObjectState::Mac { data, .. }) => { + data.extend_from_slice(&input); + ApduResponse::success() + } + _ => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + } +} + +/// MACFinal: Tag1=data, Tag2=cryptoObjectID, Tag5=MAC to validate +/// (validate contexts). Response mirrors the one-shot forms. +pub fn handle_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + let crypto_id = match tlv::find_tlv(&tlvs, TAG_2) { + Some(t) if t.value.len() == 2 => ((t.value[0] as u16) << 8) | (t.value[1] as u16), + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + let input = tlv::find_tlv(&tlvs, TAG_1) + .map(|t| t.value.clone()) + .unwrap_or_default(); + let expected_tlv = tlv::find_tlv(&tlvs, TAG_5).map(|t| t.value.clone()); + + let state = match store.crypto_objects.remove(&crypto_id) { + Some(s) => s, + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + let CryptoObjectState::Mac { algo, validate, key_id, mut data } = state else { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + }; + data.extend_from_slice(&input); + + let expected = if validate { + match expected_tlv { + Some(v) => Some(v), + None => return ApduResponse::error(SW_WRONG_DATA), + } + } else { + None + }; + + let key_obj = match store.get(&key_id) { + Some(obj) => obj.clone(), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + }; + match compute_mac(algo, &key_obj, &data) { + Ok(mac) => mac_response(mac, expected.as_deref()), + Err(sw) => ApduResponse::error(sw), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // RFC 4231 test case 1. + const HMAC_KEY: [u8; 20] = [0x0B; 20]; + const HMAC_MSG: &[u8] = b"Hi There"; + const HMAC_EXPECTED: &str = + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"; + + // NIST SP800-38B CMAC-AES128, 16-byte message. + const CMAC_KEY: [u8; 16] = [ + 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, + 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C, + ]; + const CMAC_MSG: [u8; 16] = [ + 0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, + 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93, 0x17, 0x2A, + ]; + const CMAC_EXPECTED: &str = "070a16b46b4d4144f79bdd9dd04a287c"; + + fn oneshot_apdu(key_id: [u8; 4], algo: u8, data: &[u8], p2: u8) -> ParsedApdu { + let mut body = vec![TAG_1, 0x04]; + body.extend_from_slice(&key_id); + body.extend_from_slice(&[TAG_2, 0x01, algo]); + body.push(TAG_3); + body.push(data.len() as u8); + body.extend_from_slice(data); + ParsedApdu { + cla: 0x80, + ins: INS_CRYPTO, + p1: P1_MAC, + p2, + data: body, + le: None, + } + } + + #[test] + fn test_hmac_sha256_oneshot_rfc4231_vector() { + // Chip output bench-verified on applet 3.1.1 and 7.2.0. + let key_id = [0, 0, 0, 0x70]; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::HMACKey { key: HMAC_KEY.to_vec(), policy: None }); + let resp = handle_oneshot( + &oneshot_apdu(key_id, MAC_HMAC_SHA256, HMAC_MSG, P2_GENERATE_ONESHOT), + &mut store, false); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(hex::encode(&tlv::find_tlv(&tlvs, TAG_1).unwrap().value), HMAC_EXPECTED); + } + + #[test] + fn test_cmac_aes128_oneshot_nist_vector() { + let key_id = [0, 0, 0, 0x71]; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::AESKey { key: CMAC_KEY.to_vec() }); + let resp = handle_oneshot( + &oneshot_apdu(key_id, MAC_CMAC_AES, &CMAC_MSG, P2_GENERATE_ONESHOT), + &mut store, false); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(hex::encode(&tlv::find_tlv(&tlvs, TAG_1).unwrap().value), CMAC_EXPECTED); + } + + #[test] + fn test_validate_oneshot_result_bytes() { + let key_id = [0, 0, 0, 0x72]; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::HMACKey { key: HMAC_KEY.to_vec(), policy: None }); + let good = hex::decode(HMAC_EXPECTED).unwrap(); + + let mut apdu = oneshot_apdu(key_id, MAC_HMAC_SHA256, HMAC_MSG, P2_VALIDATE_ONESHOT); + apdu.data.push(TAG_5); + apdu.data.push(good.len() as u8); + apdu.data.extend_from_slice(&good); + let resp = handle_oneshot(&apdu, &mut store, true); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, [0x01]); + + let mut bad = good.clone(); + bad[0] ^= 1; + let mut apdu = oneshot_apdu(key_id, MAC_HMAC_SHA256, HMAC_MSG, P2_VALIDATE_ONESHOT); + apdu.data.push(TAG_5); + apdu.data.push(bad.len() as u8); + apdu.data.extend_from_slice(&bad); + let resp = handle_oneshot(&apdu, &mut store, true); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, [0x02]); + } + + #[test] + fn test_mac_wrong_key_object_type_fails() { + // HMAC algos need an HMACKey; CMAC needs an AESKey. + let key_id = [0, 0, 0, 0x73]; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::AESKey { key: CMAC_KEY.to_vec() }); + let resp = handle_oneshot( + &oneshot_apdu(key_id, MAC_HMAC_SHA256, HMAC_MSG, P2_GENERATE_ONESHOT), + &mut store, false); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_mac_streaming_matches_oneshot() { + let key_id = [0, 0, 0, 0x74]; + let crypto_id = 0x0030u16; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::HMACKey { key: HMAC_KEY.to_vec(), policy: None }); + store.crypto_object_types.insert(crypto_id, (0x03, MAC_HMAC_SHA256)); + + let mut body = vec![TAG_1, 0x04]; + body.extend_from_slice(&key_id); + body.extend_from_slice(&[TAG_2, 0x02, 0x00, 0x30]); + let init = ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_MAC, p2: P2_GENERATE, data: body, le: None, + }; + assert_eq!(handle_init(&init, &mut store, false).sw, 0x9000); + + let update = ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_MAC, p2: P2_UPDATE, + data: vec![TAG_1, 0x02, b'H', b'i', TAG_2, 0x02, 0x00, 0x30], + le: None, + }; + assert_eq!(handle_update(&update, &mut store).sw, 0x9000); + + let fin = ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_MAC, p2: P2_FINAL, + data: vec![TAG_1, 0x06, b' ', b'T', b'h', b'e', b'r', b'e', + TAG_2, 0x02, 0x00, 0x30], + le: None, + }; + let resp = handle_final(&fin, &mut store); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(hex::encode(&tlv::find_tlv(&tlvs, TAG_1).unwrap().value), HMAC_EXPECTED); + } + + #[test] + fn test_mac_init_without_created_crypto_object_fails() { + // Bench-verified pattern: ops on a never-created crypto object + // fail 0x6985. + let key_id = [0, 0, 0, 0x75]; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::HMACKey { key: HMAC_KEY.to_vec(), policy: None }); + let mut body = vec![TAG_1, 0x04]; + body.extend_from_slice(&key_id); + body.extend_from_slice(&[TAG_2, 0x02, 0x07, 0x77]); + let init = ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_MAC, p2: P2_GENERATE, data: body, le: None, + }; + assert_eq!(handle_init(&init, &mut store, false).sw, SW_CONDITIONS_NOT_SATISFIED); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/management.rs b/SE050Sim/se050-sim/src/handlers/management.rs index 692f1fa..dc67ff0 100644 --- a/SE050Sim/se050-sim/src/handlers/management.rs +++ b/SE050Sim/se050-sim/src/handlers/management.rs @@ -19,36 +19,52 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -use crate::apdu::{ApduResponse, ParsedApdu, P2_VERSION, P2_MEMORY, P2_RANDOM, P2_DELETE_ALL}; +use crate::apdu::{ApduResponse, ParsedApdu, P2_VERSION, P2_MEMORY, P2_RANDOM, P2_DELETE_ALL, + SW_CONDITIONS_NOT_SATISFIED}; +use crate::applet::AppletVersion; use crate::object_store::ObjectStore; use crate::tlv::{self, Tlv, TAG_1}; use rand::RngCore; -pub fn handle(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +pub fn handle(apdu: &ParsedApdu, store: &mut ObjectStore, version: AppletVersion) -> ApduResponse { match apdu.p2 { - P2_VERSION => handle_get_version(apdu, store), - P2_MEMORY => handle_get_free_memory(apdu, store), - P2_RANDOM => handle_get_random(apdu, store), + P2_VERSION => handle_get_version(version), + P2_MEMORY => handle_get_free_memory(apdu, version), + P2_RANDOM => handle_get_random(apdu, version), P2_DELETE_ALL => handle_delete_all(apdu, store), _ => ApduResponse::error(0x6A86), } } -/// GetVersion: returns TLV[Tag1] with 7-byte version info. -fn handle_get_version(_apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduResponse { - let version_data: [u8; 7] = [0x07, 0x02, 0x00, 0x6F, 0xFF, 0x01, 0x0B]; - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &version_data)]) +/// GetVersion: returns TLV[Tag1] with the 7-byte version blob +/// (bench-captured per applet generation, see AppletVersion). +fn handle_get_version(version: AppletVersion) -> ApduResponse { + ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &version.version_bytes())]) } -/// GetFreeMemory: returns TLV[Tag1] with 4-byte free memory value. -fn handle_get_free_memory(_apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduResponse { - // Report 100KB free memory - let free_memory: u32 = 102400; - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &free_memory.to_be_bytes())]) +/// GetFreeMemory: Tag1 = memory type (1B). The response width is +/// applet-dependent: 2 bytes on 3.x, 4 bytes on 7.2 (the v04.07.01 +/// middleware parses U16 vs U32 accordingly); values as measured on +/// the bench parts. +fn handle_get_free_memory(apdu: &ParsedApdu, version: AppletVersion) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(0x6A80), + }; + let mem_type = tlv::find_tlv(&tlvs, TAG_1) + .and_then(|t| t.value.first().copied()) + .unwrap_or(0x01); + match version.free_memory_bytes(mem_type) { + Some(bytes) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &bytes)]), + None => ApduResponse::error(0x6A80), + } } -/// GetRandom: reads TLV[Tag1] as 2-byte requested length, returns random bytes. -fn handle_get_random(apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduResponse { +/// GetRandom: reads TLV[Tag1] as 2-byte requested length, returns +/// random bytes. Zero-length requests fail 0x6985 and there is a +/// per-applet maximum (880 bytes on 3.1.1, 1018 on 7.2.0), both +/// bench-verified. +fn handle_get_random(apdu: &ParsedApdu, version: AppletVersion) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(0x6A80), @@ -64,14 +80,91 @@ fn handle_get_random(apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduRespons } let requested_len = ((tag1.value[0] as usize) << 8) | (tag1.value[1] as usize); + if requested_len == 0 || requested_len > version.get_random_max() { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } let mut random_data = vec![0u8; requested_len]; rand::thread_rng().fill_bytes(&mut random_data); ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &random_data)]) } -/// DeleteAll: clears all objects from the store. +/// DeleteAll: clears all objects, curves, and crypto objects (the +/// simulator then re-provisions its default curve set, see +/// ObjectStore::clear). fn handle_delete_all(_apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { store.clear(); ApduResponse::success() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::apdu::{INS_MGMT, P1_DEFAULT}; + + fn random_apdu(len: u16) -> ParsedApdu { + ParsedApdu { + cla: 0x80, + ins: INS_MGMT, + p1: P1_DEFAULT, + p2: P2_RANDOM, + data: vec![TAG_1, 0x02, (len >> 8) as u8, len as u8], + le: None, + } + } + + #[test] + fn test_get_random_bounds_per_version() { + // Bench-verified: size 0 fails on both parts; the cap is 880 + // on the SE050C (3.1.1) and 1018 on the SE051 (7.2.0). + let mut store = ObjectStore::new(); + for (version, max) in [ + (AppletVersion::V3_1_1, 880u16), + (AppletVersion::V7_2_0, 1018u16), + ] { + let resp = handle(&random_apdu(0), &mut store, version); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + let resp = handle(&random_apdu(max), &mut store, version); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value.len(), max as usize); + let resp = handle(&random_apdu(max + 1), &mut store, version); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "{:?}", version); + } + } + + #[test] + fn test_get_version_per_applet() { + // Bench-captured blobs: SE050C 3.1.1 -> 03 01 01 6f ff 01 0b, + // SE051 7.2.0 -> 07 02 00 3f ff ff ff. + let mut store = ObjectStore::new(); + let apdu = ParsedApdu { + cla: 0x80, ins: INS_MGMT, p1: P1_DEFAULT, p2: P2_VERSION, + data: vec![], le: Some(0x0B), + }; + let resp = handle(&apdu, &mut store, AppletVersion::V3_1_1); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value, [0x03, 0x01, 0x01, 0x6F, 0xFF, 0x01, 0x0B]); + let resp = handle(&apdu, &mut store, AppletVersion::V7_2_0); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value, [0x07, 0x02, 0x00, 0x3F, 0xFF, 0xFF, 0xFF]); + } + + #[test] + fn test_get_free_memory_width_per_applet() { + // 3.x replies U16, 7.2 replies U32 (middleware parses per + // version); values as measured on the bench parts. + let mut store = ObjectStore::new(); + let apdu = ParsedApdu { + cla: 0x80, ins: INS_MGMT, p1: P1_DEFAULT, p2: P2_MEMORY, + data: vec![TAG_1, 0x01, 0x01], le: None, + }; + let resp = handle(&apdu, &mut store, AppletVersion::V3_1_1); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value.len(), 2); + assert_eq!(u16::from_be_bytes([tlvs[0].value[0], tlvs[0].value[1]]), 31304); + let resp = handle(&apdu, &mut store, AppletVersion::V7_2_0); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value.len(), 4); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/mod.rs b/SE050Sim/se050-sim/src/handlers/mod.rs index 95f95d3..5671c1d 100644 --- a/SE050Sim/se050-sim/src/handlers/mod.rs +++ b/SE050Sim/se050-sim/src/handlers/mod.rs @@ -23,7 +23,9 @@ pub mod session; pub mod management; pub mod object_mgmt; pub mod crypto_obj; +pub mod curve; pub mod ec; pub mod rsa; pub mod aes; pub mod digest; +pub mod mac; diff --git a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs index e5def5a..6d717ce 100644 --- a/SE050Sim/se050-sim/src/handlers/object_mgmt.rs +++ b/SE050Sim/se050-sim/src/handlers/object_mgmt.rs @@ -28,24 +28,29 @@ use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY}; pub fn handle_write(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { match apdu.cred_type() { P1_BINARY => handle_write_binary(apdu, store), - P1_USERID => handle_write_userid(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. + P1_USERID => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), P1_COUNTER => handle_write_counter(apdu, store), _ => ApduResponse::error(SW_WRONG_P1P2), } } -/// Handle READ commands for objects. ReadObject always refuses HMACKey -/// objects, as every real applet generation does regardless of any -/// attached read policy (verified on SE051 applet 7.2.0 and SE050C -/// applet 3.1.1 hardware); size/list/type reads are unaffected. Real -/// applets guard AESKey objects the same way, but the simulator does -/// not model that yet. -pub fn handle_read(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// Handle READ commands for objects. ReadObject always refuses +/// symmetric key objects (HMACKey and AESKey), as every real applet +/// generation does regardless of any attached read policy (verified on +/// SE051 applet 7.2.0 and SE050C applet 3.1.1 hardware, including with +/// POLICY_OBJ_ALLOW_READ attached); size/list/type reads are +/// unaffected. +pub fn handle_read(apdu: &ParsedApdu, store: &mut ObjectStore, v7: bool) -> ApduResponse { match apdu.p2 { P2_DEFAULT => handle_read_object(apdu, store), P2_SIZE => handle_read_size(apdu, store), - P2_LIST => handle_read_id_list(apdu, store), - P2_TYPE => handle_read_type(apdu, store), + P2_LIST => handle_read_id_list(apdu, store, v7), + P2_TYPE => handle_read_type(apdu, store, v7), _ => ApduResponse::error(SW_WRONG_P1P2), } } @@ -69,17 +74,20 @@ fn extract_object_id(tlvs: &[Tlv]) -> Option<[u8; 4]> { Some(id) } +/// WriteBinary: Policy(opt), Tag1=objID, Tag2=offset(2B), Tag3=file +/// length(2B), Tag4=data. The file size is fixed at creation; writes +/// beyond it fail 0x6A80 and do not grow the file (bench-verified on +/// applet 3.1.1 and 7.2.0). fn handle_write_binary(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), }; - // Find object ID - could be in TAG_1 or after a policy TLV - // The driver sends: Policy(opt), Tag1=objID, Tag2=offset, Tag3=length, Tag4=data let mut obj_id = None; let mut data = None; - let mut offset: u16 = 0; + let mut offset: usize = 0; + let mut file_len: Option = None; for tlv in &tlvs { match tlv.tag { @@ -90,9 +98,11 @@ fn handle_write_binary(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespon obj_id = Some(id); } TAG_2 if tlv.value.len() == 2 => { - offset = ((tlv.value[0] as u16) << 8) | (tlv.value[1] as u16); + offset = ((tlv.value[0] as usize) << 8) | (tlv.value[1] as usize); + } + TAG_3 if tlv.value.len() == 2 => { + file_len = Some(((tlv.value[0] as usize) << 8) | (tlv.value[1] as usize)); } - TAG_3 => {} // file length - we handle dynamically TAG_4 => { data = Some(tlv.value.clone()); } @@ -107,68 +117,38 @@ fn handle_write_binary(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespon let write_data = data.unwrap_or_default(); - // If object exists, update at offset; otherwise create new - if let Some(SecureObject::Binary { data: existing }) = store.get_mut(&obj_id) { - let end = offset as usize + write_data.len(); - if end > existing.len() { - existing.resize(end, 0); - } - existing[offset as usize..end].copy_from_slice(&write_data); - // Need to persist manually since we mutated in place - // Re-insert triggers persistence - let updated = SecureObject::Binary { data: existing.clone() }; - store.insert(obj_id, updated); - } else { - if offset > 0 { - let mut full_data = vec![0u8; offset as usize + write_data.len()]; - full_data[offset as usize..].copy_from_slice(&write_data); - store.insert(obj_id, SecureObject::Binary { data: full_data }); - } else { - store.insert(obj_id, SecureObject::Binary { data: write_data }); - } - } - - ApduResponse::success() -} - -fn handle_write_userid(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { - let tlvs = match apdu.parse_tlvs() { - Ok(t) => t, - Err(_) => return ApduResponse::error(SW_WRONG_DATA), - }; - - let mut obj_id = None; - let mut value = None; - - for tlv in &tlvs { - match tlv.tag { - TAG_POLICY => {} - TAG_1 if obj_id.is_none() && tlv.value.len() == 4 => { - let mut id = [0u8; 4]; - id.copy_from_slice(&tlv.value); - obj_id = Some(id); + match store.get_mut(&obj_id) { + Some(SecureObject::Binary { data: existing }) => { + // Update in place; the file size is immutable. + if offset + write_data.len() > existing.len() { + return ApduResponse::error(SW_WRONG_DATA); } - TAG_2 => { - value = Some(tlv.value.clone()); + existing[offset..offset + write_data.len()].copy_from_slice(&write_data); + let updated = SecureObject::Binary { data: existing.clone() }; + store.insert(obj_id, updated); + ApduResponse::success() + } + Some(_) => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + None => { + // Create: the size comes from Tag3, defaulting to the data + // length when absent. + let size = file_len.unwrap_or(write_data.len()); + if offset + write_data.len() > size { + return ApduResponse::error(SW_WRONG_DATA); } - _ => {} + let mut full = vec![0u8; size]; + full[offset..offset + write_data.len()].copy_from_slice(&write_data); + store.insert(obj_id, SecureObject::Binary { data: full }); + ApduResponse::success() } } - - let obj_id = match obj_id { - Some(id) => id, - None => return ApduResponse::error(SW_WRONG_DATA), - }; - - store.insert( - obj_id, - SecureObject::UserID { - value: value.unwrap_or_default(), - }, - ); - ApduResponse::success() } +/// WriteCounter serves three request shapes sharing one APDU header: +/// CreateCounter (Tag1 + Tag2=size), SetCounterValue (Tag1 + +/// Tag3=value bytes), and IncCounter (Tag1 only). Counter sizes are +/// fixed at creation; reads return exactly `size` bytes +/// (bench-verified with a 4-byte counter on applet 3.1.1 and 7.2.0). fn handle_write_counter(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -180,19 +160,54 @@ fn handle_write_counter(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespo None => return ApduResponse::error(SW_WRONG_DATA), }; - // Initial value from Tag3 if present, otherwise 0 - let initial = tlv::find_tlv(&tlvs, TAG_3) - .map(|t| { - let mut val = 0u64; - for &b in &t.value { - val = (val << 8) | (b as u64); - } - val - }) - .unwrap_or(0); + let size_tlv = tlv::find_tlv(&tlvs, TAG_2) + .filter(|t| t.value.len() == 2) + .map(|t| ((t.value[0] as u16) << 8) | (t.value[1] as u16)); + let value_tlv = tlv::find_tlv(&tlvs, TAG_3).map(|t| { + let mut val = 0u64; + for &b in &t.value { + val = (val << 8) | (b as u64); + } + val + }); - store.insert(obj_id, SecureObject::Counter { value: initial }); - ApduResponse::success() + let existing = match store.get(&obj_id) { + Some(SecureObject::Counter { value, size }) => Some((*value, *size)), + Some(_) => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + None => None, + }; + + match (existing, size_tlv, value_tlv) { + // CreateCounter (optionally with an initial value) + (None, Some(size), value) => { + if size == 0 || size > 8 { + return ApduResponse::error(SW_WRONG_DATA); + } + store.insert(obj_id, SecureObject::Counter { + value: value.unwrap_or(0), + size, + }); + ApduResponse::success() + } + // SetCounterValue on an existing counter + (Some((_, size)), _, Some(value)) => { + store.insert(obj_id, SecureObject::Counter { value, size }); + ApduResponse::success() + } + // IncCounter + (Some((value, size)), None, None) => { + let mask = if size >= 8 { u64::MAX } else { (1u64 << (size * 8)) - 1 }; + store.insert(obj_id, SecureObject::Counter { + value: value.wrapping_add(1) & mask, + size, + }); + ApduResponse::success() + } + // Re-creating an existing counter or operating on a missing one + (Some(_), Some(_), None) | (None, _, _) => { + ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) + } + } } fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { @@ -220,12 +235,17 @@ fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespons Some(obj) => { let data = match obj { SecureObject::Binary { data } => { - let end = length.map(|l| (offset + l).min(data.len())).unwrap_or(data.len()); - if offset >= data.len() { - vec![] - } else { - data[offset..end].to_vec() + // Reads beyond the file bounds fail 0x6985 + // (bench-verified: offset 8 + length 16 on a + // 16-byte file); they are not truncated. + let end = match length { + Some(l) => offset + l, + None => data.len(), + }; + if offset >= data.len() || end > data.len() { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } + data[offset..end].to_vec() } SecureObject::ECKeyPair { public_key, .. } => public_key.clone(), SecureObject::ECPublicKey { public_key, .. } => public_key.clone(), @@ -240,24 +260,41 @@ fn handle_read_object(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRespons vec![] } } - SecureObject::AESKey { key } => key.clone(), - SecureObject::UserID { value } => value.clone(), - SecureObject::Counter { value } => value.to_be_bytes().to_vec(), + SecureObject::AESKey { .. } => { + // Like HMACKey below: symmetric key objects are + // never exported, not even with an attached + // POLICY_OBJ_ALLOW_READ. Bench-verified 0x6986 on + // SE051 applet 7.2.0 and SE050C applet 3.1.1, with + // ReadObjectAttributes confirming the policy + // reached the chip (7.2 run). + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } + SecureObject::UserID { .. } => { + // UserID objects are authentication objects; their + // value is never readable (AN12413). Creation in + // plain sessions is refused on real parts, so this + // path only serves legacy simulator stores. + return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); + } + SecureObject::Counter { value, size } => { + let be = value.to_be_bytes(); + be[8 - (*size as usize)..].to_vec() + } SecureObject::HMACKey { .. } => { // The applet never exports an HMACKey object, not even // with POLICY_OBJ_ALLOW_READ attached at creation: // verified on SE051 applet 7.2.0 hardware, where // ReadObject fails with SW 0x6986 although the object // attributes confirm the read policy, and observed - // identically on SE050C applet 3.1.1. Real applets - // guard AESKey objects the same way; the simulator - // does not model that yet. + // identically on SE050C applet 3.1.1. return ApduResponse::error(SW_COMMAND_NOT_ALLOWED); } }; ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &data)]) } - None => ApduResponse::error(SW_FILE_NOT_FOUND), + // Operations on missing objects fail 0x6985 on real applets + // (bench-verified on 3.1.1 and 7.2.0), not 0x6A82. + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), } } @@ -277,39 +314,56 @@ fn handle_read_size(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse let size = obj.data_size() as u16; ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &size.to_be_bytes())]) } - None => ApduResponse::error(SW_FILE_NOT_FOUND), + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), } } -fn handle_read_id_list(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// ReadIDList: request Tag1=2-byte offset, Tag2=1-byte type filter +/// (0xFF = all). Response Tag1 = more indicator (kSE05x_MoreIndicator: +/// 0x01 NO_MORE, 0x02 MORE), Tag2 = concatenated 4-byte IDs. The +/// simulator always returns the whole (filtered) list in one response. +fn handle_read_id_list(apdu: &ParsedApdu, store: &mut ObjectStore, v7: bool) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), }; - // Tag1 = 2-byte offset let offset = tlv::find_tlv(&tlvs, TAG_1) .filter(|t| t.value.len() == 2) .map(|t| ((t.value[0] as usize) << 8) | (t.value[1] as usize)) .unwrap_or(0); - let ids = store.list_ids(); - let mut result = Vec::new(); + let filter = tlv::find_tlv(&tlvs, TAG_2) + .and_then(|t| t.value.first().copied()) + .unwrap_or(0xFF); - // First byte: MoreIndicator (0x00 = no more, 0x01 = more) - result.push(0x00); + let mut ids = store.list_ids(); + ids.sort(); - // Append 4-byte object IDs starting from offset + let mut id_bytes = Vec::new(); for (i, id) in ids.iter().enumerate() { - if i >= offset { - result.extend_from_slice(id); + if i < offset { + continue; + } + if filter != 0xFF && filter != 0x00 { + let matches = store + .get(id) + .map(|obj| obj.type_code(v7) == filter) + .unwrap_or(false); + if !matches { + continue; + } } + id_bytes.extend_from_slice(id); } - ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &result)]) + ApduResponse::success_with_tlvs(&[ + Tlv::new(TAG_1, &[0x01]), // kSE05x_MoreIndicator_NO_MORE + Tlv::new(TAG_2, &id_bytes), + ]) } -fn handle_read_type(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +fn handle_read_type(apdu: &ParsedApdu, store: &mut ObjectStore, v7: bool) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), @@ -322,14 +376,16 @@ fn handle_read_type(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse match store.get(&obj_id) { Some(obj) => { - let type_code = obj.type_code(); - // Tag1 = type, Tag2 = transient indicator (0x01 = persistent) + let type_code = obj.type_code(v7); + // Tag1 = type, Tag2 = transient indicator. The simulator + // only models persistent objects (0x01); real applets + // report 0x02 for objects created with INS_TRANSIENT. ApduResponse::success_with_tlvs(&[ Tlv::new(TAG_1, &[type_code]), Tlv::new(TAG_2, &[0x01]), ]) } - None => ApduResponse::error(SW_FILE_NOT_FOUND), + None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), } } @@ -359,7 +415,12 @@ fn handle_delete(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { None => return ApduResponse::error(SW_WRONG_DATA), }; - store.remove(&obj_id); + // Deleting a nonexistent object fails 0x6985 (bench-verified on + // applet 3.1.1 and 7.2.0; the SDK's erase-before-create pattern + // logs a warning for it and continues). + if store.remove(&obj_id).is_none() { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } ApduResponse::success() } @@ -389,7 +450,7 @@ mod read_policy_tests { let mut store = ObjectStore::new(); store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0xAB; 32], policy: None }); - let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); } @@ -398,7 +459,7 @@ mod read_policy_tests { let mut store = ObjectStore::new(); store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0xAB; 32], policy: Some(0x0014_0000) }); - let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); } @@ -412,7 +473,18 @@ mod read_policy_tests { key: vec![0xAB; 32], policy: Some(POLICY_OBJ_ALLOW_READ | 0x0014_0000), }); - let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); + assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); + } + + #[test] + fn test_read_aeskey_denied_regardless_of_policy() { + // Hardware ground truth (SE051 7.2.0 + SE050C 3.1.1): AES key + // objects behave exactly like HMACKey objects on ReadObject -- + // 0x6986 with no policy and with ALLOW_READ attached alike. + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x69], SecureObject::AESKey { key: vec![0x11; 16] }); + let resp = handle_read(&read_apdu([0, 0, 0, 0x69]), &mut store, true); assert_eq!(resp.sw, SW_COMMAND_NOT_ALLOWED); } @@ -425,7 +497,7 @@ mod read_policy_tests { let mut store = ObjectStore::new(); store.insert([0, 0, 0, 0x66], SecureObject::Binary { data: data.clone() }); - let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store); + let resp = handle_read(&read_apdu([0, 0, 0, 0x66]), &mut store, true); assert_eq!(resp.sw, 0x9000); let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, data); @@ -440,7 +512,175 @@ mod read_policy_tests { SecureObject::HMACKey { key: vec![0xAB; 32], policy: None }); let mut apdu = read_apdu([0, 0, 0, 0x66]); apdu.p2 = P2_SIZE; - let resp = handle_read(&apdu, &mut store); + let resp = handle_read(&apdu, &mut store, true); assert_eq!(resp.sw, 0x9000); } } + +#[cfg(test)] +mod lifecycle_tests { + use super::*; + + fn tag1_apdu(ins: u8, p1: u8, p2: u8, obj_id: [u8; 4]) -> ParsedApdu { + ParsedApdu { + cla: 0x80, + ins, + p1, + p2, + data: vec![TAG_1, 0x04, obj_id[0], obj_id[1], obj_id[2], obj_id[3]], + le: None, + } + } + + #[test] + fn test_delete_nonexistent_returns_6985() { + // Bench-verified on applet 3.1.1 and 7.2.0 (HARDWARE_VALIDATION + // ground truth #5). + let mut store = ObjectStore::new(); + let apdu = tag1_apdu(INS_MGMT, P1_DEFAULT, P2_DELETE_OBJECT, [0x7F, 0, 0, 1]); + assert_eq!(handle_mgmt(&apdu, &mut store).sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_read_missing_object_returns_6985() { + // Bench-verified: ReadObject and ReadSize on a missing object + // return 0x6985, not 0x6A82. + let mut store = ObjectStore::new(); + let apdu = tag1_apdu(INS_READ, P1_DEFAULT, P2_DEFAULT, [0x7F, 0, 0, 2]); + assert_eq!(handle_read(&apdu, &mut store, true).sw, SW_CONDITIONS_NOT_SATISFIED); + let apdu = tag1_apdu(INS_READ, P1_DEFAULT, P2_SIZE, [0x7F, 0, 0, 2]); + assert_eq!(handle_read(&apdu, &mut store, true).sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_write_userid_refused() { + // Bench-verified: WriteUserID in a plain session -> 0x6985 on + // both applet generations. + let mut store = ObjectStore::new(); + let mut apdu = tag1_apdu(INS_WRITE, P1_USERID, P2_DEFAULT, [0x7F, 0, 0, 3]); + apdu.data.extend_from_slice(&[TAG_2, 0x04, b'u', b's', b'e', b'r']); + assert_eq!(handle_write(&apdu, &mut store).sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.get(&[0x7F, 0, 0, 3]).is_none()); + } + + fn write_binary_apdu(obj_id: [u8; 4], offset: u16, len: Option, data: &[u8]) + -> ParsedApdu + { + let mut body = vec![TAG_1, 0x04]; + body.extend_from_slice(&obj_id); + body.extend_from_slice(&[TAG_2, 0x02, (offset >> 8) as u8, offset as u8]); + if let Some(l) = len { + body.extend_from_slice(&[TAG_3, 0x02, (l >> 8) as u8, l as u8]); + } + body.push(TAG_4); + body.push(data.len() as u8); + body.extend_from_slice(data); + ParsedApdu { cla: 0x80, ins: INS_WRITE, p1: P1_BINARY, p2: P2_DEFAULT, + data: body, le: None } + } + + #[test] + fn test_binary_bounds_enforced() { + // Bench-verified on a 16-byte file: write at offset 8 with 16 + // bytes -> 0x6A80 and the size stays 16; read offset 8 length + // 16 -> 0x6985. + let id = [0x7F, 0, 0, 4]; + let mut store = ObjectStore::new(); + let create = write_binary_apdu(id, 0, Some(16), &[0xC3; 16]); + assert_eq!(handle_write(&create, &mut store).sw, 0x9000); + + let past_end = write_binary_apdu(id, 8, Some(16), &[0xC3; 16]); + assert_eq!(handle_write(&past_end, &mut store).sw, SW_WRONG_DATA); + match store.get(&id) { + Some(SecureObject::Binary { data }) => assert_eq!(data.len(), 16), + _ => panic!("binary object missing"), + } + + let mut read = tag1_apdu(INS_READ, P1_DEFAULT, P2_DEFAULT, id); + read.data.extend_from_slice(&[TAG_2, 0x02, 0x00, 0x08, TAG_3, 0x02, 0x00, 0x10]); + assert_eq!(handle_read(&read, &mut store, true).sw, SW_CONDITIONS_NOT_SATISFIED); + + // In-bounds partial read still works. + let mut read = tag1_apdu(INS_READ, P1_DEFAULT, P2_DEFAULT, id); + read.data.extend_from_slice(&[TAG_2, 0x02, 0x00, 0x08, TAG_3, 0x02, 0x00, 0x08]); + let resp = handle_read(&read, &mut store, true); + assert_eq!(resp.sw, 0x9000); + } + + #[test] + fn test_counter_create_set_inc_read() { + // Bench-verified: a counter created with size 4 reads back + // exactly 4 bytes and ReadSize reports 4. + let id = [0x7F, 0, 0, 5]; + let mut store = ObjectStore::new(); + + let mut create = tag1_apdu(INS_WRITE, P1_COUNTER, P2_DEFAULT, id); + create.data.extend_from_slice(&[TAG_2, 0x02, 0x00, 0x04]); + assert_eq!(handle_write(&create, &mut store).sw, 0x9000); + + let mut set = tag1_apdu(INS_WRITE, P1_COUNTER, P2_DEFAULT, id); + set.data.extend_from_slice(&[TAG_3, 0x04, 0x01, 0x02, 0x03, 0x04]); + assert_eq!(handle_write(&set, &mut store).sw, 0x9000); + + let inc = tag1_apdu(INS_WRITE, P1_COUNTER, P2_DEFAULT, id); + assert_eq!(handle_write(&inc, &mut store).sw, 0x9000); + + let read = tag1_apdu(INS_READ, P1_DEFAULT, P2_DEFAULT, id); + let resp = handle_read(&read, &mut store, true); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, + vec![0x01, 0x02, 0x03, 0x05]); + + let mut size = tag1_apdu(INS_READ, P1_DEFAULT, P2_SIZE, id); + size.p2 = P2_SIZE; + let resp = handle_read(&size, &mut store, true); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, vec![0x00, 0x04]); + } + + #[test] + fn test_read_id_list_format() { + // Response format per the SDK parser: Tag1 = more indicator + // (0x01 = NO_MORE), Tag2 = 4-byte IDs (bench-verified layout). + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 1], SecureObject::Binary { data: vec![1, 2, 3] }); + store.insert([0, 0, 0, 2], SecureObject::AESKey { key: vec![0; 16] }); + + let apdu = ParsedApdu { + cla: 0x80, ins: INS_READ, p1: P1_DEFAULT, p2: P2_LIST, + data: vec![TAG_1, 0x02, 0x00, 0x00, TAG_2, 0x01, 0xFF], + le: None, + }; + let resp = handle_read(&apdu, &mut store, true); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, vec![0x01]); + let ids = &tlv::find_tlv(&tlvs, TAG_2).unwrap().value; + assert_eq!(ids.len(), 8); + assert_eq!(&ids[..4], &[0, 0, 0, 1]); + assert_eq!(&ids[4..], &[0, 0, 0, 2]); + } + + #[test] + fn test_read_type_version_dependent_ec_codes() { + // Bench-verified: a P-256 pair reads type 0x29 on the SE051 + // (applet 7.2.0) and generic 0x01 on the SE050C (applet 3.1.1). + use crate::object_store::types::ECCurve; + let id = [0x7F, 0, 0, 6]; + let mut store = ObjectStore::new(); + store.insert(id, SecureObject::ECKeyPair { + curve: ECCurve::NistP256, + private_key: vec![0; 32], + public_key: vec![0x04; 65], + }); + let apdu = tag1_apdu(INS_READ, P1_DEFAULT, P2_TYPE, id); + let resp = handle_read(&apdu, &mut store, true); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, vec![0x29]); + + let resp = handle_read(&apdu, &mut store, false); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlv::find_tlv(&tlvs, TAG_1).unwrap().value, vec![0x01]); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/rsa.rs b/SE050Sim/se050-sim/src/handlers/rsa.rs index cc08dfa..ae6f8a6 100644 --- a/SE050Sim/se050-sim/src/handlers/rsa.rs +++ b/SE050Sim/se050-sim/src/handlers/rsa.rs @@ -280,7 +280,7 @@ pub fn handle_rsa_encrypt(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; let public_key = match public_key_from_obj(&key_obj) { @@ -354,7 +354,7 @@ pub fn handle_rsa_decrypt(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes let key_obj = match store.get(&key_id) { Some(obj) => obj.clone(), - None => return ApduResponse::error(SW_FILE_NOT_FOUND), + None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), }; let SecureObject::RSAKeyPair { private_key_der, .. } = &key_obj else { diff --git a/SE050Sim/se050-sim/src/handlers/session.rs b/SE050Sim/se050-sim/src/handlers/session.rs index f90b01e..d7d279b 100644 --- a/SE050Sim/se050-sim/src/handlers/session.rs +++ b/SE050Sim/se050-sim/src/handlers/session.rs @@ -20,6 +20,7 @@ */ use crate::apdu::{ApduResponse, ParsedApdu}; +use crate::applet::AppletVersion; use crate::object_store::ObjectStore; /// SE050 applet AID @@ -28,17 +29,19 @@ const SE050_AID: [u8; 16] = [ 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, ]; -/// Simulated version info: major=7, minor=2, patch=0, features=0x6FFF, securebox=0x010B -const APP_VERSION: [u8; 7] = [0x07, 0x02, 0x00, 0x6F, 0xFF, 0x01, 0x0B]; - /// Handle SELECT applet command (CLA=0x00, INS=0xA4). /// The response is raw bytes (not TLV-wrapped), matching what the driver -/// expects in receive_apdu_raw. -pub fn handle_select(apdu: &ParsedApdu, _store: &mut ObjectStore) -> ApduResponse { +/// expects in receive_apdu_raw. The 7-byte body is the same version +/// blob GetVersion returns; the middleware parses it to decide applet +/// compatibility ("Compiled for ... Got older ..." aborts). +pub fn handle_select( + apdu: &ParsedApdu, + _store: &mut ObjectStore, + version: AppletVersion, +) -> ApduResponse { // Verify the AID matches if apdu.data.len() >= 16 && apdu.data[..16] == SE050_AID { - // Return 7-byte version info + SW 0x9000 - ApduResponse::success_with_data(APP_VERSION.to_vec()) + ApduResponse::success_with_data(version.version_bytes().to_vec()) } else { ApduResponse::error(0x6A82) // File not found } diff --git a/SE050Sim/se050-sim/src/lib.rs b/SE050Sim/se050-sim/src/lib.rs index 6ef05b6..1a0beb5 100644 --- a/SE050Sim/se050-sim/src/lib.rs +++ b/SE050Sim/se050-sim/src/lib.rs @@ -20,6 +20,7 @@ */ pub mod apdu; +pub mod applet; 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 8601cf5..3f9d25b 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -28,7 +28,11 @@ use types::SecureObject; /// Hex-encoded 4-byte object ID used as JSON key. type ObjectIdKey = String; -/// State for a transient crypto object (digest or cipher context). +/// All five EC curve parameters (A, B, G, N, PRIME) present, matching +/// the kSE05x_ECCurveParam bit assignments. +pub const CURVE_PARAMS_COMPLETE: u8 = 0x1F; + +/// State for a transient crypto object (digest, cipher, or MAC context). #[derive(Debug, Clone)] pub enum CryptoObjectState { Digest { @@ -37,9 +41,19 @@ pub enum CryptoObjectState { }, Cipher { encrypting: bool, + mode: u8, + key_id: [u8; 4], + /// CBC chaining vector / CTR counter, advanced as blocks are + /// processed across CipherUpdate calls. + chain: Vec, + /// Input bytes not yet processed (less than one block). + pending: Vec, + }, + Mac { + algo: u8, + validate: bool, key_id: [u8; 4], - iv: Vec, - accumulated: Vec, + data: Vec, }, } @@ -47,17 +61,29 @@ pub enum CryptoObjectState { pub struct ObjectStore { objects: HashMap<[u8; 4], SecureObject>, persist_path: Option, - /// Transient crypto objects (digest/cipher contexts), keyed by 2-byte crypto object ID. + /// EC curve objects: curve ID -> bitmask of uploaded parameters + /// (kSE05x_ECCurveParam bits; CURVE_PARAMS_COMPLETE = usable). + /// Real applets ship with no curves created; the simulator + /// pre-provisions its supported NIST curves so hosts that predate + /// curve management keep working out of the box. + ec_curves: HashMap, + /// Transient crypto objects (digest/cipher/MAC contexts), keyed by 2-byte crypto object ID. pub crypto_objects: HashMap, /// Registry of created crypto object types (ID -> (context_type, subtype)). pub crypto_object_types: HashMap, } +fn default_curves() -> HashMap { + // P-192, P-224, P-256, P-384, P-521 fully parameterized. + (0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect() +} + impl ObjectStore { pub fn new() -> Self { Self { objects: HashMap::new(), persist_path: None, + ec_curves: default_curves(), crypto_objects: HashMap::new(), crypto_object_types: HashMap::new(), } @@ -67,6 +93,7 @@ impl ObjectStore { let mut store = Self { objects: HashMap::new(), persist_path: Some(path.clone()), + ec_curves: default_curves(), crypto_objects: HashMap::new(), crypto_object_types: HashMap::new(), }; @@ -104,7 +131,14 @@ impl ObjectStore { } pub fn clear(&mut self) { + // DeleteAll on a real applet also deletes created curves and + // crypto objects. The simulator re-provisions its default + // curve set afterwards (see ec_curves) so key generation keeps + // working for hosts that never create curves themselves. self.objects.clear(); + self.ec_curves = default_curves(); + self.crypto_objects.clear(); + self.crypto_object_types.clear(); self.persist(); } @@ -112,14 +146,67 @@ impl ObjectStore { self.objects.len() } + // ---- EC curve object state ---- + + /// Curve exists (parameterized or not). A created but param-less + /// curve still shows as SET in ReadECCurveList on real applets. + pub fn curve_exists(&self, curve_id: u8) -> bool { + self.ec_curves.contains_key(&curve_id) + } + + /// Curve exists and all five parameters have been uploaded; only + /// then do key operations on it succeed (bench-verified: keygen on + /// a param-less curve fails 0x6985 on applet 3.1.1 and 7.2.0). + pub fn curve_ready(&self, curve_id: u8) -> bool { + self.ec_curves.get(&curve_id) == Some(&CURVE_PARAMS_COMPLETE) + } + + /// Create a curve object with no parameters uploaded yet. + pub fn curve_create(&mut self, curve_id: u8) { + self.ec_curves.insert(curve_id, 0); + self.persist(); + } + + /// Reset an existing curve to the parameter-less state (applet + /// 3.1.1 duplicate-CreateECCurve behavior). + pub fn curve_reset(&mut self, curve_id: u8) { + self.ec_curves.insert(curve_id, 0); + self.persist(); + } + + /// Record an uploaded curve parameter (kSE05x_ECCurveParam bit). + pub fn curve_add_param(&mut self, curve_id: u8, param: u8) { + if let Some(mask) = self.ec_curves.get_mut(&curve_id) { + *mask |= param & CURVE_PARAMS_COMPLETE; + self.persist(); + } + } + + pub fn curve_delete(&mut self, curve_id: u8) -> bool { + let removed = self.ec_curves.remove(&curve_id).is_some(); + if removed { + self.persist(); + } + removed + } + fn persist(&self) { let Some(path) = &self.persist_path else { return }; - let serializable: HashMap = self + let objects: HashMap = self .objects .iter() .map(|(k, v)| (hex::encode(k), v)) .collect(); - if let Ok(json) = serde_json::to_string_pretty(&serializable) { + let curves: HashMap = self + .ec_curves + .iter() + .map(|(k, v)| (format!("{:02x}", k), *v)) + .collect(); + let doc = serde_json::json!({ + "objects": objects, + "ec_curves": curves, + }); + if let Ok(json) = serde_json::to_string_pretty(&doc) { let _ = std::fs::write(path, json); } } @@ -127,8 +214,30 @@ impl ObjectStore { fn load(&mut self) { let Some(path) = &self.persist_path else { return }; let Ok(json) = std::fs::read_to_string(path) else { return }; + let Ok(value): Result = serde_json::from_str(&json) else { + return; + }; + + // Current schema: {"objects": {...}, "ec_curves": {...}}. + // Legacy schema (pre curve-state): a flat hex-id -> object map. + let objects_value = if value.get("objects").is_some() { + if let Some(curves) = value.get("ec_curves").and_then(|v| v.as_object()) { + self.ec_curves = curves + .iter() + .filter_map(|(k, v)| { + let id = u8::from_str_radix(k, 16).ok()?; + let mask = v.as_u64()? as u8; + Some((id, mask)) + }) + .collect(); + } + value.get("objects").cloned().unwrap_or_default() + } else { + value + }; + let Ok(deserialized): Result, _> = - serde_json::from_str(&json) + serde_json::from_value(objects_value) else { return; }; @@ -149,3 +258,47 @@ impl Default for ObjectStore { Self::new() } } + +#[cfg(test)] +mod persistence_tests { + use super::*; + + #[test] + fn test_legacy_flat_store_file_still_loads() { + // Pre-curve-state store files are a flat hex-id -> object map; + // they must keep loading (with the default curve set) so + // existing on-disk stores survive the schema change. + let dir = std::env::temp_dir(); + let path = dir.join("se050_sim_legacy_store_test.json"); + let legacy = r#"{ + "00000042": { "Binary": { "data": [1, 2, 3] } } + }"#; + std::fs::write(&path, legacy).unwrap(); + + let store = ObjectStore::with_persistence(path.clone()); + match store.get(&[0, 0, 0, 0x42]) { + Some(SecureObject::Binary { data }) => assert_eq!(data, &vec![1, 2, 3]), + other => panic!("legacy object missing: {:?}", other.is_some()), + } + assert!(store.curve_ready(0x03), "default curves provisioned"); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn test_curve_state_round_trips_through_persistence() { + let dir = std::env::temp_dir(); + let path = dir.join("se050_sim_curve_store_test.json"); + let _ = std::fs::remove_file(&path); + { + let mut store = ObjectStore::with_persistence(path.clone()); + store.curve_delete(0x01); + store.curve_create(0x06); // brainpool160r1, param-less + } + let store = ObjectStore::with_persistence(path.clone()); + assert!(!store.curve_exists(0x01)); + assert!(store.curve_exists(0x06)); + assert!(!store.curve_ready(0x06)); + assert!(store.curve_ready(0x03)); + 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 fea1b7b..195cbaf 100644 --- a/SE050Sim/se050-sim/src/object_store/types.rs +++ b/SE050Sim/se050-sim/src/object_store/types.rs @@ -22,11 +22,13 @@ use serde::{Deserialize, Serialize}; /// Types of EC curves supported by the simulator. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum ECCurve { + NistP192, NistP224, NistP256, NistP384, + NistP521, Ed25519, Curve25519, } @@ -35,14 +37,37 @@ impl ECCurve { /// Parse from the SE050 curve constant byte. pub fn from_se050_byte(b: u8) -> Option { match b { + 0x01 => Some(ECCurve::NistP192), 0x02 => Some(ECCurve::NistP224), 0x03 => Some(ECCurve::NistP256), 0x04 => Some(ECCurve::NistP384), + 0x05 => Some(ECCurve::NistP521), 0x40 => Some(ECCurve::Ed25519), 0x41 => Some(ECCurve::Curve25519), _ => None, } } + + /// Scalar (private key / shared secret) size in bytes. This is + /// also what ReadSize reports for EC key objects on real applets + /// (bench-verified: 32 for P-256, 66 for P-521, 24 for P-192). + pub fn scalar_len(self) -> usize { + match self { + ECCurve::NistP192 => 24, + ECCurve::NistP224 => 28, + ECCurve::NistP256 => 32, + ECCurve::NistP384 => 48, + ECCurve::NistP521 => 66, + ECCurve::Ed25519 | ECCurve::Curve25519 => 32, + } + } + + /// Whether this is a Weierstrass curve that must exist as a + /// parameterized curve object on the applet before key operations + /// (25519 curves are built-in constants and need no curve object). + pub fn needs_curve_object(self) -> bool { + !matches!(self, ECCurve::Ed25519 | ECCurve::Curve25519) + } } /// RSA key components accumulated across per-component `WriteRSAKey` APDUs. @@ -107,6 +132,12 @@ pub enum SecureObject { }, Counter { value: u64, + /// Counter size in bytes, fixed at creation (1..=8). Real + /// applets return exactly this many bytes from ReadObject and + /// this value from ReadSize (bench-verified with a 4-byte + /// counter on applet 3.1.1 and 7.2.0). + #[serde(default = "default_counter_size")] + size: u16, }, HMACKey { key: Vec, @@ -121,23 +152,44 @@ pub enum SecureObject { } impl SecureObject { - /// Get the SE050 secure object type code (v7.2.0+ curve-specific for EC). - pub fn type_code(&self) -> u8 { + /// Get the SE050 secure object type code as reported by ReadType. + /// + /// Applet 7.2 reports curve-specific EC type codes; applet 3.x + /// reports the generic kSE05x_SecObjTyp_EC_KEY_PAIR (0x01) / + /// EC_PUB_KEY (0x03). Bench-verified: a P-256 pair reads back 0x29 + /// on the SE051 and 0x01 on the SE050C; a P-521 pair reads 0x31 on + /// the SE051 and 0x01 on the SE050C. The public-key and 25519 + /// generic codes follow the SDK's SE05x_SecureObjectType_t enum. + pub fn type_code(&self, v7: bool) -> u8 { match self { - SecureObject::ECKeyPair { curve, .. } => match curve { - ECCurve::NistP224 => 0x25, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P224 - ECCurve::NistP256 => 0x29, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P256 - ECCurve::NistP384 => 0x2D, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P384 - ECCurve::Ed25519 => 0x65, // kSE05x_SecObjTyp_EC_KEY_PAIR_ED25519 - ECCurve::Curve25519 => 0x69, // kSE05x_SecObjTyp_EC_KEY_PAIR_MONT_DH_25519 - }, - SecureObject::ECPublicKey { curve, .. } => match curve { - ECCurve::NistP224 => 0x26, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P224 - ECCurve::NistP256 => 0x2A, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P256 - ECCurve::NistP384 => 0x2E, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P384 - ECCurve::Ed25519 => 0x67, // kSE05x_SecObjTyp_EC_PUB_KEY_ED25519 - ECCurve::Curve25519 => 0x6B, // kSE05x_SecObjTyp_EC_PUB_KEY_MONT_DH_25519 - }, + SecureObject::ECKeyPair { curve, .. } => { + if !v7 { + return 0x01; // kSE05x_SecObjTyp_EC_KEY_PAIR + } + match curve { + ECCurve::NistP192 => 0x21, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P192 + ECCurve::NistP224 => 0x25, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P224 + ECCurve::NistP256 => 0x29, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P256 + ECCurve::NistP384 => 0x2D, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P384 + ECCurve::NistP521 => 0x31, // kSE05x_SecObjTyp_EC_KEY_PAIR_NIST_P521 + ECCurve::Ed25519 => 0x65, // kSE05x_SecObjTyp_EC_KEY_PAIR_ED25519 + ECCurve::Curve25519 => 0x69, // kSE05x_SecObjTyp_EC_KEY_PAIR_MONT_DH_25519 + } + } + SecureObject::ECPublicKey { curve, .. } => { + if !v7 { + return 0x03; // kSE05x_SecObjTyp_EC_PUB_KEY + } + match curve { + ECCurve::NistP192 => 0x22, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P192 + ECCurve::NistP224 => 0x26, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P224 + ECCurve::NistP256 => 0x2A, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P256 + ECCurve::NistP384 => 0x2E, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P384 + ECCurve::NistP521 => 0x32, // kSE05x_SecObjTyp_EC_PUB_KEY_NIST_P521 + ECCurve::Ed25519 => 0x67, // kSE05x_SecObjTyp_EC_PUB_KEY_ED25519 + ECCurve::Curve25519 => 0x6B, // kSE05x_SecObjTyp_EC_PUB_KEY_MONT_DH_25519 + } + } SecureObject::RSAKeyPair { .. } => 0x04, SecureObject::AESKey { .. } => 0x09, SecureObject::Binary { .. } => 0x0B, @@ -155,25 +207,34 @@ impl SecureObject { _ => None, }?; Some(match curve { + ECCurve::NistP192 => 0x01, ECCurve::NistP224 => 0x02, ECCurve::NistP256 => 0x03, ECCurve::NistP384 => 0x04, + ECCurve::NistP521 => 0x05, ECCurve::Ed25519 => 0x40, ECCurve::Curve25519 => 0x41, }) } - /// Get the size of the object's primary data in bytes. + /// Size reported by ReadSize, in bytes. For EC objects real + /// applets report the scalar size, not the encoded public key + /// length (bench-verified: 32 for a P-256 pair, 66 for P-521, + /// 24 for P-192); counters report their creation-time size. pub fn data_size(&self) -> usize { match self { - SecureObject::ECKeyPair { public_key, .. } => public_key.len(), - SecureObject::ECPublicKey { public_key, .. } => public_key.len(), + SecureObject::ECKeyPair { curve, .. } => curve.scalar_len(), + SecureObject::ECPublicKey { curve, .. } => curve.scalar_len(), SecureObject::RSAKeyPair { key_size_bits, .. } => (*key_size_bits as usize) / 8, SecureObject::AESKey { key } => key.len(), SecureObject::Binary { data } => data.len(), SecureObject::UserID { value } => value.len(), - SecureObject::Counter { .. } => 8, + SecureObject::Counter { size, .. } => *size as usize, SecureObject::HMACKey { key, .. } => key.len(), } } } + +fn default_counter_size() -> u16 { + 8 +} From fa6d52aa888fa9af110d4b07b7921e74e7364438 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Mon, 3 Aug 2026 16:57:01 +0100 Subject: [PATCH 2/2] SE050Sim: address review - MAC context type check, unique test store paths MACInit now requires the crypto object to have been created with the SIGNATURE context (0x03), matching how the SDK creates MAC contexts and how the digest (0x01) and cipher (0x02) handlers already validate theirs. The persistence tests write to per-invocation store paths (pid + nanos) so concurrent cargo test processes cannot interfere through a shared temp file. --- SE050Sim/se050-sim/src/handlers/mac.rs | 28 ++++++++++++++++++++-- SE050Sim/se050-sim/src/object_store/mod.rs | 22 +++++++++++++---- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/SE050Sim/se050-sim/src/handlers/mac.rs b/SE050Sim/se050-sim/src/handlers/mac.rs index 4a8549c..8b560ab 100644 --- a/SE050Sim/se050-sim/src/handlers/mac.rs +++ b/SE050Sim/se050-sim/src/handlers/mac.rs @@ -176,10 +176,15 @@ pub fn handle_init(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool) - _ => return ApduResponse::error(SW_WRONG_DATA), }; // The crypto object must have been created first; ops on a - // never-created crypto object fail 0x6985 on real applets. - let Some(&(_, subtype)) = store.crypto_object_types.get(&crypto_id) else { + // never-created crypto object fail 0x6985 on real applets. MAC + // contexts are created with kSE05x_CryptoContext_SIGNATURE (0x03) + // by the SDK; refuse digest/cipher objects like their handlers do. + let Some(&(context, subtype)) = store.crypto_object_types.get(&crypto_id) else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); }; + if context != 0x03 { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } if !store.exists(&key_id) { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); } @@ -416,4 +421,23 @@ mod tests { }; assert_eq!(handle_init(&init, &mut store, false).sw, SW_CONDITIONS_NOT_SATISFIED); } + + #[test] + fn test_mac_init_rejects_non_signature_context_object() { + // A crypto object created with the DIGEST context (0x01) must + // not be usable as a MAC context; the SDK creates MAC contexts + // with kSE05x_CryptoContext_SIGNATURE (0x03). + let key_id = [0, 0, 0, 0x76]; + let crypto_id = 0x0031u16; + let mut store = ObjectStore::new(); + store.insert(key_id, SecureObject::HMACKey { key: HMAC_KEY.to_vec(), policy: None }); + store.crypto_object_types.insert(crypto_id, (0x01, 0x04)); // DIGEST/SHA256 + let mut body = vec![TAG_1, 0x04]; + body.extend_from_slice(&key_id); + body.extend_from_slice(&[TAG_2, 0x02, 0x00, 0x31]); + let init = ParsedApdu { + cla: 0x80, ins: INS_CRYPTO, p1: P1_MAC, p2: P2_GENERATE, data: body, le: None, + }; + assert_eq!(handle_init(&init, &mut store, false).sw, SW_CONDITIONS_NOT_SATISFIED); + } } diff --git a/SE050Sim/se050-sim/src/object_store/mod.rs b/SE050Sim/se050-sim/src/object_store/mod.rs index 3f9d25b..12090a4 100644 --- a/SE050Sim/se050-sim/src/object_store/mod.rs +++ b/SE050Sim/se050-sim/src/object_store/mod.rs @@ -263,13 +263,27 @@ impl Default for ObjectStore { mod persistence_tests { use super::*; + /// Unique per-invocation store path so concurrent `cargo test` + /// processes cannot interfere through a shared temp file. + fn unique_store_path(tag: &str) -> PathBuf { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .subsec_nanos(); + std::env::temp_dir().join(format!( + "se050_sim_{}_{}_{}.json", + tag, + std::process::id(), + nanos + )) + } + #[test] fn test_legacy_flat_store_file_still_loads() { // Pre-curve-state store files are a flat hex-id -> object map; // they must keep loading (with the default curve set) so // existing on-disk stores survive the schema change. - let dir = std::env::temp_dir(); - let path = dir.join("se050_sim_legacy_store_test.json"); + let path = unique_store_path("legacy_store_test"); let legacy = r#"{ "00000042": { "Binary": { "data": [1, 2, 3] } } }"#; @@ -286,9 +300,7 @@ mod persistence_tests { #[test] fn test_curve_state_round_trips_through_persistence() { - let dir = std::env::temp_dir(); - let path = dir.join("se050_sim_curve_store_test.json"); - let _ = std::fs::remove_file(&path); + let path = unique_store_path("curve_store_test"); { let mut store = ObjectStore::with_persistence(path.clone()); store.curve_delete(0x01);