From 69b4caf5d81af8618100267b4486c69fbb86b6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 21 Sep 2026 23:20:50 +0300 Subject: [PATCH] fix: restore MF after fast-init probing so eUICC cards init cleanly (v2.8.1) With an eUICC in the reader the fast init raised SwMatchError twice and fell back to the stock pysim init: pick_profile_no_reset() disables the physical resets that CardProfile.match_with_card() normally performs, so the successful SGP.22 probe leaves the ISD-R ADF selected. RuntimeState then selects MF by FID (00 A4 00 04 02 3F00) from within the ADF, which this card answers with 6A82. - _restore_mf_after_probe() re-selects MF after the profile pick: the cheap select keeps the reset-free path for normal cards, a physical reset covers cards that refuse the MF select from an ADF. - The EID read restores with rs.reset() (soft reset, escalating to a physical reset) instead of rs.soft_reset(), which had the same trap. - tests: FakeScc mf_select_error mode + two _restore_mf_after_probe cases (no reset / exactly one physical reset). --- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/fastinit.py | 21 ++++++++++++++++++++- pysim_simple_server/server.py | 2 +- tests/test_fastinit.py | 19 ++++++++++++++++++- 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 8a969ab..2a46ed2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1436,7 +1436,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '2.8.0'; +const SIMPLE_VERSION = '2.8.1'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== diff --git a/frontend/sw.js b/frontend/sw.js index 8c4756c..b60a1fe 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v225'; +const CACHE = 'simple-v226'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 2974e6f..e4dd4fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.8.0" +version = "2.8.1" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/fastinit.py b/pysim_simple_server/fastinit.py index 6801dae..1d962f7 100644 --- a/pysim_simple_server/fastinit.py +++ b/pysim_simple_server/fastinit.py @@ -80,6 +80,18 @@ def pick_profile_no_reset(scc): scc.reset_card = original_reset +def _restore_mf_after_probe(scc): + """Probing can leave an ADF (e.g. ISD-R on an eUICC) selected. The + RuntimeState construction selects MF by FID, which some cards refuse from + within an ADF (6A82), so restore it here: the cheap select keeps the + reset-free path for normal cards, the physical reset covers the rest.""" + try: + scc.select_file('3f00') + except SwMatchError: + sys.stderr.write('FAST-INIT: MF restore after probing failed; physical reset\n') + scc.reset_card() + + def init_card_fast(sl, skip_card_init=False, wait=True): """Replacement for pySim.app.init_card() that avoids redundant resets. @@ -112,6 +124,11 @@ def _init_card_once(sl, skip_card_init, wait): if profile is None: return None, card + # A successful probe may leave an ADF selected (e.g. ISD-R on an eUICC); + # RuntimeState selects MF by FID and would fail on cards that refuse that + # from within an ADF. + _restore_mf_after_probe(scc) + if generic_card and isinstance(profile, CardProfileUICC): card._adm_chv_num = 0x0A @@ -137,7 +154,9 @@ def _init_card_once(sl, skip_card_init, wait): except SwMatchError: pass finally: - rs.soft_reset() + # rs.reset() tries the reset-free soft reset first and escalates + # to a physical reset when MF cannot be selected from the ADF. + rs.reset() return rs, card diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index fc25ef1..6e618e3 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -27,7 +27,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '2.8.0' +VERSION = '2.8.1' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_fastinit.py b/tests/test_fastinit.py index b46eb1e..914968b 100644 --- a/tests/test_fastinit.py +++ b/tests/test_fastinit.py @@ -24,17 +24,21 @@ from pysim_simple_server.fastinit import ( class FakeScc: - def __init__(self): + def __init__(self, mf_select_error=False): self.sel_ctrl = '0004' self.cla_byte = '00' self.resets = 0 self.selected = [] + self.mf_select_error = mf_select_error def reset_card(self): self.resets += 1 def select_file(self, fid): self.selected.append(fid) + if fid == '3f00' and self.mf_select_error: + # eUICC with the ISD-R ADF selected: MF is not selectable by FID + raise SwMatchError('6a82', '9000') return ('', '9000') def select_adf(self, aid): @@ -56,6 +60,19 @@ class TestPickProfileNoReset(unittest.TestCase): self.assertEqual(scc.resets, 1) +class TestRestoreMfAfterProbe(unittest.TestCase): + def test_normal_card_keeps_the_reset_free_path(self): + scc = FakeScc() + fastinit._restore_mf_after_probe(scc) + self.assertEqual(scc.resets, 0) + self.assertEqual(scc.selected[-1], '3f00') + + def test_adf_selected_card_falls_back_to_a_physical_reset(self): + scc = FakeScc(mf_select_error=True) + fastinit._restore_mf_after_probe(scc) + self.assertEqual(scc.resets, 1) + + class FakeLchan: def __init__(self): self.scc = types.SimpleNamespace(scp=object())