From 1864a3ace5c7be856d8da5c7a9bbc01ba41cfd25 Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 27 Aug 2026 12:32:16 +0100 Subject: [PATCH 1/2] SE050Sim: add the SE052F applet personality Bench-characterized an SE052F (IoT applet 7.2.22) and added it as a fourth personality, selected with SE050_SIM_APPLET=f / 52f / se052f / 7.2.22. The suffix rules are checked before the leading-digit rule, so "3f" selects the SE052F just as "3e" selects the SE050E. The important part is the GetFreeMemory reply width. Se05x_API_GetFreeMemory picks its parser from SE05X_CHECK_52F_VERSION, which tests (applet_version >> 8) & 0xFF for 0x10..=0x1F. Since applet_version is major<<24 | minor<<16 | patch<<8, that byte is the applet patch number, which NXP overloads to mark the SE052 family. Applet 7.2.22 (patch 0x16) therefore takes the tlvGet_U32 branch while 3.1.1 / 7.2.0 / SE050E (patch 0x00) take tlvGet_U16. Each parser rejects the other's length outright, so a mismatched width fails the host call rather than degrading quietly. free_memory_bytes now carries u32 values internally and emits 2 or 4 bytes per the new free_memory_is_u32 predicate. The SE052F reports 86336 / 1157 / 1152; PERSISTENT does not fit in a u16 at all, which is why the U32 path exists in the first place. Also modelled, all measured on silicon over Platform SCP03: * version blob 07 02 16 26 f2 ff ff. appletConfig 0x26f2 clears EDDSA, DH_MONT, DES, MIFARE and RFU1 relative to the SE051's 0x3fff, but keeps both RSA bits. * GetRandom cap 1003. Measured over SCP03, which is the only channel a real SE052F serves - the part ships locked and refuses plain GetRandom, so its plain-channel cap could not be measured. * Duplicate CreateECCurve refused 0x6985 with the curve left intact, matching the other 7.2 parts. * Ed25519 and X25519 key writes refused 0x6985 (supports_25519). * RSA below 2048 bits refused 0x6985 (rsa_min_key_bits) and RSA public-key import refused 0x6A80 (supports_rsa_public_import). RSA 2048 CRT generation and RSASign work. handle_write_ec_key takes the applet version so it can gate the 25519 curves; that is the only signature change. Deliberately not modelled: the factory restricted mode (real parts refuse everything but GetVersion until an SCP03 session exists - the object store's scp_required flag and SetPlatformSCPRequest already provide the mechanism if it is ever wanted), the P-256-only factory curve list (the simulator pre-provisions five curves for every personality, and the SE051/SE050E factory lists differ from that default too), and RSA key format, which the simulator does not distinguish for any personality. Tests cover the reply width per personality including the exact byte pattern for the SE052F, the token parsing, the 25519 refusals with the other personalities still serving those curves, and the RSA size and import restrictions. --- SE050Sim/se050-sim/src/applet.rs | 216 ++++++++++++++---- SE050Sim/se050-sim/src/dispatch.rs | 2 +- SE050Sim/se050-sim/src/handlers/curve.rs | 9 +- SE050Sim/se050-sim/src/handlers/ec.rs | 68 +++++- SE050Sim/se050-sim/src/handlers/management.rs | 40 +++- SE050Sim/se050-sim/src/handlers/rsa.rs | 71 ++++++ 6 files changed, 353 insertions(+), 53 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet.rs b/SE050Sim/se050-sim/src/applet.rs index 9fda316..45994fb 100644 --- a/SE050Sim/se050-sim/src/applet.rs +++ b/SE050Sim/se050-sim/src/applet.rs @@ -21,31 +21,39 @@ /// Applet personality selection. /// -/// The simulator can present itself as any of the three parts that were +/// The simulator can present itself as any of the four 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: +/// SE051 running applet 7.2.0, an SE050E running applet 7.2.0 with the +/// RSA feature bits disabled, or an SE052F running applet 7.2.22. Almost +/// all behavior is identical across the four; the differences the +/// simulator models are: /// -/// * 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. +/// * SELECT / GetVersion version bytes. The appletConfig word carries +/// the feature bits: SE051 0x3fff, SE050E 0x3f9f (clears RSA_PLAIN and +/// RSA_CRT), SE052F 0x26f2 (clears EDDSA, DH_MONT, DES, MIFARE and +/// RFU1 but keeps both RSA bits). +/// * GetFreeMemory reply *width* and per-type values. The 3.1.1, 7.2.0 +/// and SE050E parts all reply with a 2-byte value (the SE050E clamps +/// PERSISTENT at 0x7FFF). The SE052F replies with 4 bytes -- its +/// PERSISTENT figure of 86336 does not fit in a U16 at all. See +/// `free_memory_is_u32`. /// * GetRandom maximum request size (880 bytes on the SE050C, 1018 on -/// the SE051 and SE050E). +/// the SE051 and SE050E, 1003 on the SE052F). /// * 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). +/// on 3.x, curve-specific on 7.2 and later). +/// * CreateECCurve on an already existing curve: applets 7.2 and later +/// refuse 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. +/// fails its RSA test with WC_HW_E against real SE050E silicon. The +/// SE052F has RSA but restricts it -- see `rsa_min_key_bits` and +/// `supports_rsa_public_import`. +/// * Ed25519 / X25519: absent on the SE052F, which refuses key writes +/// on those curves with SW 0x6985 (see `supports_25519`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AppletVersion { @@ -56,6 +64,11 @@ pub enum AppletVersion { /// SE050E, applet 7.2.0 with RSA disabled in appletConfig (ATR /// historical bytes also read "eSE051" on real parts). V7_2_0E, + /// SE052F, applet 7.2.22. Keeps RSA (with restrictions) but has no + /// Ed25519 / X25519, and is the only part whose GetFreeMemory reply + /// is 4 bytes wide. Real parts ship locked behind Platform SCP03 and + /// their ATR historical bytes also read "SE051". + V7_2_22F, } impl AppletVersion { @@ -70,13 +83,19 @@ impl AppletVersion { } } - /// 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. + /// Parse a personality token. Accepts "f", "52f", "se052f" -- any + /// value ending in "f" -- plus "7.2.22", for the SE052F; "e", + /// "se050e", "7.2.0e" -- any value ending in "e" -- for the SE050E; + /// "3", "3.1.1" for the SE050C; and anything else ("7", "7.2", + /// "7.2.0", unrecognized) for the SE051. + /// + /// The suffix rules are checked before the leading-digit rule, so + /// "3f" selects the SE052F and "3e" the SE050E. pub fn from_token(token: &str) -> Self { let t = token.trim().to_ascii_lowercase(); - if t.ends_with('e') { + if t.ends_with('f') || t.starts_with("7.2.22") { + AppletVersion::V7_2_22F + } else if t.ends_with('e') { AppletVersion::V7_2_0E } else if t.starts_with('3') { AppletVersion::V3_1_1 @@ -91,25 +110,69 @@ impl AppletVersion { /// 03 01 01 6f ff 01 0b, SE051 applet 7.2.0 returns /// 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). + /// and RSA_CRT 0x0040), SE052F applet 7.2.22 returns + /// 07 02 16 26 f2 ff ff (appletConfig clears EDDSA 0x0004, + /// DH_MONT 0x0008, DES 0x0100, MIFARE 0x0800, RFU1 0x1000 and the + /// undocumented bit 0x0001, keeping both RSA bits). + /// + /// The patch byte matters beyond cosmetics: the v04.07.01 + /// middleware's SE05X_CHECK_52F_VERSION treats a patch of + /// 0x10..=0x1F as the SE052 family, which switches GetFreeMemory to + /// a U32 parse (see `free_memory_is_u32`). 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], + AppletVersion::V7_2_22F => [0x07, 0x02, 0x16, 0x26, 0xF2, 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 and SE050E). + /// SE051 7.2.0 and SE050E, 1003 on the SE052F). + /// + /// The SE052F figure was measured over Platform SCP03, which is the + /// only channel a real SE052F serves -- the part ships locked and + /// refuses plain GetRandom outright, so its plain-channel cap could + /// not be measured. R-MAC and R-ENC overhead is the likely reason it + /// sits below the 1018 the other 7.2 parts allow in plain mode. pub fn get_random_max(self) -> usize { match self { AppletVersion::V3_1_1 => 880, AppletVersion::V7_2_0 | AppletVersion::V7_2_0E => 1018, + AppletVersion::V7_2_22F => 1003, } } + /// Whether the applet supports the 25519 curves (Ed25519 signing and + /// X25519 key agreement). The SE052F's appletConfig clears both + /// EDDSA (0x0004) and DH_MONT (0x0008); bench-verified that key + /// generation on either curve is refused with SW 0x6985. Key import + /// on those curves is refused the same way here, which follows from + /// the feature bits being clear but was not separately measured. + pub fn supports_25519(self) -> bool { + !matches!(self, AppletVersion::V7_2_22F) + } + + /// Smallest RSA key size the applet will generate. Bench-verified on + /// the SE052F: 1024-bit CRT key generation is refused 0x6985 while + /// 2048-bit CRT generation (and RSASign with it) works. + pub fn rsa_min_key_bits(self) -> u16 { + match self { + AppletVersion::V7_2_22F => 2048, + _ => 1024, + } + } + + /// Whether the applet accepts an RSA *public* key import (modulus + + /// exponent). Bench-verified on the SE052F: refused with SW 0x6A80, + /// while key generation on the same part works. Private-component + /// import was not exercised on that part and is left enabled. + pub fn supports_rsa_public_import(self) -> bool { + !matches!(self, AppletVersion::V7_2_22F) + } + /// 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). @@ -126,18 +189,40 @@ impl AppletVersion { !matches!(self, AppletVersion::V3_1_1) } + /// Whether GetFreeMemory replies with a 4-byte (U32) value instead + /// of the usual 2-byte (U16) one. + /// + /// `Se05x_API_GetFreeMemory` picks its parser from + /// `SE05X_CHECK_52F_VERSION(applet_version)`, which tests + /// `(applet_version >> 8) & 0xFF` for the range 0x10..=0x1F. Because + /// `applet_version` is `major<<24 | minor<<16 | patch<<8`, that byte + /// is the applet *patch* number, which NXP overloads to mark the + /// SE052 family. Applet 7.2.22 (patch 0x16) therefore takes the + /// `tlvGet_U32` branch, while 3.1.1 / 7.2.0 / SE050E (patch 0x00) + /// take `tlvGet_U16`. + /// + /// Getting this wrong is a hard failure rather than a subtle one: + /// `tlvGet_U32` only accepts a 4-byte TLV value and `tlvGet_U16` + /// only a 2-byte one, so a mismatched width makes the host call fail + /// outright. Bench-confirmed on real SE052F silicon, where the call + /// returns 0x9000 through the U32 path. + pub fn free_memory_is_u32(self) -> bool { + matches!(self, AppletVersion::V7_2_22F) + } + /// 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 - /// 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.) + /// parts. The 3.1.1, 7.2.0 and SE050E parts reply with a 2-byte + /// big-endian value (the SE050E reports PERSISTENT clamped at + /// 0x7FFF); the SE052F replies with 4 bytes, and its PERSISTENT + /// figure of 86336 could not be expressed in 2 bytes at all, which + /// is precisely why the U32 path exists. pub fn free_memory_bytes(self, memory_type: u8) -> Option> { - let (persistent, transient_reset, transient_deselect): (u16, u16, u16) = + 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), AppletVersion::V7_2_0E => (32767, 796, 784), + AppletVersion::V7_2_22F => (86336, 1157, 1152), }; let value = match memory_type { 0x01 => persistent, @@ -145,7 +230,11 @@ impl AppletVersion { 0x03 => transient_deselect, _ => return None, }; - Some(value.to_be_bytes().to_vec()) + Some(if self.free_memory_is_u32() { + value.to_be_bytes().to_vec() + } else { + (value as u16).to_be_bytes().to_vec() + }) } } @@ -165,9 +254,17 @@ mod tests { ("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. + ("f", AppletVersion::V7_2_22F), + ("F", AppletVersion::V7_2_22F), + ("52f", AppletVersion::V7_2_22F), + ("se052f", AppletVersion::V7_2_22F), + ("SE052F", AppletVersion::V7_2_22F), + ("7.2.22", AppletVersion::V7_2_22F), + (" se052f ", AppletVersion::V7_2_22F), + // The suffix rules take precedence over the leading-3 rule, + // matching the documented behavior. ("3e", AppletVersion::V7_2_0E), + ("3f", AppletVersion::V7_2_22F), (" se050e ", AppletVersion::V7_2_0E), ("bogus", AppletVersion::V7_2_0), ("", AppletVersion::V7_2_0), @@ -182,21 +279,54 @@ 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. + // The SE052F keeps both RSA appletConfig bits, unlike the E. + assert!(AppletVersion::V7_2_22F.supports_rsa()); + // The SE050E and SE052F are 7.2-generation parts: they 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, - AppletVersion::V7_2_0, - AppletVersion::V7_2_0E, + assert!(AppletVersion::V7_2_22F.is_v7()); + // 25519 curves: present everywhere except the SE052F. + assert!(AppletVersion::V3_1_1.supports_25519()); + assert!(AppletVersion::V7_2_0.supports_25519()); + assert!(AppletVersion::V7_2_0E.supports_25519()); + assert!(!AppletVersion::V7_2_22F.supports_25519()); + // RSA restrictions are SE052F-only. + assert_eq!(AppletVersion::V7_2_0.rsa_min_key_bits(), 1024); + assert_eq!(AppletVersion::V7_2_22F.rsa_min_key_bits(), 2048); + assert!(AppletVersion::V7_2_0.supports_rsa_public_import()); + assert!(!AppletVersion::V7_2_22F.supports_rsa_public_import()); + for mem_type in [0x01u8, 0x02, 0x03] { + assert!(AppletVersion::V7_2_22F.free_memory_bytes(mem_type).is_some()); + } + } + + #[test] + fn test_free_memory_width_per_applet() { + // A mismatched width is a hard host-side failure: tlvGet_U16 + // rejects a 4-byte value and tlvGet_U32 rejects a 2-byte one. + for (v, want_len) in [ + (AppletVersion::V3_1_1, 2usize), + (AppletVersion::V7_2_0, 2), + (AppletVersion::V7_2_0E, 2), + (AppletVersion::V7_2_22F, 4), ] { for mem_type in [0x01u8, 0x02, 0x03] { - assert_eq!(v.free_memory_bytes(mem_type).unwrap().len(), 2); + assert_eq!( + v.free_memory_bytes(mem_type).unwrap().len(), + want_len, + "{:?} type {:#04x}", v, mem_type + ); } - assert!(v.free_memory_bytes(0x04).is_none()); + assert!(v.free_memory_bytes(0x04).is_none(), "{:?}", v); } + // The SE052F's PERSISTENT figure does not fit in a U16, which is + // why the applet reports it over four bytes in the first place. + let bytes = AppletVersion::V7_2_22F.free_memory_bytes(0x01).unwrap(); + assert_eq!(bytes, vec![0x00, 0x01, 0x51, 0x40]); + assert_eq!(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + 86336); + assert!(86336u32 > u16::MAX as u32); } } diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 6cf222b..4f67663 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -54,7 +54,7 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) -> match base_ins { INS_WRITE => match cred_type { - P1_EC => handlers::ec::handle_write_ec_key(apdu, store), + P1_EC => handlers::ec::handle_write_ec_key(apdu, store, version), 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), diff --git a/SE050Sim/se050-sim/src/handlers/curve.rs b/SE050Sim/se050-sim/src/handlers/curve.rs index e8ba18f..ebe4c35 100644 --- a/SE050Sim/se050-sim/src/handlers/curve.rs +++ b/SE050Sim/se050-sim/src/handlers/curve.rs @@ -70,9 +70,12 @@ pub fn handle_create( } if store.curve_exists(curve_id) { return match version { - // 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 => { + // Bench-verified on the SE051, SE050E and SE052F: + // re-creating an existing curve is refused and the curve is + // left intact. + AppletVersion::V7_2_0 + | AppletVersion::V7_2_0E + | AppletVersion::V7_2_22F => { ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED) } // Bench-verified on the SE050C: the duplicate create is diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index 1ed552b..301db78 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -48,7 +48,11 @@ fn pad_hash(data: &[u8], scalar_len: usize) -> Vec { } /// Handle WRITE EC key command (key generation when P2=Default and no private key data). -pub fn handle_write_ec_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +pub fn handle_write_ec_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), @@ -73,6 +77,17 @@ pub fn handle_write_ec_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRe _ => return ApduResponse::error(SW_WRONG_DATA), }; + // The SE052F's appletConfig clears both the EDDSA and DH_MONT + // feature bits, and the part refuses key generation on either 25519 + // curve with 0x6985 (bench-verified). Imports are refused here too, + // which follows from the feature being absent rather than from a + // separate measurement. + if matches!(curve, ECCurve::Ed25519 | ECCurve::Curve25519) + && !version.supports_25519() + { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + // 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 @@ -1270,3 +1285,54 @@ mod test_ed25519_vector { "Ed25519 signature mismatch for empty message"); } } + +#[cfg(test)] +mod test_se052f_curves { + use super::*; + use crate::applet::AppletVersion; + + fn keygen_apdu(obj_id: [u8; 4], curve_byte: u8) -> ParsedApdu { + let mut data = Vec::new(); + data.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + data.extend_from_slice(&Tlv::new(TAG_2, &[curve_byte]).encode()); + ParsedApdu { + cla: 0x80, + ins: crate::apdu::INS_WRITE, + p1: crate::apdu::P1_EC | P1_KEY_PAIR, + p2: crate::apdu::P2_DEFAULT, + data, + le: None, + } + } + + #[test] + fn se052f_refuses_25519_curves() { + // Bench-verified on real SE052F silicon: its appletConfig + // (0x26f2) clears EDDSA and DH_MONT, and key generation on + // either 25519 curve is refused 0x6985. NIST curves still work, + // and the other personalities keep serving 25519. + let mut store = ObjectStore::new(); + let obj_id = [0x7Fu8, 0x50, 0x00, 0x01]; + + for (name, curve_byte) in [("Ed25519", 0x40u8), ("X25519", 0x41)] { + let resp = handle_write_ec_key( + &keygen_apdu(obj_id, curve_byte), &mut store, + AppletVersion::V7_2_22F); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "{}", name); + assert!(!store.exists(&obj_id), "{} must not be created", name); + + let resp = handle_write_ec_key( + &keygen_apdu(obj_id, curve_byte), &mut store, + AppletVersion::V7_2_0); + assert_eq!(resp.sw, 0x9000, "{} on the SE051 personality", name); + assert!(store.exists(&obj_id)); + store.remove(&obj_id); + } + + // P-256 (curve id 0x03) is unaffected on the SE052F. + let resp = handle_write_ec_key( + &keygen_apdu(obj_id, 0x03), &mut store, AppletVersion::V7_2_22F); + assert_eq!(resp.sw, 0x9000, "SE052F still generates NIST keys"); + assert!(store.exists(&obj_id)); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/management.rs b/SE050Sim/se050-sim/src/handlers/management.rs index 343d1ba..852f206 100644 --- a/SE050Sim/se050-sim/src/handlers/management.rs +++ b/SE050Sim/se050-sim/src/handlers/management.rs @@ -155,6 +155,9 @@ mod tests { (AppletVersion::V3_1_1, 880u16), (AppletVersion::V7_2_0, 1018u16), (AppletVersion::V7_2_0E, 1018u16), + // SE052F, measured over Platform SCP03 (the only channel a + // real SE052F serves). + (AppletVersion::V7_2_22F, 1003u16), ] { let resp = handle(&random_apdu(0), &mut store, version); assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); @@ -186,15 +189,20 @@ mod tests { 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]); + let resp = handle(&apdu, &mut store, AppletVersion::V7_2_22F); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + assert_eq!(tlvs[0].value, [0x07, 0x02, 0x16, 0x26, 0xF2, 0xFF, 0xFF]); } #[test] 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. + // Per-type values as measured on the bench (the SE050E clamps + // PERSISTENT at 0x7FFF). The reply width is applet-dependent and + // load-bearing: the v04.07.01 middleware parses these applets + // with tlvGet_U16, which rejects any TLV value longer than 2 + // bytes, so a 4-byte reply here would make + // Se05x_API_GetFreeMemory fail host-side. The SE052F is the + // opposite case and is covered separately below. let mut store = ObjectStore::new(); for (version, persistent) in [ (AppletVersion::V3_1_1, 31304u16), @@ -215,4 +223,26 @@ mod tests { ); } } + + #[test] + fn test_get_free_memory_is_u32_on_se052f() { + // Applet 7.2.22 has patch byte 0x16, so SE05X_CHECK_52F_VERSION + // routes the host to tlvGet_U32, which only accepts a 4-byte + // value. Bench-confirmed on SE052F silicon: 86336/1157/1152, + // and PERSISTENT is above 0xFFFF so it could not be reported + // over two bytes at all. + let mut store = ObjectStore::new(); + for (mem_type, expected) in [(0x01u8, 86336u32), (0x02, 1157), (0x03, 1152)] { + let apdu = ParsedApdu { + cla: 0x80, ins: INS_MGMT, p1: P1_DEFAULT, p2: P2_MEMORY, + data: vec![TAG_1, 0x01, mem_type], le: None, + }; + let resp = handle(&apdu, &mut store, AppletVersion::V7_2_22F); + assert_eq!(resp.sw, 0x9000); + let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); + let v = &tlvs[0].value; + assert_eq!(v.len(), 4, "memory type {:#04x}", mem_type); + assert_eq!(u32::from_be_bytes([v[0], v[1], v[2], v[3]]), expected); + } + } } diff --git a/SE050Sim/se050-sim/src/handlers/rsa.rs b/SE050Sim/se050-sim/src/handlers/rsa.rs index 346d6e2..46051ef 100644 --- a/SE050Sim/se050-sim/src/handlers/rsa.rs +++ b/SE050Sim/se050-sim/src/handlers/rsa.rs @@ -76,6 +76,13 @@ pub fn handle_write_rsa_key( }); } + // The SE052F has RSA but refuses a public-key import (modulus + + // exponent) with 0x6A80, while generating a key pair on the same + // part works. Bench-verified. + if apdu.key_type() == P1_PUBLIC_KEY && !version.supports_rsa_public_import() { + return ApduResponse::error(SW_WRONG_DATA); + } + let obj_id = match tlv::find_tlv(&tlvs, TAG_1) { Some(t) if t.value.len() == 4 => { let mut id = [0u8; 4]; @@ -115,6 +122,11 @@ pub fn handle_write_rsa_key( if ![1024, 2048, 3072, 4096].contains(&key_size_usize) { return ApduResponse::error(SW_WRONG_DATA); } + // The SE052F refuses anything below 2048 bits with 0x6985 + // (bench-verified: 1024-bit CRT generation fails, 2048 works). + if key_size_bits < version.rsa_min_key_bits() { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } let Ok(private_key) = RsaPrivateKey::new(&mut OsRng, key_size_usize) else { return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); }; @@ -602,4 +614,63 @@ mod tests { assert_eq!(resp.sw, 0x9000, "SE051 personality keeps RSA keygen"); assert!(store.exists(&obj_id)); } + + fn write_rsa_pub_apdu(data: Vec) -> crate::apdu::ParsedApdu { + crate::apdu::ParsedApdu { + cla: 0x80, + ins: crate::apdu::INS_WRITE, + p1: crate::apdu::P1_RSA | crate::apdu::P1_PUBLIC_KEY, + p2: crate::apdu::P2_DEFAULT, + data, + le: None, + } + } + + #[test] + fn se052f_rsa_restrictions() { + // Bench-verified on real SE052F silicon: RSA is present (unlike + // the SE050E) but 1024-bit generation refuses 0x6985, 2048-bit + // generation works, and a public-key import refuses 0x6A80. + use crate::applet::AppletVersion; + let mut store = ObjectStore::new(); + let obj_id = [0x7Fu8, 0x40, 0x00, 0x02]; + + 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_22F); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, + "SE052F refuses RSA below 2048 bits"); + assert!(!store.exists(&obj_id)); + + let mut pub_import = Vec::new(); + pub_import.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + pub_import.extend_from_slice(&Tlv::new(TAG_2, &2048u16.to_be_bytes()).encode()); + pub_import.extend_from_slice( + &Tlv::new(TAG_RSA_PUB_EXP, &[0x01, 0x00, 0x01]).encode()); + pub_import.extend_from_slice(&Tlv::new(TAG_RSA_PUB_MOD, &[0xB1; 256]).encode()); + let resp = handle_write_rsa_key( + &write_rsa_pub_apdu(pub_import.clone()), &mut store, + AppletVersion::V7_2_22F); + assert_eq!(resp.sw, SW_WRONG_DATA, + "SE052F refuses RSA public key import"); + assert!(!store.exists(&obj_id)); + + // The same import is served by the SE051 personality. + let resp = handle_write_rsa_key( + &write_rsa_pub_apdu(pub_import), &mut store, AppletVersion::V7_2_0); + assert_eq!(resp.sw, 0x9000); + assert!(store.exists(&obj_id)); + + // 2048-bit generation works on the SE052F. + let obj_2k = [0x7Fu8, 0x40, 0x00, 0x03]; + let mut keygen_2k = Vec::new(); + keygen_2k.extend_from_slice(&Tlv::new(TAG_1, &obj_2k).encode()); + keygen_2k.extend_from_slice(&Tlv::new(TAG_2, &2048u16.to_be_bytes()).encode()); + let resp = handle_write_rsa_key( + &write_rsa_apdu(keygen_2k), &mut store, AppletVersion::V7_2_22F); + assert_eq!(resp.sw, 0x9000, "SE052F generates 2048-bit RSA keys"); + assert!(store.exists(&obj_2k)); + } } From 2bd95a857106a3f85f68ceb32b50acbd34c4ad3b Mon Sep 17 00:00:00 2001 From: Andrew Hutchings Date: Thu, 27 Aug 2026 12:51:17 +0100 Subject: [PATCH 2/2] SE050Sim: address review - SE052F token match, RSA public import Two fixes from review feedback on the SE052F personality. from_token matched the version token with starts_with("7.2.22"), so "7.2.220" and "7.2.22e" also selected the SE052F. Made it an exact match; the "*f" suffix forms are unchanged, and "7.2.22e" now falls through to the ends-with-"e" rule as it should. Both cases are pinned in the token test. The SE052F public-import refusal keyed only on the P1 key-part bits, so an APDU carrying modulus and exponent with those bits clear still created a public key object. Now an APDU that carries a whole public key -- modulus and exponent with no private component -- is refused as well, since it is the same operation reached through a different encoding. The private-key import path is deliberately untouched. The wolfCrypt port imports (E, D, N) across three separate APDUs, so no single APDU there carries modulus and exponent together without private material, and none of them sets the public key part. Refusing on the mere presence of a public component would have broken that flow at its first APDU, so there is a regression test walking the three steps against the SE052F personality. --- SE050Sim/se050-sim/src/applet.rs | 8 +- SE050Sim/se050-sim/src/handlers/rsa.rs | 106 +++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/SE050Sim/se050-sim/src/applet.rs b/SE050Sim/se050-sim/src/applet.rs index 45994fb..ce52bc9 100644 --- a/SE050Sim/se050-sim/src/applet.rs +++ b/SE050Sim/se050-sim/src/applet.rs @@ -84,7 +84,7 @@ impl AppletVersion { } /// Parse a personality token. Accepts "f", "52f", "se052f" -- any - /// value ending in "f" -- plus "7.2.22", for the SE052F; "e", + /// value ending in "f" -- plus exactly "7.2.22", for the SE052F; "e", /// "se050e", "7.2.0e" -- any value ending in "e" -- for the SE050E; /// "3", "3.1.1" for the SE050C; and anything else ("7", "7.2", /// "7.2.0", unrecognized) for the SE051. @@ -93,7 +93,7 @@ impl AppletVersion { /// "3f" selects the SE052F and "3e" the SE050E. pub fn from_token(token: &str) -> Self { let t = token.trim().to_ascii_lowercase(); - if t.ends_with('f') || t.starts_with("7.2.22") { + if t.ends_with('f') || t == "7.2.22" { AppletVersion::V7_2_22F } else if t.ends_with('e') { AppletVersion::V7_2_0E @@ -261,6 +261,10 @@ mod tests { ("SE052F", AppletVersion::V7_2_22F), ("7.2.22", AppletVersion::V7_2_22F), (" se052f ", AppletVersion::V7_2_22F), + // The version token matches exactly: a longer string that + // merely starts with it must not select the SE052F. + ("7.2.220", AppletVersion::V7_2_0), + ("7.2.22e", AppletVersion::V7_2_0E), // The suffix rules take precedence over the leading-3 rule, // matching the documented behavior. ("3e", AppletVersion::V7_2_0E), diff --git a/SE050Sim/se050-sim/src/handlers/rsa.rs b/SE050Sim/se050-sim/src/handlers/rsa.rs index 46051ef..856fb80 100644 --- a/SE050Sim/se050-sim/src/handlers/rsa.rs +++ b/SE050Sim/se050-sim/src/handlers/rsa.rs @@ -76,11 +76,38 @@ pub fn handle_write_rsa_key( }); } - // The SE052F has RSA but refuses a public-key import (modulus + - // exponent) with 0x6A80, while generating a key pair on the same - // part works. Bench-verified. - if apdu.key_type() == P1_PUBLIC_KEY && !version.supports_rsa_public_import() { - return ApduResponse::error(SW_WRONG_DATA); + // The SE052F has RSA but refuses a public-key import with 0x6A80, + // while generating a key pair on the same part works. Bench-verified + // with the key part set to Public and modulus + exponent in one + // APDU. + // + // Two APDU shapes mean "import a public key", and both are refused + // so the restriction cannot be sidestepped: the applet's own + // discriminator (the P1 key-part bits), and an APDU that simply + // carries a complete public key -- modulus and exponent -- with no + // private component. The second form was not exercised on silicon; + // it is refused because it is the same operation reached through a + // different encoding. + // + // The private-key import path is deliberately left alone. The + // wolfCrypt port imports (E, D, N) across three separate APDUs, so + // no single APDU there carries modulus and exponent together + // without private material, and none of them sets the public key + // part. Refusing on the mere presence of a public component would + // break that flow at its first APDU. + if !version.supports_rsa_public_import() { + let has_private_component = [ + TAG_RSA_P, TAG_RSA_Q, TAG_RSA_DP, TAG_RSA_DQ, TAG_RSA_QINV, + TAG_RSA_PRIV, + ] + .iter() + .any(|tag| tlv::find_tlv(&tlvs, *tag).is_some()); + let carries_whole_public_key = tlv::find_tlv(&tlvs, TAG_RSA_PUB_MOD).is_some() + && tlv::find_tlv(&tlvs, TAG_RSA_PUB_EXP).is_some() + && !has_private_component; + if apdu.key_type() == P1_PUBLIC_KEY || carries_whole_public_key { + return ApduResponse::error(SW_WRONG_DATA); + } } let obj_id = match tlv::find_tlv(&tlvs, TAG_1) { @@ -673,4 +700,73 @@ mod tests { assert_eq!(resp.sw, 0x9000, "SE052F generates 2048-bit RSA keys"); assert!(store.exists(&obj_2k)); } + + #[test] + fn se052f_public_import_refused_without_key_part_bits() { + // The key-part bits are the applet's own discriminator, but an + // APDU carrying a whole public key means the same thing even + // with those bits clear. Refusing only on the bits would let a + // caller sidestep the SE052F restriction. + use crate::applet::AppletVersion; + let mut store = ObjectStore::new(); + let obj_id = [0x7Fu8, 0x40, 0x00, 0x04]; + + 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()); + + // write_rsa_apdu leaves P1 as bare P1_RSA: no key-part bits. + let resp = handle_write_rsa_key( + &write_rsa_apdu(import.clone()), &mut store, AppletVersion::V7_2_22F); + assert_eq!(resp.sw, SW_WRONG_DATA); + assert!(!store.exists(&obj_id), "no public key object may be created"); + + // Unaffected on a personality that allows public import. + let resp = handle_write_rsa_key( + &write_rsa_apdu(import), &mut store, AppletVersion::V7_2_0); + assert_eq!(resp.sw, 0x9000); + assert!(store.exists(&obj_id)); + } + + #[test] + fn se052f_staged_private_import_still_works() { + // The wolfCrypt port imports a private key as (E, D, N) across + // three separate APDUs. None of them carries modulus and + // exponent together without private material, so the SE052F + // public-import refusal must not touch this flow -- refusing on + // the mere presence of a public component would break it at the + // very first APDU. + use crate::applet::AppletVersion; + let mut store = ObjectStore::new(); + let obj_id = [0x7Fu8, 0x40, 0x00, 0x05]; + + let step = |tag: u8, value: &[u8]| { + let mut data = Vec::new(); + data.extend_from_slice(&Tlv::new(TAG_1, &obj_id).encode()); + data.extend_from_slice(&Tlv::new(TAG_2, &2048u16.to_be_bytes()).encode()); + data.extend_from_slice(&Tlv::new(tag, value).encode()); + crate::apdu::ParsedApdu { + cla: 0x80, + ins: crate::apdu::INS_WRITE, + p1: crate::apdu::P1_RSA | crate::apdu::P1_KEY_PAIR, + p2: crate::apdu::P2_DEFAULT, + data, + le: None, + } + }; + + for (name, tag, value) in [ + ("exponent", TAG_RSA_PUB_EXP, vec![0x01, 0x00, 0x01]), + ("private exponent", TAG_RSA_PRIV, vec![0xC3; 256]), + ("modulus", TAG_RSA_PUB_MOD, vec![0xB1; 256]), + ] { + let resp = handle_write_rsa_key( + &step(tag, &value), &mut store, AppletVersion::V7_2_22F); + assert_eq!(resp.sw, 0x9000, "SE052F staged import step: {}", name); + } + assert!(store.exists(&obj_id)); + } }