v3.26.0: per-TAR default keysets (the preset/SCP80/RAM rework)

Every preset TAR entry carries `keyset` (int 1-15) - the keyset every
operation addressed to that TAR uses: mandatory for any non-zero MSL
(the counters are keyset-linked), optional for MSL 00 (allowed: some
cards reject the no-security 00/00 KIc/KID form).  The store fills it
automatically when the preset has a single keyset, the validation
refuses an entry naming an undefined keyset, and the schema is 6 (a v5
file loads leniently; editing it forces the fix).

The keyset selectors are gone: the SCP80 TAR field became a select of
the preset's entries (applying the TAR's keyset to the form and seeding
the SPI1 from its MSL); the RAM operations use the preset's ISD role
entry (the PWA sends only the preset id, the ISD TAR, SPI and the
counter override - the server resolves the keyset and the keys:
presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset,
_op_keyset, used by send-ota's APDU path, ram-install, ram-install-app
and cap-compat); the counter probe and the test runner's scp80 steps
default to their TAR's keyset (an explicit kvn stays an override).  A
security TAR without a keyset refuses the operation ("set it in the
Cards tab") - never a silent keyless send - and the start buttons wait
for a usable operation-TAR keyset.

The TAR probe checks the preset's TAR list only (the standard
TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars`
list selects a subset, a TAR outside the preset is refused), each row
using its own MSL and default keyset.  The Cards editor's TAR table
gained a Keyset column (the only keyset preselected, an amber marker
when a security MSL lacks one).

Docs: help EN/RU, api.md's tar-probe contract, both AGENTS.  Tests: the
store (the field, the auto-fill, the schema, the string forms), the
server resolution (send-ota + the refusal, the probe's per-row keysets
and refusals), the PWA (the TAR select, the RAM ISD resolution, the
keyset-free requests) - several fixtures updated for the new schema.
891 frontend + 885 Python; sw cache simple-v434.
This commit is contained in:
2026-10-14 12:21:16 +03:00
parent 6e4494195c
commit cb40662e4e
18 changed files with 955 additions and 611 deletions
+57 -5
View File
@@ -69,7 +69,7 @@ from pathlib import Path
DEFAULT_DIR = '.pysim-simple-server'
DEFAULT_FILENAME = 'card_presets.json'
SCHEMA_VERSION = 5
SCHEMA_VERSION = 6
# The fields a preset carries: the localStorage shape plus the id. The OTA key
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
@@ -88,7 +88,12 @@ DIGIT_FIELDS = ('pin1', 'puk1', 'pin2', 'puk2')
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
# The TAR table: the three mandatory role entries (their values are pre-settled
# in the Cards form) and free entries with an optional description. Every
# entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
# entry carries the TAR, its MSL (Minimum SPI1, one hex byte) and its **default
# keyset** (v3.26.0): the keyset every operation addressed to that TAR uses.
# The keyset is mandatory for any non-zero MSL (the counters are keyset-linked,
# TS 102 225 A.1) and may be omitted only for MSL 00 - where setting one is
# still allowed, to pass its KIc/KID in the packet (some cards reject the
# no-security '00/00' form even when SPI declares no security).
TAR_ROLES = ('isd', 'uiccRfm', 'usimRfm')
TAR_ROLE_DEFAULTS = {
'isd': {'tar': '000000', 'msl': '16'},
@@ -194,6 +199,13 @@ def tar_msl(preset, tar_hex):
return str((tar_entry(preset, tar_hex) or {}).get('msl') or '')
def tar_keyset(preset, tar_hex):
"""The default keyset number (int 1-15) of a TAR, or None when the preset
has no such entry or the entry carries no keyset (allowed for an MSL 00
entry - the packet is then built keyless; v3.26.0)."""
return (tar_entry(preset, tar_hex) or {}).get('keyset') or None
class PresetError(Exception):
"""A refused mutation (validation, duplicate ICCID, ...)."""
@@ -306,6 +318,14 @@ class PresetStore:
raw = [{k: fields.get(k) for k in LEGACY_KEY_FIELDS}]
out['keysets'] = [self._normalise_keyset(ks) for ks in raw
if isinstance(ks, dict)]
# v3.26.0: a preset with a single keyset fills every TAR entry that has
# no default keyset with it - the TARs' keysets drive the operations.
nums = [keyset_kvn(ks) for ks in out['keysets']]
nums = [n for n in nums if n]
if len(nums) == 1:
for t in out['tars']:
if not t.get('keyset'):
t['keyset'] = nums[0]
pid = str(fields.get('id') or '').strip().lower()
out['id'] = pid or uuid.uuid4().hex
return out
@@ -328,6 +348,7 @@ class PresetStore:
'tar': re.sub(r'\s', '', str(t.get('tar') or '')).upper(),
'msl': re.sub(r'\s', '', str(t.get('msl') or '')).upper(),
'desc': str(t.get('desc') or '').strip(),
'keyset': self._normalise_keyset_ref(t.get('keyset')),
})
else:
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
@@ -338,6 +359,7 @@ class PresetStore:
'tar': re.sub(r'\s', '', str(fields.get(src) or '')).upper(),
'msl': msl,
'desc': '',
'keyset': None,
})
out = []
for role in TAR_ROLES:
@@ -346,16 +368,37 @@ class PresetStore:
out.append({'role': role,
'tar': (cur or {}).get('tar') or d['tar'],
'msl': (cur or {}).get('msl') or d['msl'],
'desc': ''})
'desc': '',
'keyset': (cur or {}).get('keyset')})
for e in entries:
if e['role']:
continue
if not (e['tar'] or e['msl'] or e['desc']):
if not (e['tar'] or e['msl'] or e['desc'] or e.get('keyset')):
continue # a blank editor row
# free entries carry no `role`: the three roles are the fixed ones
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc']})
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc'],
'keyset': e.get('keyset')})
return out
@staticmethod
def _normalise_keyset_ref(v):
"""A TAR's default keyset number (int 1-15) or None. Accepts an int
and the decimal ('1'..'15') or hex-ish ('01'..'0F') string forms; an
unusable value becomes None - the validation reports a missing keyset
where one is mandatory (v3.26.0)."""
if v is None or v == '':
return None
s = str(v).strip().upper()
if re.fullmatch(r'[0-9]{1,2}', s):
n = int(s, 10)
if 1 <= n <= KVN_MAX:
return n
if re.fullmatch(r'[0-9A-F]{1,2}', s):
n = int(s, 16)
if 1 <= n <= KVN_MAX:
return n
return None
def _normalise_keyset(self, ks):
"""One keyset, cleaned. The counter is kept as typed here and
validated + padded in _validate_keyset() - a bad value must be
@@ -402,6 +445,15 @@ class PresetStore:
'byte, TS 102 226 8.2.1.3.2.4)' % t['tar'])
if not re.fullmatch(r'[0-9A-F]{2}', t['msl']):
raise PresetError('MSL must be one hex byte: %r' % t['msl'])
if t.get('keyset'):
if not any(keyset_kvn(ks) == t['keyset'] for ks in p['keysets']):
raise PresetError('TAR %s names keyset %02X, which this '
'preset does not define'
% (t['tar'], t['keyset']))
elif t['msl'] != '00':
raise PresetError('TAR %s needs a default keyset (its MSL %s '
'requires security) - set it in the Cards tab'
% (t['tar'], t['msl']))
if t['tar'] in seen_tars:
raise PresetError('duplicate TAR %s in this preset' % t['tar'])
seen_tars.add(t['tar'])
+236 -79
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.25.8'
VERSION = '3.26.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1700,6 +1700,54 @@ def _preset_keyset_check(server, preset_id, kic, kid):
return None
def _preset_tar_keyset(server, preset_id, tar):
"""The default keyset number of a TAR in the named preset (v3.26.0), or
None (no preset, no such TAR entry, or an MSL 00 entry without one)."""
preset = _preset_by_id(server, preset_id)
if preset is None:
return None
return presets.tar_keyset(preset, tar)
def _preset_keyset_by_kvn(preset, kvn):
"""The preset's keyset dict with number `kvn`, or None."""
return next((ks for ks in _preset_keysets(preset)
if presets.keyset_kvn(ks) == kvn), None)
def _resolve_operation_keyset(server, body, tar):
"""Fill an operation's keyset fields from the preset's default keyset for
`tar` (v3.26.0).
An explicit KIc/KID in the request is an API override and stays as it is;
otherwise the TAR's default keyset supplies kic/kid/kicKey/kidKey. A TAR
whose MSL requires security must have one - the operation refuses instead
of sending a keyless packet the card would reject (or, worse, process as
no-security). Returns ``(kvn, error)``."""
explicit_kic = str((body or {}).get('kic') or '').strip().upper()
explicit_kid = str((body or {}).get('kid') or '').strip().upper()
if explicit_kic or explicit_kid:
return (_kvn_of(explicit_kic, explicit_kid)[0] or None), None
preset = _preset_by_id(server, (body or {}).get('preset_id'))
if preset is None:
return None, None
kvn = presets.tar_keyset(preset, tar)
if not kvn:
msl = presets.tar_msl(preset, tar)
if msl and msl != '00':
return None, ('TAR %s needs a default keyset (its MSL %s requires '
'security) - set it in the Cards tab' % (tar, msl))
return None, None
keyset = _preset_keyset_by_kvn(preset, kvn)
if keyset is None:
return None, ('keyset %02X is not defined in preset %s - add it in the '
'Cards tab' % (kvn, preset.get('name') or
(body or {}).get('preset_id')))
for field in ('kic', 'kid', 'kicKey', 'kidKey'):
body[field] = str(keyset.get(field) or '').strip()
return kvn, None
def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
"""Persist the counter an operation accepted into the server-side preset
store (v3.8.0), into the keyset it used (each keyset has its own dedicated
@@ -1916,6 +1964,7 @@ def _counter_probe_params(preset, body):
keysets = _preset_keysets(preset)
if not keysets:
raise ValueError('the preset defines no keyset')
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
want = body.get('kvn')
if want not in (None, ''):
try:
@@ -1926,7 +1975,13 @@ def _counter_probe_params(preset, body):
if keyset is None:
raise ValueError('keyset %d is not defined in the preset' % want)
else:
keyset = keysets[0]
# v3.26.0: the TAR's default keyset, else the first - the probe is a
# repair tool and keeps working for a legacy preset without TAR keysets.
tar_kvn = presets.tar_keyset(preset, tar)
keyset = (next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
if tar_kvn else None)
if keyset is None:
keyset = keysets[0]
kic = str(keyset.get('kic') or '').strip().upper()
kid = str(keyset.get('kid') or '').strip().upper()
kic_key = str(keyset.get('kicKey') or '').strip()
@@ -1953,7 +2008,6 @@ def _counter_probe_params(preset, body):
except (TypeError, ValueError):
raise ValueError('invalid attempt budget')
max_attempts = max(1, min(64, max_attempts))
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'), prefer_msl=True)
if not _counter_tracked(spi1):
raise ValueError('the probe needs a counter check - SPI1 %s has none '
@@ -6133,26 +6187,6 @@ _CARD_FREE_GET = frozenset([
'/api/scp81/log', '/api/bip/log',
])
# TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1): the
# TAR probe sends one harmless command to each and reports the card's answer.
# 'B00200' is not an application - it is the start of the RFU range, kept as a
# control (a card must not answer it).
TAR_PROBE_TARS = [
('000000', 'Issuer Security Domain (compact)'),
('B20100', 'Issuer Security Domain (expanded)'),
('B00000', 'UICC shared file system RFM (compact)'),
('B00001', 'ADF RFM (compact)'),
('B00010', 'SIM file system RFM (compact)'),
('B00120', 'UICC shared file system RFM (expanded)'),
('B00130', 'SIM file system RFM (expanded)'),
('B00140', 'ADF RFM (expanded)'),
('B20000', 'USAT interpreter'),
('B20200', 'Multiplexing application'),
('B20201', 'Controlling Authority Security Domain'),
('B20202', 'OMA BCAST audience measurement'),
('B20203', 'OMA DM LWM2M application'),
('B00200', 'RFU (control)'),
]
TAR_PROBE_DEFAULT_APDU = '00A40000023F00' # SELECT MF: harmless, applet-visible
# The TARs the tool treats as remote-management targets: the preset's role
@@ -6447,6 +6481,24 @@ def _test_preset_error(script, preset):
if not _keyset_complete(ks):
return ('step %d: keyset %d is incomplete (KIc, KID, both '
'keys and a counter)' % (i + 1, want))
else:
# v3.26.0: the step's TAR picks its default keyset - it must exist
# and be complete; a TAR whose MSL requires security must have one.
tar = p.get('tar') or presets.role_tar(preset, 'isd')
tar_kvn = presets.tar_keyset(preset, tar)
if tar_kvn:
ks = next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
if ks is None:
return ('step %d: keyset %d (the default of TAR %s) is not '
'defined in the preset' % (i + 1, tar_kvn, tar))
if not _keyset_complete(ks):
return ('step %d: keyset %d (the default of TAR %s) is '
'incomplete (KIc, KID, both keys and a counter)'
% (i + 1, tar_kvn, tar))
elif (presets.tar_msl(preset, tar) or '') not in ('', '00'):
return ('step %d: TAR %s needs a default keyset (its MSL %s '
'requires security) - set it in the Cards tab'
% (i + 1, tar, presets.tar_msl(preset, tar)))
tar = p.get('tar') or presets.role_tar(preset, 'isd')
if not tar:
return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1)
@@ -6781,8 +6833,10 @@ def _test_run_scp80(server, step, ctx):
preset = ctx['preset']
p = step['params']
# The keyset: a step may name a keyset number (`kvn`), otherwise the
# preset's first keyset is used (each keyset has its own keys and counter -
# TS 102 225 Annex A.1).
# default keyset of the step's TAR is used (v3.26.0; each keyset has its
# own keys and counter - TS 102 225 Annex A.1). A TAR whose MSL requires
# security must have one - the counters are keyset-linked; an MSL 00 step
# falls back to the preset's first keyset.
keysets = _preset_keysets(preset)
keyset = None
if p.get('kvn') is not None:
@@ -6792,7 +6846,22 @@ def _test_run_scp80(server, step, ctx):
raise testscript.ScriptError('keyset %d is not defined in the '
'selected card preset' % want)
elif keysets:
keyset = keysets[0]
tar = str(p.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
tar_kvn = presets.tar_keyset(preset, tar)
if tar_kvn:
keyset = next((ks for ks in keysets
if presets.keyset_kvn(ks) == tar_kvn), None)
if keyset is None:
raise testscript.ScriptError(
'keyset %d (the default of TAR %s) is not defined in the '
'selected card preset' % (tar_kvn, tar))
else:
msl = presets.tar_msl(preset, tar)
if msl and msl != '00':
raise testscript.ScriptError(
'TAR %s needs a default keyset (its MSL %s requires '
'security) - set it in the Cards tab' % (tar, msl))
keyset = keysets[0]
kic = p.get('kic') or (keyset or {}).get('kic') or ''
kid = p.get('kid') or (keyset or {}).get('kid') or ''
kic_key = p.get('kicKey') or (keyset or {}).get('kicKey') or ''
@@ -8043,6 +8112,23 @@ class PysimHandler(BaseHTTPRequestHandler):
err = _preset_keyset_check(self.server, body.get('preset_id'), kic, kid)
return (kvn or None), err
def _op_keyset(self, body, tar=None):
"""v3.26.0: resolve an operation's keyset from the preset's TAR table
(the RAM operations default to the ISD role's TAR), then run the
KIc/KID checks. An explicit request KIc/KID stays an API override.
Returns ``(kvn, kic, kid, error)`` with the effective values."""
t = str(tar if tar is not None else (body.get('tar') or '')).strip().upper()
if not t and body.get('preset_id'):
preset = _preset_by_id(self.server, body.get('preset_id'))
t = presets.role_tar(preset, 'isd') if preset else ''
_kvn, err = _resolve_operation_keyset(self.server, body, t)
if err:
return None, '', '', err
kic = str(body.get('kic') or '').strip().upper()
kid = str(body.get('kid') or '').strip().upper()
kvn, err = self._keyset_guard(body, kic, kid)
return kvn, kic, kid, err
def _log_req(self, body=None):
if self.server.log_requests:
if body is not None:
@@ -9216,11 +9302,15 @@ class PysimHandler(BaseHTTPRequestHandler):
# deliberately ask for more security than the TAR's minimum), with
# a warning below the TAR's MSL; without one it is derived from the
# MSL of the packet's TAR (TS 102 226 8.2.1.3.2.4 - the card checks
# the MSL before the security processing).
# the MSL before the security processing). An APDU send without a
# TAR uses the preset's ISD role entry (v3.26.0).
try:
spi1, spi_warn = _spi1_for_tar(
_preset_by_id(self.server, body.get('preset_id')),
str(body.get('tar') or '').strip().upper(), body.get('spi1'))
preset_ref = _preset_by_id(self.server, body.get('preset_id'))
tar_ref = str(body.get('tar') or '').strip().upper()
if not tar_ref and apdu and preset_ref:
tar_ref = presets.role_tar(preset_ref, 'isd')
spi1, spi_warn = _spi1_for_tar(preset_ref, tar_ref,
body.get('spi1'))
except ValueError as e:
resp = {'error': str(e)}
self._send_json(resp, 400)
@@ -9245,9 +9335,15 @@ class PysimHandler(BaseHTTPRequestHandler):
# RAM operation: SCP80-wrap the raw GP command. The packet
# may exceed one SMS (pySim refuses that), so use our own
# encoder and let _send_secured_packet segment it.
kic = body.get('kic', '25')
kid = body.get('kid', '25')
tar = body.get('tar', '000000')
# The keyset: the preset's default for the packet's TAR
# (v3.26.0) unless the request names KIc/KID itself.
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
if keyset_err:
resp = {'error': keyset_err}
self._send_json(resp, 400)
self._log_resp(resp)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: an explicit `cntr` wins; otherwise the
@@ -9494,26 +9590,26 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
sd_aid = body.get('sd_aid', '').replace(' ', '')
install_params_hex = body.get('install_params', '').replace(' ', '')
stk_params_hex = body.get('stk_params', '').replace(' ', '')
make_selectable = body.get('make_selectable', True)
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
# The keyset number must be consistent and defined in the preset
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
# before the format probe consumes a counter.
kvn, keyset_err = self._keyset_guard(body, kic, kid)
# The keyset: the preset's default for the operation's TAR (the
# ISD role entry; v3.26.0) unless the request names KIc/KID
# itself - then the number must be consistent and defined in
# the preset (TS 102 225 A.2 / the "keyset must exist" rule),
# checked before the format probe consumes a counter.
kvn, kic, kid, keyset_err = self._op_keyset(body)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
@@ -9707,19 +9803,23 @@ class PysimHandler(BaseHTTPRequestHandler):
return
sp_state = {
'spi1': spi1, 'spi2': body.get('spi2', '01'),
'kic': body.get('kic', '25'), 'kid': body.get('kid', '25'),
'tar': tar, 'cntr': body.get('cntr', '00000000'),
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'include_cpi': body.get('includeCpi', True)}
# The keyset number must be consistent and defined in the preset
# The keyset: the preset's default for the operation's TAR
# (v3.26.0) unless the request names KIc/KID itself - then the
# number must be consistent and defined in the preset
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
# before the format probe consumes a counter.
kvn, keyset_err = self._keyset_guard(body, sp_state['kic'], sp_state['kid'])
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
sp_state['kic'] = kic
sp_state['kid'] = kid
sp_state['kic_key'] = body.get('kicKey', '')
sp_state['kid_key'] = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
@@ -9832,18 +9932,19 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
steps = []
kvn, keyset_err = self._keyset_guard(body, kic, kid)
# The keyset: the preset's default for the operation's TAR
# (the ISD role entry; v3.26.0) unless the request names
# KIc/KID itself.
kvn, kic, kid, keyset_err = self._op_keyset(body)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
cntr = _preset_counter_seed(self.server, body, kvn) or cntr
@@ -10487,40 +10588,59 @@ class PysimHandler(BaseHTTPRequestHandler):
return
body = self._read_body()
self._log_req(body)
kic = body.get('kic', '')
kid = body.get('kid', '')
kvn, keyset_err = self._keyset_guard(body, kic, kid)
if keyset_err:
err = {'success': False, 'error': keyset_err}
# v3.26.0: the probe checks the preset's TAR list only - no
# arbitrary TARs - and each row uses its TAR's default keyset.
if not body.get('preset_id'):
err = {'success': False,
'error': 'preset_id is required - the probe checks the '
'preset TAR list'}
self._send_json(err, 400)
self._log_resp(err)
return
preset = _preset_by_id(self.server, body.get('preset_id'))
if not preset:
err = {'success': False, 'error': 'unknown preset'}
self._send_json(err, 400)
self._log_resp(err)
return
# The probe sends real secured packets: it needs a counter check
# (every accepted packet advances and is persisted) and a PoR. Each
# TAR uses its own MSL when the preset carries it; the caller's SPI1
# is the fallback for the arbitrary TARs the checklist probes.
# (every accepted packet advances and is persisted) and a PoR.
# Each TAR uses its own MSL and its own default keyset.
spi1_base = str(body.get('spi1') or '').strip().upper()
spi2 = str(body.get('spi2') or '01').strip().upper()
preset = _preset_by_id(self.server, body.get('preset_id'))
apdu = (body.get('apdu') or TAR_PROBE_DEFAULT_APDU).replace(' ', '').upper()
labels = {tar: label for tar, label in TAR_PROBE_TARS}
role_label = {'isd': 'ISD', 'uiccRfm': 'UICC RFM', 'usimRfm': 'ADF RFM'}
by_tar = {}
for t in (preset.get('tars') or []):
tar = str(t.get('tar') or '').strip().upper()
if tar:
by_tar[tar] = t
wanted = body.get('tars')
if isinstance(wanted, list) and wanted:
entries = []
for t in wanted:
tar = str(t).replace(' ', '').upper()
entries.append((tar, labels.get(tar, '')))
e = by_tar.get(tar)
if e is None:
err = {'success': False,
'error': 'TAR %s is not in the preset - the probe '
'checks the preset TAR list only' % tar}
self._send_json(err, 400)
self._log_resp(err)
return
entries.append((tar, e))
else:
entries = list(TAR_PROBE_TARS)
# Resolve every TAR's SPI1 before the first packet is sent: a
# missing MSL must refuse the whole probe, not leave it half-run.
entries = [(tar, e) for tar, e in by_tar.items()]
# Resolve every TAR's SPI1 and keyset before the first packet is
# sent: a missing MSL or keyset must refuse the whole probe, not
# leave it half-run.
resolved = []
for tar, label in entries:
for tar, e in entries:
try:
tar_spi1, _ = _spi1_for_tar(preset, tar, spi1_base,
prefer_msl=True)
except ValueError as e:
err = {'success': False, 'error': str(e)}
except ValueError as exc:
err = {'success': False, 'error': str(exc)}
self._send_json(err, 400)
self._log_resp(err)
return
@@ -10532,31 +10652,67 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(err, 400)
self._log_resp(err)
return
resolved.append((tar, label, tar_spi1))
row_kvn = e.get('keyset')
row_ks = None
if row_kvn:
row_ks = _preset_keyset_by_kvn(preset, row_kvn)
if row_ks is None:
err = {'success': False,
'error': 'keyset %02X (TAR %s) is not defined in '
'the preset' % (row_kvn, tar)}
self._send_json(err, 400)
self._log_resp(err)
return
else:
msl = str(e.get('msl') or '')
if msl and msl != '00':
err = {'success': False,
'error': 'TAR %s needs a default keyset (its MSL '
'%s requires security) - set it in the '
'Cards tab' % (tar, msl)}
self._send_json(err, 400)
self._log_resp(err)
return
label = (str(e.get('desc') or '').strip()
or role_label.get(e.get('role') or '', ''))
resolved.append((tar, label, tar_spi1, row_kvn, row_ks))
explicit_cntr = str(body.get('cntr') or '').strip().upper()
state = {'steps': [], 'encode_error': None, 'failure': {},
# the counter: explicit `cntr` wins, else the preset's
# value for this keyset (the server owns the counters,
# v3.20.0)
'cntr': _preset_counter_seed(self.server, body, kvn) or '00000000',
'preset_id': body.get('preset_id'), 'kvn': kvn}
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': kic, 'kid': kid, 'tar': '000000',
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'cntr': '00000000', 'preset_id': body.get('preset_id'),
'kvn': None}
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': '00', 'kid': '00',
'tar': '000000', 'kic_key': '', 'kid_key': '',
'include_cpi': body.get('includeCpi', True)}
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s, kvn=%s\n'
% (len(entries), apdu, spi1_base or '-', spi2, kvn))
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s\n'
% (len(entries), apdu, spi1_base or '-', spi2))
_ram_progress_begin('tar-probe', len(entries))
try:
for index, (tar, _label, tar_spi1) in enumerate(resolved):
for index, (tar, _label, tar_spi1, row_kvn, row_ks) in enumerate(resolved):
_ram_progress_step(index, 'TAR ' + tar)
sp['tar'] = tar
sp['spi1'] = tar_spi1
sp['kic'] = str((row_ks or {}).get('kic') or '00')
sp['kid'] = str((row_ks or {}).get('kid') or '00')
sp['kic_key'] = str((row_ks or {}).get('kicKey') or '')
sp['kid_key'] = str((row_ks or {}).get('kidKey') or '')
state['kvn'] = row_kvn
# Each keyset has its own counter (TS 102 225 A.1): seed
# the row's from the store (an explicit `cntr` override
# applies to the first row only).
if index == 0 and explicit_cntr:
state['cntr'] = explicit_cntr
else:
state['cntr'] = (_preset_counter_seed(
self.server, {'preset_id': body.get('preset_id')},
row_kvn) or '00000000')
_ram_send_gp_apdu(self.server, scc, sp, state, 'TAR ' + tar, apdu)
finally:
_ram_progress_end()
results = []
for (tar, label, tar_spi1), step in zip(resolved, state['steps']):
for (tar, label, tar_spi1, row_kvn, _ks), step in zip(resolved, state['steps']):
results.append({
'tar': tar, 'label': label, 'spi1': tar_spi1,
'kvn': row_kvn,
'verdict': _tar_probe_verdict(step),
'sw': step.get('sw'), 'por_status': step.get('por_status'),
'por_sw': step.get('por_sw'), 'por_data': step.get('por_data'),
@@ -10565,7 +10721,8 @@ class PysimHandler(BaseHTTPRequestHandler):
registered = sum(1 for r in results if r['verdict'] == 'registered')
resp = {'success': True, 'results': results, 'steps': state['steps'],
'registered': registered, 'total': len(results),
'final_cntr': state['cntr'], 'kvn': kvn,
'final_cntr': state['cntr'],
'kvn': resolved[0][3] if resolved else None,
'spi1': spi1_base, 'spi2': spi2, 'apdu': apdu}
self._send_json(resp)
self._log_resp(resp)