From f8ba5dea2c1375872ef549a9ebe19768ca935237 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 22:13:52 +0300
Subject: [PATCH] fix: use the card's own SimCardCommands for ADM and
AUTHENTICATE (v2.7.20)
/api/verify-adm sent VERIFY with CLA A0 on a UICC (SW 6E00) while
pySim-shell's verify_adm worked: fast init builds the card on its own
SimCardCommands instance, but __main__ kept server.scc at the startup
placeholder left at the SIM defaults; only an equip repointed it.
- __main__ adopts card._scc after init (cat_cla still set on it), so
server.scc carries the card's cla_byte/sel_ctrl from startup on.
- _verify_adm prefers app.rs.lchan[0].scc / app.card._scc, exactly like
pySim-shell's verify_adm, independent of server.scc.
- netsim AUTHENTICATE follows the card class: a UICC gets 00 88 00 81 22
(RAND+AUTN, DB/DC response), a SIM gets A0 88 00 00 10 (RAND only,
SRES+Kc); the 61xx GET RESPONSE uses the same CLA.
- tests: ADM with a stale placeholder scc; 2G builder/parser; SIM-CLA
runner case; help EN/RU and AGENTS updated.
---
frontend/help-ru.html | 2 +-
frontend/help.html | 2 +-
frontend/index.html | 2 +-
frontend/sw.js | 2 +-
pyproject.toml | 2 +-
pysim_simple_server/__main__.py | 5 +++++
pysim_simple_server/netsim.py | 33 +++++++++++++++++++++++++++------
pysim_simple_server/server.py | 11 ++++++++++-
tests/test_adm_verify.py | 17 +++++++++++++++++
tests/test_netsim.py | 28 ++++++++++++++++++++++++++++
10 files changed, 92 insertions(+), 12 deletions(-)
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index 92b031f..ae0693f 100644
--- a/frontend/help-ru.html
+++ b/frontend/help-ru.html
@@ -487,7 +487,7 @@
00 88 00 81 22) с заданными или случайными RAND/AUTN и показ ответа (успех DB или ошибка синхронизации DC с AUTS).00 88 00 81 22, ответ DB — успех или DC — ошибка синхронизации с AUTS), SIM — форму 2G (A0 88 00 00 10, только RAND, ответ SRES + Kc).
Параметры (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка «Домашняя сеть», заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая «Отказ: записать FPLMN») и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN дописывается только сценарием постоянного отказа (TS 31.102 §4.2.16, без дубликатов), а подключение к сети из списка сначала очищает её запись (успешный ручной выбор, TS 23.122); 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.
diff --git a/frontend/help.html b/frontend/help.html index 82c05bb..da68092 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -487,7 +487,7 @@00 88 00 81 22) with the given or random RAND/AUTN and show the response (success DB or synchronisation failure DC with AUTS).00 88 00 81 22, response DB success or DC synchronisation failure with AUTS), a SIM the 2G form (A0 88 00 00 10, RAND only, response SRES + Kc).Parameters (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a Home network button that fills the card’s HPLMN from EF.HPLMNwAcT’s first record, falling back to the IMSI), LAC/Cell ID/TAC/RAC, optional identity values (empty = random: TMSI, GUTI, KSI, KASME, Kc, NAS counts, algorithm, RAND/AUTN), the scenario toggles (including Rejection: write FPLMN) and the churn count/delay. The step log lists every write, ENVELOPE and AUTHENTICATE with its SW. Only UPDATE BINARY/RECORD, ENVELOPE and AUTHENTICATE are sent — FPLMN is appended only by the permanent rejection scenario (TS 31.102 §4.2.16, never duplicated) and an attach to a listed PLMN clears its entry first (successful manual selection, TS 23.122), and the 5GS location files are never written; the writes change the card and are visible to later snapshot comparisons.
diff --git a/frontend/index.html b/frontend/index.html index 874cddf..795fc35 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1414,7 +1414,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.7.19'; +const SIMPLE_VERSION = '2.7.20'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== diff --git a/frontend/sw.js b/frontend/sw.js index 384f42e..3cbc0d0 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v222'; +const CACHE = 'simple-v223'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 6789266..d7ed51c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.7.19" +version = "2.7.20" 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/__main__.py b/pysim_simple_server/__main__.py index 04fbc89..912b5eb 100644 --- a/pysim_simple_server/__main__.py +++ b/pysim_simple_server/__main__.py @@ -138,6 +138,11 @@ def main(): sys.stderr.write('INIT: no card in the reader — server ready; insert a card or press Equip\n') _tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000)) if card is not None: + # Fast init builds the card on its own SimCardCommands instance; + # adopt it so server.scc carries the card's cla_byte/sel_ctrl (the + # placeholder created above is a different object left at the SIM + # defaults, which made VERIFY go out with CLA A0 on a UICC). + scc = card._scc scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0' except Exception: print("Warning: reader/card initialization failed:", file=sys.stderr) diff --git a/pysim_simple_server/netsim.py b/pysim_simple_server/netsim.py index 6712014..c7377fc 100644 --- a/pysim_simple_server/netsim.py +++ b/pysim_simple_server/netsim.py @@ -336,15 +336,30 @@ def build_auth_apdu(rand_hex, autn_hex, cla='00'): rand = bytes.fromhex(_norm_hex(rand_hex, 16)) autn = bytes.fromhex(_norm_hex(autn_hex, 16)) body = bytes([len(rand)]) + rand + bytes([len(autn)]) + autn - return '%s880081%02X%s' % (cla, len(body), body.hex().upper()) + return '%s880081%02X%s' % (cla.upper(), len(body), body.hex().upper()) -def parse_auth_response(data_hex): - """Parse the AUTHENTICATE response: DB (success) or DC (sync failure).""" +def build_auth_apdu_gsm(rand_hex, cla='a0'): + """2G AUTHENTICATE (GSM 11.11 8.8): P1/P2 00, Lc 0x10, RAND only. + + The response is SRES (4 B) followed by Kc (8 B). + """ + rand = bytes.fromhex(_norm_hex(rand_hex, 16)) + return '%s88000010%s' % (cla.upper(), rand.hex().upper()) + + +def parse_auth_response(data_hex, gsm=False): + """Parse the AUTHENTICATE response: DB (success) or DC (sync failure) for + 3G/EPS/5G, SRES||Kc for the 2G SIM command.""" try: data = bytes.fromhex(_norm_hex(data_hex or '')) except ValueError: return None + if gsm: + if len(data) < 12: + return None + return {'type': 'gsm', 'sres': data[0:4].hex().upper(), + 'kc': data[4:12].hex().upper()} if not data or data[0] not in (0xDB, 0xDC): return None out = {'type': 'success' if data[0] == 0xDB else 'synchronisation_failure'} @@ -815,11 +830,17 @@ class NetSimRunner: autn = self.p('autn') or rand_hex(16) if not self.scc: raise StepError('card session not available') - apdu = build_auth_apdu(rand, autn) + # The command follows the card's class: a UICC gets the 3G/EPS/5G + # AUTHENTICATE (P2 81, RAND + AUTN), a SIM the 2G one (P1/P2 00, + # RAND only, SRES + Kc in the response). + cla = (getattr(self.scc, 'cla_byte', None) or '00').upper() + gsm = cla == 'A0' + apdu = (build_auth_apdu_gsm(rand, cla) if gsm + else build_auth_apdu(rand, autn, cla)) data, sw = self.scc._tp.send_apdu(apdu) if sw.startswith('61'): - data, sw = self.scc._tp.send_apdu('00C00000' + sw[2:4]) - parsed = parse_auth_response(data) + data, sw = self.scc._tp.send_apdu(cla + 'C00000' + sw[2:4]) + parsed = parse_auth_response(data, gsm=gsm) self._add('authenticate', data=apdu, response=(data or '').upper(), parsed=parsed, sw=sw, ok=True) diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 1217e77..d3f03d1 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '2.7.19' +VERSION = '2.7.20' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -2698,6 +2698,15 @@ def _verify_adm(scc, app, adm_hex): remaining attempts: every failed VERIFY consumes one, and a blocked ADM cannot be recovered from here (it needs the unblock key). """ + # Use the card's own logical channel, exactly like pySim-shell's + # verify_adm. server.scc can still be the startup placeholder (SIM CLA) + # when no equip has happened yet, and a UICC then answers 6E00. + rs = getattr(app, 'rs', None) + lchan = rs.lchan[0] if rs is not None and getattr(rs, 'lchan', None) else None + card_scc = (getattr(lchan, 'scc', None) + or getattr(getattr(app, 'card', None), '_scc', None)) + if card_scc is not None: + scc = card_scc chv = getattr(getattr(app, 'card', None), '_adm_chv_num', 0x0A) fc = rpad(str(adm_hex).lower(), 16) _data, sw = scc.send_apdu(scc.cla_byte + '2000' + ('%02X' % chv) + '08' + fc) diff --git a/tests/test_adm_verify.py b/tests/test_adm_verify.py index 76b6533..f93fab9 100644 --- a/tests/test_adm_verify.py +++ b/tests/test_adm_verify.py @@ -77,6 +77,23 @@ class AdmVerifyTests(unittest.TestCase): self.assertEqual(out, {'adm': '