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..8b560ab --- /dev/null +++ b/SE050Sim/se050-sim/src/handlers/mac.rs @@ -0,0 +1,443 @@ +/* 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. 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); + } + 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); + } + + #[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/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..12090a4 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,59 @@ impl Default for ObjectStore { Self::new() } } + +#[cfg(test)] +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 path = unique_store_path("legacy_store_test"); + 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 path = unique_store_path("curve_store_test"); + { + 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 +}