From 8256cd58ddb14ec527eb87ab2e9faaa756ad710f Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Fri, 7 Aug 2026 16:08:06 +0100 Subject: [PATCH 1/2] SE050Sim: add SE050E personality, fix GetFreeMemory reply width Bench-probed an SE050E on the hardware rig. The part runs IoT applet 7.2.0 and matches the SE051 on the entire probe matrix (GetRandom cap 1018, curve-specific ReadType codes, duplicate CreateECCurve refused 0x6985, ECDSA digest-length enforcement, identical cipher/MAC vectors, stateful curves) except for three differences, now modelled as a third applet personality selected with SE050_SIM_APPLET=e / se050e: - Version blob 07 02 00 3f 9f ff ff: the appletConfig word clears exactly RSA_PLAIN (0x0020) and RSA_CRT (0x0040). - No RSA at all: keygen refused 0x6985 for every size and format, key import refused 0x6a80. The wolfCrypt suite against real SE050E silicon passes through AES-GCM then fails RSA with WC_HW_E and aborts; there is no software fallback. - GetFreeMemory values 32767/796/784, PERSISTENT clamped at 0x7fff. Also fixes the 7.2.0 personality's GetFreeMemory reply width: all bench applets reply with a 2-byte value. The earlier 4-byte encoding came from misreading the v04.07.01 middleware, whose U32 parse path is gated on SE05X_CHECK_52F_VERSION (applet minor 0x10-0x1f, the SE052F family) and never runs for these applets; tlvGet_U16 rejects TLV values longer than 2 bytes, so the U32 reply would make Se05x_API_GetFreeMemory fail host-side. --- SE050Sim/se050-sim/src/applet.rs | 108 ++++++++++++++---- SE050Sim/se050-sim/src/dispatch.rs | 2 +- SE050Sim/se050-sim/src/handlers/curve.rs | 8 +- SE050Sim/se050-sim/src/handlers/management.rs | 61 ++++++---- SE050Sim/se050-sim/src/handlers/rsa.rs | 70 +++++++++++- 5 files changed, 200 insertions(+), 49 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet.rs b/SE050Sim/se050-sim/src/applet.rs index f5e59cb..d4ee4df 100644 --- a/SE050Sim/se050-sim/src/applet.rs +++ b/SE050Sim/se050-sim/src/applet.rs @@ -21,23 +21,31 @@ /// 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: +/// The simulator can present itself as any of the three parts that were +/// bench-characterized on real silicon (August 2026, see +/// SE050Sim/HARDWARE_VALIDATION.md): an SE050C running applet 3.1.1, an +/// SE051 running applet 7.2.0, or an SE050E running applet 7.2.0 with +/// the RSA feature bits disabled. Almost all behavior is identical +/// across the three; 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. +/// * SELECT / GetVersion version bytes (the SE050E's appletConfig word +/// clears the RSA_PLAIN and RSA_CRT bits: 0x3f9f vs the SE051's +/// 0x3fff). +/// * GetFreeMemory per-type values. All three parts reply with a 2-byte +/// value (the SE050E clamps PERSISTENT at 0x7FFF); the v04.07.01 +/// middleware parses U16 for every applet below minor version 0x10 +/// and U32 only for the SE052F family. /// * GetRandom maximum request size (880 bytes on the SE050C, 1018 on -/// the SE051). +/// the SE051 and SE050E). /// * 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). +/// * RSA: the SE050E refuses key generation with SW 0x6985 and key +/// import with SW 0x6A80; wolfSSL's wolfcrypt suite consequently +/// fails its RSA test with WC_HW_E against real SE050E silicon. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppletVersion { @@ -45,16 +53,21 @@ pub enum AppletVersion { V3_1_1, /// SE051, applet 7.2.0 (ATR historical bytes "eSE051"). Default. V7_2_0, + /// SE050E, applet 7.2.0 with RSA disabled in appletConfig (ATR + /// historical bytes also read "eSE051" on real parts). + V7_2_0E, } 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" + /// variable. Accepts "3", "3.1.1" (SE050C); "e", "se050e", "7.2.0e" + /// (SE050E, any value ending in "e"); 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, + Ok(v) if v.to_ascii_lowercase().ends_with('e') => AppletVersion::V7_2_0E, _ => AppletVersion::V7_2_0, } } @@ -63,32 +76,46 @@ impl AppletVersion { /// 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. + /// 07 02 00 3f ff ff ff, SE050E applet 7.2.0 returns + /// 07 02 00 3f 9f ff ff (appletConfig clears RSA_PLAIN 0x0020 + /// and RSA_CRT 0x0040). 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], + AppletVersion::V7_2_0E => [0x07, 0x02, 0x00, 0x3F, 0x9F, 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). + /// SE051 7.2.0 and SE050E). pub fn get_random_max(self) -> usize { match self { AppletVersion::V3_1_1 => 880, - AppletVersion::V7_2_0 => 1018, + AppletVersion::V7_2_0 | AppletVersion::V7_2_0E => 1018, } } + /// Whether the applet supports RSA at all. The SE050E's applet + /// build has the RSA_PLAIN / RSA_CRT feature bits cleared + /// (bench-verified: keygen refuses 0x6985, import refuses 0x6A80). + pub fn supports_rsa(self) -> bool { + !matches!(self, AppletVersion::V7_2_0E) + } + /// 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). + /// parts. All three parts reply with a 2-byte big-endian value; the + /// SE050E reports PERSISTENT clamped at 0x7FFF. (An earlier revision + /// emitted 4 bytes for 7.2.0 after misreading the middleware's + /// SE052F-only U32 parse path; the v04.07.01 middleware rejects + /// TLV values longer than 2 bytes for these applets.) pub fn free_memory_bytes(self, memory_type: u8) -> Option> { - let (persistent, transient_reset, transient_deselect): (u32, u32, u32) = + let (persistent, transient_reset, transient_deselect): (u16, u16, u16) = match self { AppletVersion::V3_1_1 => (31304, 575, 560), AppletVersion::V7_2_0 => (21000, 605, 592), + AppletVersion::V7_2_0E => (32767, 796, 784), }; let value = match memory_type { 0x01 => persistent, @@ -96,9 +123,50 @@ impl AppletVersion { 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(), - }) + Some(value.to_be_bytes().to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_env_tokens() { + // Tests run single-threaded (see CLAUDE.md), so mutating the + // process environment here cannot race another test. + for (token, expected) in [ + ("3", AppletVersion::V3_1_1), + ("3.1.1", AppletVersion::V3_1_1), + ("7.2.0", AppletVersion::V7_2_0), + ("e", AppletVersion::V7_2_0E), + ("E", AppletVersion::V7_2_0E), + ("se050e", AppletVersion::V7_2_0E), + ("7.2.0e", AppletVersion::V7_2_0E), + ("bogus", AppletVersion::V7_2_0), + ] { + std::env::set_var("SE050_SIM_APPLET", token); + assert_eq!(AppletVersion::from_env(), expected, "token {:?}", token); + } + std::env::remove_var("SE050_SIM_APPLET"); + assert_eq!(AppletVersion::from_env(), AppletVersion::V7_2_0); + } + + #[test] + fn test_personality_traits() { + assert!(AppletVersion::V3_1_1.supports_rsa()); + assert!(AppletVersion::V7_2_0.supports_rsa()); + assert!(!AppletVersion::V7_2_0E.supports_rsa()); + // All personalities reply GetFreeMemory as 2-byte values. + for v in [ + AppletVersion::V3_1_1, + AppletVersion::V7_2_0, + AppletVersion::V7_2_0E, + ] { + for mem_type in [0x01u8, 0x02, 0x03] { + assert_eq!(v.free_memory_bytes(mem_type).unwrap().len(), 2); + } + assert!(v.free_memory_bytes(0x04).is_none()); + } } } diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 425d0ea..80ce8ea 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -49,7 +49,7 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { match base_ins { INS_WRITE => match cred_type { P1_EC => handlers::ec::handle_write_ec_key(apdu, store), - P1_RSA => handlers::rsa::handle_write_rsa_key(apdu, store), + P1_RSA => handlers::rsa::handle_write_rsa_key(apdu, store, version), 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), diff --git a/SE050Sim/se050-sim/src/handlers/curve.rs b/SE050Sim/se050-sim/src/handlers/curve.rs index ba0424b..e8ba18f 100644 --- a/SE050Sim/se050-sim/src/handlers/curve.rs +++ b/SE050Sim/se050-sim/src/handlers/curve.rs @@ -70,9 +70,11 @@ pub fn handle_create( } 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 SE051 and SE050E: re-creating an + // existing curve is refused and the curve is left intact. + AppletVersion::V7_2_0 | AppletVersion::V7_2_0E => { + 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 diff --git a/SE050Sim/se050-sim/src/handlers/management.rs b/SE050Sim/se050-sim/src/handlers/management.rs index dc67ff0..8f9524c 100644 --- a/SE050Sim/se050-sim/src/handlers/management.rs +++ b/SE050Sim/se050-sim/src/handlers/management.rs @@ -42,10 +42,9 @@ fn handle_get_version(version: AppletVersion) -> ApduResponse { ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &version.version_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. +/// GetFreeMemory: Tag1 = memory type (1B). All bench parts (3.1.1, +/// 7.2.0, SE050E) reply with a 2-byte value; per-type values as +/// measured on the bench parts (see AppletVersion::free_memory_bytes). fn handle_get_free_memory(apdu: &ParsedApdu, version: AppletVersion) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -62,8 +61,8 @@ fn handle_get_free_memory(apdu: &ParsedApdu, version: AppletVersion) -> ApduResp /// 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. +/// per-applet maximum (880 bytes on 3.1.1, 1018 on 7.2.0 and the +/// SE050E), all bench-verified. fn handle_get_random(apdu: &ParsedApdu, version: AppletVersion) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, @@ -115,12 +114,14 @@ mod tests { #[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). + // Bench-verified: size 0 fails on all parts; the cap is 880 + // on the SE050C (3.1.1) and 1018 on the SE051 (7.2.0) and + // SE050E. let mut store = ObjectStore::new(); for (version, max) in [ (AppletVersion::V3_1_1, 880u16), (AppletVersion::V7_2_0, 1018u16), + (AppletVersion::V7_2_0E, 1018u16), ] { let resp = handle(&random_apdu(0), &mut store, version); assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); @@ -136,7 +137,8 @@ mod tests { #[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. + // SE051 7.2.0 -> 07 02 00 3f ff ff ff, SE050E 7.2.0 -> + // 07 02 00 3f 9f ff ff (appletConfig without the RSA bits). let mut store = ObjectStore::new(); let apdu = ParsedApdu { cla: 0x80, ins: INS_MGMT, p1: P1_DEFAULT, p2: P2_VERSION, @@ -148,23 +150,36 @@ mod tests { 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]); + let resp = handle(&apdu, &mut store, AppletVersion::V7_2_0E); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value, [0x07, 0x02, 0x00, 0x3F, 0x9F, 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. + fn test_get_free_memory_values_per_applet() { + // All bench parts reply with a 2-byte value; per-type values + // as measured on the bench (SE050E clamps PERSISTENT at + // 0x7FFF). The v04.07.01 middleware rejects TLV values longer + // than 2 bytes for these applets (tlvGet_U16), so a 4-byte + // reply would make Se05x_API_GetFreeMemory fail host-side. 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); + for (version, persistent) in [ + (AppletVersion::V3_1_1, 31304u16), + (AppletVersion::V7_2_0, 21000u16), + (AppletVersion::V7_2_0E, 32767u16), + ] { + 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, version); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value.len(), 2, "{:?}", version); + assert_eq!( + u16::from_be_bytes([tlvs[0].value[0], tlvs[0].value[1]]), + persistent, + "{:?}", version + ); + } } } diff --git a/SE050Sim/se050-sim/src/handlers/rsa.rs b/SE050Sim/se050-sim/src/handlers/rsa.rs index ae6f8a6..346d6e2 100644 --- a/SE050Sim/se050-sim/src/handlers/rsa.rs +++ b/SE050Sim/se050-sim/src/handlers/rsa.rs @@ -50,13 +50,32 @@ const TAG_RSA_PUB_MOD: u8 = 0x4A; // TAG_10 /// simulator accumulates components in `RSAKeyPair::staged` and materializes /// a PKCS#1 DER once the set is sufficient (N+E+D, or CRT primes+E+N). /// -/// P1 = `P1_RSA | key_part`, P2 = `rsa_format`. Tags 3–10 per the map above. -pub fn handle_write_rsa_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// P1 = `P1_RSA | key_part`, P2 = `rsa_format`. Tags 3-10 per the map above. +/// +/// On a personality without RSA support (SE050E) the applet refuses +/// keygen with SW 0x6985 and any APDU carrying key material with SW +/// 0x6A80 (both bench-verified on real SE050E silicon). +pub fn handle_write_rsa_key( + apdu: &ParsedApdu, + store: &mut ObjectStore, + version: crate::applet::AppletVersion, +) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), }; + if !version.supports_rsa() { + let has_component = tlvs.iter().any(|t| { + (TAG_RSA_P..=TAG_RSA_PUB_MOD).contains(&t.tag) + }); + return ApduResponse::error(if has_component { + SW_WRONG_DATA + } else { + SW_CONDITIONS_NOT_SATISFIED + }); + } + let obj_id = match tlv::find_tlv(&tlvs, TAG_1) { Some(t) if t.value.len() == 4 => { let mut id = [0u8; 4]; @@ -536,4 +555,51 @@ mod tests { .expect("decrypt"); assert_eq!(recovered, plaintext); } + + fn write_rsa_apdu(data: Vec) -> crate::apdu::ParsedApdu { + crate::apdu::ParsedApdu { + cla: 0x80, + ins: crate::apdu::INS_WRITE, + p1: crate::apdu::P1_RSA, + p2: crate::apdu::P2_DEFAULT, + data, + le: None, + } + } + + #[test] + fn se050e_refuses_rsa_keygen_and_import() { + // Bench-verified on real SE050E silicon: keygen (size-only + // APDU) refuses 0x6985; any APDU carrying key material + // refuses 0x6A80. The SE051 personality still serves both. + use crate::applet::AppletVersion; + let mut store = ObjectStore::new(); + let obj_id = [0x7Fu8, 0x40, 0x00, 0x01]; + + let mut keygen = Vec::new(); + keygen.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + keygen.extend_from_slice(&Tlv::new(TAG_2, &2048u16.to_be_bytes()).encode()); + let resp = handle_write_rsa_key( + &write_rsa_apdu(keygen), &mut store, AppletVersion::V7_2_0E); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(!store.exists(&obj_id)); + + let mut import = Vec::new(); + import.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + import.extend_from_slice(&Tlv::new(TAG_2, &2048u16.to_be_bytes()).encode()); + import.extend_from_slice(&Tlv::new(TAG_RSA_PUB_EXP, &[0x01, 0x00, 0x01]).encode()); + import.extend_from_slice(&Tlv::new(TAG_RSA_PUB_MOD, &[0xB1; 256]).encode()); + let resp = handle_write_rsa_key( + &write_rsa_apdu(import), &mut store, AppletVersion::V7_2_0E); + assert_eq!(resp.sw, SW_WRONG_DATA); + assert!(!store.exists(&obj_id)); + + let mut keygen_1k = Vec::new(); + keygen_1k.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + keygen_1k.extend_from_slice(&Tlv::new(TAG_2, &1024u16.to_be_bytes()).encode()); + let resp = handle_write_rsa_key( + &write_rsa_apdu(keygen_1k), &mut store, AppletVersion::V7_2_0); + assert_eq!(resp.sw, 0x9000, "SE051 personality keeps RSA keygen"); + assert!(store.exists(&obj_id)); + } } From 6953974094f0dce98342bafb688687cf57a7fc14 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Fri, 7 Aug 2026 16:25:23 +0100 Subject: [PATCH 2/2] SE050Sim: address review - SE050E v7 read behavior, from_token parsing The dispatch v7 flag was computed as version == V7_2_0, so the SE050E personality would have received the 3.1.1 generic ReadType codes. Bench runs show the SE050E reports the same curve-specific codes as the SE051 (P-256 pair 0x29, P-521 pair 0x31); add is_v7() covering both 7.2 parts and use it in dispatch. Split token parsing out of from_env into from_token with the ending-in-e rule taking precedence over the leading-3 rule, matching the documented behavior for tokens like 3e, and rewrite the parsing test against from_token so it no longer mutates the process environment. --- SE050Sim/se050-sim/src/applet.rs | 58 ++++++++++++++++++++++-------- SE050Sim/se050-sim/src/dispatch.rs | 2 +- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet.rs b/SE050Sim/se050-sim/src/applet.rs index d4ee4df..9fda316 100644 --- a/SE050Sim/se050-sim/src/applet.rs +++ b/SE050Sim/se050-sim/src/applet.rs @@ -60,15 +60,28 @@ pub enum AppletVersion { impl AppletVersion { /// Read the personality from the SE050_SIM_APPLET environment - /// variable. Accepts "3", "3.1.1" (SE050C); "e", "se050e", "7.2.0e" - /// (SE050E, any value ending in "e"); and "7", "7.2", "7.2.0" - /// (SE051). Unset or unrecognized values select 7.2.0, matching the - /// version the simulator has always advertised. + /// variable. Unset selects 7.2.0, matching the version the + /// simulator has always advertised; see `from_token` for the + /// accepted values. pub fn from_env() -> Self { match std::env::var("SE050_SIM_APPLET") { - Ok(v) if v.starts_with('3') => AppletVersion::V3_1_1, - Ok(v) if v.to_ascii_lowercase().ends_with('e') => AppletVersion::V7_2_0E, - _ => AppletVersion::V7_2_0, + Ok(v) => Self::from_token(&v), + Err(_) => AppletVersion::V7_2_0, + } + } + + /// Parse a personality token. Accepts "e", "se050e", "7.2.0e" -- + /// any value ending in "e" or "E" -- for the SE050E; "3", "3.1.1" + /// for the SE050C; and anything else ("7", "7.2", "7.2.0", + /// unrecognized) for the SE051. + pub fn from_token(token: &str) -> Self { + let t = token.trim().to_ascii_lowercase(); + if t.ends_with('e') { + AppletVersion::V7_2_0E + } else if t.starts_with('3') { + AppletVersion::V3_1_1 + } else { + AppletVersion::V7_2_0 } } @@ -104,6 +117,15 @@ impl AppletVersion { !matches!(self, AppletVersion::V7_2_0E) } + /// Whether this is a 7.2-generation applet (SE051 or SE050E). + /// Gates the 7.2-specific read behaviors: curve-specific ReadType + /// codes and ReadObjectAttributes support. Bench-verified: the + /// SE050E reports the same curve-specific type codes as the SE051 + /// (P-256 pair 0x29, P-521 pair 0x31). + pub fn is_v7(self) -> bool { + !matches!(self, AppletVersion::V3_1_1) + } + /// GetFreeMemory reply for a memory type, as measured on the bench /// parts. All three parts reply with a 2-byte big-endian value; the /// SE050E reports PERSISTENT clamped at 0x7FFF. (An earlier revision @@ -132,24 +154,27 @@ mod tests { use super::*; #[test] - fn test_from_env_tokens() { - // Tests run single-threaded (see CLAUDE.md), so mutating the - // process environment here cannot race another test. + fn test_from_token_mapping() { for (token, expected) in [ ("3", AppletVersion::V3_1_1), ("3.1.1", AppletVersion::V3_1_1), + ("7", AppletVersion::V7_2_0), ("7.2.0", AppletVersion::V7_2_0), ("e", AppletVersion::V7_2_0E), ("E", AppletVersion::V7_2_0E), ("se050e", AppletVersion::V7_2_0E), + ("SE050E", AppletVersion::V7_2_0E), ("7.2.0e", AppletVersion::V7_2_0E), + // The ending-in-e rule takes precedence over the leading-3 + // rule, matching the documented behavior. + ("3e", AppletVersion::V7_2_0E), + (" se050e ", AppletVersion::V7_2_0E), ("bogus", AppletVersion::V7_2_0), + ("", AppletVersion::V7_2_0), ] { - std::env::set_var("SE050_SIM_APPLET", token); - assert_eq!(AppletVersion::from_env(), expected, "token {:?}", token); + assert_eq!(AppletVersion::from_token(token), expected, + "token {:?}", token); } - std::env::remove_var("SE050_SIM_APPLET"); - assert_eq!(AppletVersion::from_env(), AppletVersion::V7_2_0); } #[test] @@ -157,6 +182,11 @@ mod tests { assert!(AppletVersion::V3_1_1.supports_rsa()); assert!(AppletVersion::V7_2_0.supports_rsa()); assert!(!AppletVersion::V7_2_0E.supports_rsa()); + // The SE050E is a 7.2-generation part: it must get the + // curve-specific ReadType codes, not the 3.x generic ones. + assert!(!AppletVersion::V3_1_1.is_v7()); + assert!(AppletVersion::V7_2_0.is_v7()); + assert!(AppletVersion::V7_2_0E.is_v7()); // All personalities reply GetFreeMemory as 2-byte values. for v in [ AppletVersion::V3_1_1, diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 80ce8ea..9dbcf91 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -31,7 +31,7 @@ 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; + let v7 = version.is_v7(); // SELECT command (CLA=0x00, INS=0xA4) if apdu.cla == 0x00 && apdu.ins == 0xA4 {