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.
This commit is contained in:
@@ -487,7 +487,7 @@
|
||||
<li><strong>Серия переподключений</strong> — реальная → невалидная запись EPSNSC подряд (число циклов и задержка настраиваются).</li>
|
||||
<li><strong>Принято SMS</strong> — инкремент счётчика EF.SMSstatus (чтение-изменение-запись) и, по желанию, перезапись location-файлов.</li>
|
||||
<li><strong>Перенастройка CB</strong> — запись списков CBMI/CBMIR или их очистка (все FF).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — команда AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) с заданными или случайными RAND/AUTN и показ ответа (успех <code class="font-mono text-sm">DB</code> или ошибка синхронизации <code class="font-mono text-sm">DC</code> с AUTS).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — команда AUTHENTICATE с заданными или случайными RAND/AUTN и показ ответа. Команда зависит от класса карты: UICC получает форму 3G/EPS/5G (<code class="font-mono text-sm">00 88 00 81 22</code>, ответ <code class="font-mono text-sm">DB</code> — успех или <code class="font-mono text-sm">DC</code> — ошибка синхронизации с AUTS), SIM — форму 2G (<code class="font-mono text-sm">A0 88 00 00 10</code>, только RAND, ответ SRES + Kc).</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Параметры</strong> (свёрнуты) задают оператора (поиск по мировому списку MCC/MNC с сервера плюс выбор случайного роуминг-оператора и кнопка <strong>«Домашняя сеть»</strong>, заполняющая HPLMN карты из первой записи EF.HPLMNwAcT с откатом на IMSI), LAC/Cell ID/TAC/RAC, необязательные идентификаторы (пусто = случайно: TMSI, GUTI, KSI, KASME, Kc, счётчики NAS, алгоритм, RAND/AUTN), переключатели сценария (включая <strong>«Отказ: записать FPLMN»</strong>) и число циклов/задержку. Журнал шагов показывает каждую запись, ENVELOPE и AUTHENTICATE с их SW. Отправляются только UPDATE BINARY/RECORD, ENVELOPE и AUTHENTICATE — FPLMN дописывается только сценарием постоянного отказа (TS 31.102 §4.2.16, без дубликатов), а подключение к сети из списка сначала очищает её запись (успешный ручной выбор, TS 23.122); 5GS location-файлы не записываются; записи меняют карту и видны в последующих сравнениях снимков.</p>
|
||||
|
||||
|
||||
+1
-1
@@ -487,7 +487,7 @@
|
||||
<li><strong>Churn</strong> — replay real → invalid EPSNSC records back-to-back (count and delay configurable).</li>
|
||||
<li><strong>SMS received</strong> — bump the EF.SMSstatus counter (read-modify-write) and optionally rewrite the location files.</li>
|
||||
<li><strong>CB reconfig</strong> — write the CBMI/CBMIR message-ID lists or clear them (all FF).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — send AUTHENTICATE (3G/EPS/5G, <code class="font-mono text-sm">00 88 00 81 22</code>) with the given or random RAND/AUTN and show the response (success <code class="font-mono text-sm">DB</code> or synchronisation failure <code class="font-mono text-sm">DC</code> with AUTS).</li>
|
||||
<li><strong>AUTHENTICATE</strong> — send AUTHENTICATE with the given or random RAND/AUTN and show the response. The command follows the card class: a UICC gets the 3G/EPS/5G form (<code class="font-mono text-sm">00 88 00 81 22</code>, response <code class="font-mono text-sm">DB</code> success or <code class="font-mono text-sm">DC</code> synchronisation failure with AUTS), a SIM the 2G form (<code class="font-mono text-sm">A0 88 00 00 10</code>, RAND only, response SRES + Kc).</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3"><strong>Parameters</strong> (collapsed) provide the operator (searchable worldwide MCC/MNC list served from the server, plus a random roaming picker and a <strong>Home network</strong> 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 <strong>Rejection: write FPLMN</strong>) 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.</p>
|
||||
|
||||
|
||||
+1
-1
@@ -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 =====
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v222';
|
||||
const CACHE = 'simple-v223';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -77,6 +77,23 @@ class AdmVerifyTests(unittest.TestCase):
|
||||
self.assertEqual(out, {'adm': '<redacted>', 'psk_hex': '<redacted>', 'other': 'x'})
|
||||
self.assertEqual(server._redact_psk_fields({'adm': ''}), {'adm': ''})
|
||||
|
||||
def test_verify_adm_uses_the_cards_own_channel(self):
|
||||
# server.scc can be the startup placeholder left at the SIM CLA ('a0')
|
||||
# while the card is a UICC: the VERIFY must go out through the card's
|
||||
# own channel (CLA 00), exactly like pySim-shell's verify_adm.
|
||||
stale = FakeScc('6E00')
|
||||
stale.cla_byte = 'a0'
|
||||
card_scc = FakeScc('9000')
|
||||
card_scc.cla_byte = '00'
|
||||
app = SimpleNamespace(
|
||||
card=SimpleNamespace(_adm_chv_num=0x0A, _scc=card_scc),
|
||||
rs=SimpleNamespace(adm_verified=False,
|
||||
lchan=[SimpleNamespace(scc=card_scc)]))
|
||||
res = server._verify_adm(stale, app, '0011')
|
||||
self.assertEqual(res, {'ok': True, 'sw': '9000'})
|
||||
self.assertEqual(card_scc.apdus, ['0020000A08' + '0011' + 'f' * 12])
|
||||
self.assertEqual(stale.apdus, [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -191,6 +191,17 @@ class BuilderTests(unittest.TestCase):
|
||||
self.assertEqual(sync['objects'], ['CC' * 16])
|
||||
self.assertIsNone(netsim.parse_auth_response('9000'))
|
||||
|
||||
def test_authenticate_2g_apdu_and_response(self):
|
||||
# SIM cards use the 2G AUTHENTICATE: P1/P2 00, RAND only
|
||||
self.assertEqual(netsim.build_auth_apdu_gsm('11' * 16),
|
||||
'A088000010' + '11' * 16)
|
||||
parsed = netsim.parse_auth_response('AABBCCDD' + '1122334455667788',
|
||||
gsm=True)
|
||||
self.assertEqual(parsed, {'type': 'gsm', 'sres': 'AABBCCDD',
|
||||
'kc': '1122334455667788'})
|
||||
# a truncated 2G response is not parsed
|
||||
self.assertIsNone(netsim.parse_auth_response('AABBCCDD', gsm=True))
|
||||
|
||||
|
||||
class FakeFileInfo:
|
||||
def __init__(self, size=None, record_len=None, num=1, data=''):
|
||||
@@ -430,6 +441,23 @@ class RunnerTests(unittest.TestCase):
|
||||
self.assertEqual(step['sw'], '9000')
|
||||
self.assertEqual(step['parsed']['type'], 'success')
|
||||
|
||||
def test_authenticate_uses_the_2g_command_for_a_sim_cla(self):
|
||||
runner, lchan, srv = make_runner()
|
||||
scc = srv._server_ref.scc
|
||||
scc.cla_byte = 'a0' # a SIM card session
|
||||
sent = []
|
||||
def _send(apdu):
|
||||
sent.append(apdu)
|
||||
return 'AABBCCDD' + '11' * 8, '9000'
|
||||
scc._tp.send_apdu = _send
|
||||
out = runner.run('authenticate')
|
||||
self.assertTrue(out['success'])
|
||||
self.assertTrue(sent[0].startswith('A088000010'), sent[0])
|
||||
step = [s for s in out['steps'] if s['action'] == 'authenticate'][0]
|
||||
self.assertEqual(step['sw'], '9000')
|
||||
self.assertEqual(step['parsed'],
|
||||
{'type': 'gsm', 'sres': 'AABBCCDD', 'kc': '11' * 8})
|
||||
|
||||
def test_unknown_scenario_raises(self):
|
||||
runner, _lchan, _srv = make_runner()
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user