v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR

Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4).  v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.

Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint.  The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row.  The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).

PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs).  Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.

Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
This commit is contained in:
2026-10-02 08:28:46 +03:00
parent f059d05040
commit 6360dc5bae
19 changed files with 1187 additions and 269 deletions
+115 -13
View File
@@ -9,6 +9,17 @@ owns the store: an accepted counter is persisted in the same operation, and a
closed tab, a lost response or a second browser window can no longer lose an
increment.
A preset holds a **TAR table**: every Remote Management application the card
exposes is addressed by its TAR, and each carries its own **Minimum Security
Level** (MSL) - the "Minimum SPI1" the card checks before it processes a
secured packet (TS 102 226 6.1/8.2.1.3.2.4; a too-low SPI1 is answered with
response status 0A "Insufficient security level"). The three role entries
(`isd`, `uiccRfm`, `usimRfm`) are mandatory - the Cards form pre-settles their
values - and further TARs can be added with an optional description. A packet
for a TAR derives its SPI1 from that TAR's MSL; the preset no longer carries a
card-wide SPI1/SPI2 (SPI2 is an operation property: whether and how a Proof of
Receipt is requested).
A preset holds **several keysets**; the b8..b5 nibble of KIc/KID numbers them
(TS 102 225 5.1.2/A.2). Each keyset has its own KIc/KID keys and its own
counter - "a dedicated counter shall be associated to each key version"
@@ -30,7 +41,9 @@ deliberately human-readable: hand-editing a counter or moving the file between
machines is part of the workbench workflow, and the Cards tab's export/import
uses the same shape (which is also how old localStorage presets move over -
there is no automatic migration). v3.8.0's flat ``kic/kid/kicKey/kidKey/cntr``
shape is converted into a single keyset on load and on import.
shape is converted into a single keyset, and a v3.9.1 preset's card-wide
``spi1``/``tar``/``uiccTar``/``usimTar`` become the three role entries (the
spi1 as their MSL) - on load and on import alike.
"""
import copy
@@ -46,21 +59,28 @@ from pathlib import Path
DEFAULT_DIR = '.pysim-simple-server'
DEFAULT_FILENAME = 'card_presets.json'
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
# The fields a preset carries: the localStorage shape plus the id. Defaults
# mirror the Cards form (spi1/spi2 and the per-target TARs). The OTA key
# 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,
# each with its own counter (TS 102 225 Annex A.1: "a dedicated counter shall
# be associated to each key version").
PRESET_FIELDS = ('name', 'iccid', 'adm', 'spi1', 'spi2',
'tar', 'uiccTar', 'usimTar', 'pskIdentity', 'pskKey')
# be associated to each key version"); the TAR table lives in `tars` (see
# TAR_ROLES).
PRESET_FIELDS = ('name', 'iccid', 'adm', 'pskIdentity', 'pskKey')
# The v3.8.0 flat key fields: converted into a single keyset on load/import.
LEGACY_KEY_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
KEYSET_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
HEX_FIELDS = ('adm', 'spi1', 'spi2', 'tar', 'uiccTar', 'usimTar', 'pskKey')
HEX_FIELDS = ('adm', 'pskKey')
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
TAR_DEFAULTS = {'tar': '000000', 'uiccTar': 'B00000', 'usimTar': 'B00001'}
# 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).
TAR_ROLES = ('isd', 'uiccRfm', 'usimRfm')
TAR_ROLE_DEFAULTS = {
'isd': {'tar': '000000', 'msl': '16'},
'uiccRfm': {'tar': 'B00000', 'msl': '16'},
'usimRfm': {'tar': 'B00001', 'msl': '16'},
}
COUNTER_BITS = 40 # the SCP80 counter is 5 bytes (TS 31.115)
COUNTER_WIDTH = 10 # ... shown as 10 hex digits
KVN_MAX = 0x0F # keyset number '00' is reserved: no key set, no counter
@@ -134,6 +154,32 @@ def counter_ahead(old, new):
return (o - n) > (1 << (COUNTER_BITS - 1))
def tar_entry(preset, tar_hex):
"""The preset's TAR entry whose TAR matches `tar_hex` (case-insensitive),
or None."""
want = re.sub(r'\s', '', str(tar_hex or '')).upper()
if not want:
return None
for t in (preset or {}).get('tars') or []:
if str(t.get('tar') or '').upper() == want:
return dict(t)
return None
def role_tar(preset, role):
"""The TAR of a role entry ('isd' | 'uiccRfm' | 'usimRfm'), or ''."""
for t in (preset or {}).get('tars') or []:
if t.get('role') == role:
return str(t.get('tar') or '')
return ''
def tar_msl(preset, tar_hex):
"""The MSL (Minimum SPI1) of a TAR, or '' when the preset has no such
entry or no MSL for it."""
return str((tar_entry(preset, tar_hex) or {}).get('msl') or '')
class PresetError(Exception):
"""A refused mutation (validation, duplicate ICCID, ...)."""
@@ -234,10 +280,7 @@ class PresetStore:
if k in HEX_FIELDS:
v = re.sub(r'\s', '', v).upper()
out[k] = v
out['spi1'] = out['spi1'] or '16'
out['spi2'] = out['spi2'] or '01'
for k, d in TAR_DEFAULTS.items():
out[k] = out[k] or d
out['tars'] = self._normalise_tars(fields)
# Keysets: a v3.8.0 preset (flat kic/kid/kicKey/kidKey/cntr) converts
# into a single keyset, so old files and exports keep working.
raw = fields.get('keysets')
@@ -251,6 +294,51 @@ class PresetStore:
out['id'] = pid or uuid.uuid4().hex
return out
def _normalise_tars(self, fields):
"""The preset's TAR table: the three mandatory role entries (values
filled from the defaults when empty, so a partial API call and the
pre-settled form both work) followed by the free entries in input
order. A v3.9.1 preset (tar/uiccTar/usimTar + the card-wide spi1)
converts: the spi1 becomes the MSL of all three roles."""
raw = fields.get('tars')
entries = []
if isinstance(raw, list):
for t in raw:
if not isinstance(t, dict):
continue
role = str(t.get('role') or '').strip()
entries.append({
'role': role if role in TAR_ROLES else '',
'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(),
})
else:
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
for role, src in (('isd', 'tar'), ('uiccRfm', 'uiccTar'),
('usimRfm', 'usimTar')):
entries.append({
'role': role,
'tar': re.sub(r'\s', '', str(fields.get(src) or '')).upper(),
'msl': msl,
'desc': '',
})
out = []
for role in TAR_ROLES:
cur = next((e for e in entries if e['role'] == role), None)
d = TAR_ROLE_DEFAULTS[role]
out.append({'role': role,
'tar': (cur or {}).get('tar') or d['tar'],
'msl': (cur or {}).get('msl') or d['msl'],
'desc': ''})
for e in entries:
if e['role']:
continue
if not (e['tar'] or e['msl'] or e['desc']):
continue # a blank editor row
out.append(e)
return out
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
@@ -281,6 +369,18 @@ class PresetStore:
if kvn in seen:
raise PresetError('duplicate keyset number %02X in this preset' % kvn)
seen.add(kvn)
seen_tars = set()
for t in p['tars']:
if not re.fullmatch(r'[0-9A-F]{6}', t['tar']):
raise PresetError('TAR must be three hex bytes: %r' % t['tar'])
if not t['msl']:
raise PresetError('TAR %s needs its MSL (Minimum SPI1, one hex '
'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['tar'] in seen_tars:
raise PresetError('duplicate TAR %s in this preset' % t['tar'])
seen_tars.add(t['tar'])
norm = normalize_iccid(p['iccid'])
if norm:
for other in self._presets:
@@ -372,6 +472,8 @@ class PresetStore:
merged[k] = fields[k]
if isinstance(fields, dict) and 'keysets' in fields:
merged['keysets'] = fields['keysets']
if isinstance(fields, dict) and isinstance(fields.get('tars'), list):
merged['tars'] = fields['tars']
new = self._normalise(merged)
new['id'] = cur['id']
self._validate(new, skip_id=cur['id'])
+153 -40
View File
@@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.9.1'
VERSION = '3.10.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1503,6 +1503,57 @@ def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
sys.stderr.write('PRESETS: counter persist failed (%s): %s\n' % (source, e))
def _preset_by_id(server, preset_id):
"""The named card preset, or None (no store configured / unknown id)."""
store = getattr(server, 'card_presets', None)
if store is None or not preset_id:
return None
return store.get(preset_id)
def _hex_lt(a, b):
"""Numeric ``a < b`` for two hex byte strings; invalid input is never a
comparison (the packet builder's own validation reports it)."""
try:
return int(a, 16) < int(b, 16)
except (TypeError, ValueError):
return False
def _spi1_for_tar(preset, tar_hex, explicit='', prefer_msl=False):
"""Resolve the SPI1 of a packet addressed to `tar_hex` from the preset's
TAR table.
Each TAR carries its **MSL** (Minimum SPI1, TS 102 226 8.2.1.3.2.4): the
card checks it before the security processing and answers response status
0A "Insufficient security level" when the packet's SPI1 is below it, so the
MSL is the natural value for packets to that TAR.
``prefer_msl=False`` (a hand send): the caller's explicit SPI1 wins - the
operator may deliberately send more security than the minimum - and a value
below the TAR's MSL only produces a warning. ``prefer_msl=True`` (an
operation): the TAR's MSL wins; the explicit value is the fallback for a
TAR the preset does not carry (e.g. the TAR probe's checklist). Raises
ValueError when neither exists - guessing would either be refused by the
card or send more security than the application expects.
Returns ``(spi1, warning)``."""
explicit = str(explicit or '').strip().upper()
msl = presets.tar_msl(preset, tar_hex)
if prefer_msl and msl:
return msl, ''
if explicit:
warning = ''
if msl and _hex_lt(explicit, msl):
warning = ('SPI1 %s is below the MSL %s of TAR %s - the card may '
'answer 0A (insufficient security level)'
% (explicit, msl, tar_hex))
return explicit, warning
if msl:
return msl, ''
raise ValueError('no MSL for TAR %s in the card preset - set it in the '
'Cards tab or pass spi1' % (tar_hex or '?'))
# RAM command formats (TS 102 226 5.2.1): the bare C-APDU (compact) or the
# Command TLV '22' inside the 'AA' scripting template (expanded) - the form
# the reference terminal traces use. The format is detected per operation
@@ -1656,7 +1707,8 @@ def _counter_probe_params(preset, body):
except (TypeError, ValueError):
raise ValueError('invalid attempt budget')
max_attempts = max(1, min(64, max_attempts))
spi1 = str(body.get('spi1') or preset.get('spi1') or '16').strip().upper()
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 '
'(b5b4 = 00)' % spi1)
@@ -1666,8 +1718,8 @@ def _counter_probe_params(preset, body):
'start': start, 'ceiling': ceiling,
'max_attempts': max_attempts,
'spi1': spi1,
'spi2': str(body.get('spi2') or preset.get('spi2') or '21').strip().upper(),
'tar': str(body.get('tar') or preset.get('tar') or '000000').strip().upper(),
'spi2': str(body.get('spi2') or '01').strip().upper(),
'tar': tar,
# the probe command is fixed: read-only SELECT MF, which every applet
# answers (an error SW is fine - the counter advances on the packet's
# security acceptance, not on the command's own result)
@@ -5594,10 +5646,12 @@ 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))
if not (p.get('tar') or preset.get('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)
if not (p.get('spi1') or preset.get('spi1')) or not (p.get('spi2') or preset.get('spi2')):
return 'step %d: no SPI1/SPI2 (neither in the step nor in the preset)' % (i + 1)
if not (p.get('spi1') or presets.tar_msl(preset, tar)):
return ('step %d: no SPI1 for TAR %s (neither in the step nor as '
'its MSL in the preset)' % (i + 1, tar))
return None
@@ -5742,9 +5796,14 @@ def _test_run_scp80(server, step, ctx):
kvn, kvn_err = _kvn_of(kic, kid)
if kvn_err:
raise testscript.ScriptError(kvn_err)
spi1 = p.get('spi1') or preset.get('spi1') or '16'
spi2 = p.get('spi2') or preset.get('spi2') or '01'
tar = p.get('tar') or preset.get('tar') or ''
tar = p.get('tar') or presets.role_tar(preset, 'isd') or ''
try:
# a step may override the SPI1 deliberately (the test runner sends what
# the operator asked for); the TAR's MSL is the default when absent
spi1, _ = _spi1_for_tar(preset, tar, p.get('spi1'))
except ValueError as e:
raise testscript.ScriptError(str(e))
spi2 = p.get('spi2') or '01'
counters = ctx.setdefault('counters', {})
if kvn and kvn not in counters:
counters[kvn] = (keyset or {}).get('cntr') or '00000000'
@@ -7210,14 +7269,29 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(resp, 400)
self._log_resp(resp)
return
# SPI1 for the packet: the caller's value wins (a hand send may
# 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).
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'))
except ValueError as e:
resp = {'error': str(e)}
self._send_json(resp, 400)
self._log_resp(resp)
return
spi2 = str(body.get('spi2') or '01').strip().upper()
# Only a packet whose SPI1 asks the card to check the counter has a
# counter to track (TS 102 225 5.1.1 b5b4): with b5b4 = 00 (the
# keyless SPI1 00 case) the field is ignored and never updated, so
# no counter is advanced or persisted.
counter_tracked = _counter_tracked(body.get('spi1', ''))
counter_tracked = _counter_tracked(spi1)
if body.get('cntr') and not counter_tracked:
sys.stderr.write('OTA SEND: counter not tracked (SPI1 %s: '
'no counter check)\n' % body.get('spi1', ''))
'no counter check)\n' % spi1)
include_cpi = body.get('includeCpi', True)
ram_format = None
try:
@@ -7225,8 +7299,6 @@ 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.
spi1 = body.get('spi1', '16')
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
tar = body.get('tar', '000000')
@@ -7256,7 +7328,7 @@ class PysimHandler(BaseHTTPRequestHandler):
# Regular SCP80: use pre-built secured packet
sp_hex = sp
cntr = body.get('cntr', '')
spi2_val = int(body.get('spi2', '00'), 16)
spi2_val = int(spi2, 16)
# Capture the PoR from either transport: inline in the
# ENVELOPE response or as a proactive SEND SHORT MESSAGE (some
# cards always submit it, whatever the SPI2 request bit says;
@@ -7269,7 +7341,7 @@ class PysimHandler(BaseHTTPRequestHandler):
scc._tp.proactive_handler = submit_handler
try:
sys.stderr.write('OTA SEND: SPI %s %s KIc %s KID %s TAR %s CNTR %s LEN %dB\n' % (
body.get('spi1', ''), body.get('spi2', ''), body.get('kic', ''),
spi1, spi2, body.get('kic', ''),
body.get('kid', ''), body.get('tar', ''), body.get('cntr', ''),
len(sp_hex) // 2))
sys.stderr.write('RAM C-APDU: %s\n' % apdu if apdu else sp)
@@ -7288,7 +7360,7 @@ class PysimHandler(BaseHTTPRequestHandler):
failed_por_hex = result.get('response_data') or ''
if not failed_por_hex and submit_handler:
failed_por_hex = _sms_submit_por(submit_handler)
failed_por = _decode_por(body.get('spi1', ''), body.get('spi2', ''),
failed_por = _decode_por(spi1, spi2,
body.get('kic', ''), body.get('kid', ''),
body.get('cntr', ''), body.get('kicKey', ''),
body.get('kidKey', ''), failed_por_hex)
@@ -7326,7 +7398,7 @@ class PysimHandler(BaseHTTPRequestHandler):
if idx >= 0:
por_hex = tpdu_b[idx:].hex()
por_src = 'sms-submit'
por = _decode_por(body.get('spi1', ''), body.get('spi2', ''), body.get('kic', ''),
por = _decode_por(spi1, spi2, body.get('kic', ''),
body.get('kid', ''), body.get('cntr', ''), body.get('kicKey', ''),
body.get('kidKey', ''), por_hex)
# Check for SPI2=0x21 (PoR required) but got 9000 with no PoR → card refuses PoR
@@ -7384,6 +7456,8 @@ class PysimHandler(BaseHTTPRequestHandler):
if counter_tracked:
_preset_counter_persist(self.server, body.get('preset_id'),
resp.get('final_cntr'), 'send-ota', kvn)
if spi_warn:
resp['warning'] = spi_warn
self._send_json(resp)
self._log_resp(resp)
except Exception as e:
@@ -7434,12 +7508,23 @@ class PysimHandler(BaseHTTPRequestHandler):
# Parse .cap file
loadfile_aid, module_aid, loadfile_data = _cap_parse(cap_hex)
# SCP80 params
spi1 = body.get('spi1', '16')
# SCP80 params: the SPI1 comes from the MSL of the packet's
# TAR (the operation uses the preset's table); a caller-supplied
# value is only the fallback for a TAR the preset does not carry.
preset = _preset_by_id(self.server, body.get('preset_id'))
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
or '000000').strip().upper()
try:
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'),
prefer_msl=True)
except ValueError as e:
err = {'success': False, 'error': str(e)}
self._send_json(err, 400)
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
tar = body.get('tar', '000000')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
@@ -7612,9 +7697,9 @@ class PysimHandler(BaseHTTPRequestHandler):
stk_params=stk_params_hex, make_selectable=make_selectable)
step_name = 'INSTALL [for install]'
sp_state = {
'spi1': body.get('spi1', '16'), 'spi2': body.get('spi2', '01'),
'spi1': spi1, 'spi2': body.get('spi2', '01'),
'kic': body.get('kic', '25'), 'kid': body.get('kid', '25'),
'tar': body.get('tar', '000000'), 'cntr': body.get('cntr', '00000000'),
'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
@@ -7723,11 +7808,20 @@ class PysimHandler(BaseHTTPRequestHandler):
'(block size %d)\n' % (loadfile_aid, boundary,
total_blocks, block_size))
spi1 = body.get('spi1', '16')
preset = _preset_by_id(self.server, body.get('preset_id'))
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
or '000000').strip().upper()
try:
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'),
prefer_msl=True)
except ValueError as e:
err = {'success': False, 'error': str(e)}
self._send_json(err, 400)
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
tar = body.get('tar', '000000')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
@@ -7939,16 +8033,12 @@ class PysimHandler(BaseHTTPRequestHandler):
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.
spi1 = str(body.get('spi1') or '16').strip().upper()
# (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.
spi1_base = str(body.get('spi1') or '').strip().upper()
spi2 = str(body.get('spi2') or '01').strip().upper()
if not _counter_tracked(spi1):
err = {'success': False,
'error': 'the TAR probe needs a counter check - SPI1 %s has '
'none (b5b4 = 00)' % spi1}
self._send_json(err, 400)
self._log_resp(err)
return
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}
wanted = body.get('tars')
@@ -7959,26 +8049,49 @@ class PysimHandler(BaseHTTPRequestHandler):
entries.append((tar, labels.get(tar, '')))
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.
resolved = []
for tar, label 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)}
self._send_json(err, 400)
self._log_resp(err)
return
if not _counter_tracked(tar_spi1):
err = {'success': False,
'error': 'the TAR probe needs a counter check - SPI1 %s '
'for TAR %s has none (b5b4 = 00)'
% (tar_spi1, tar)}
self._send_json(err, 400)
self._log_resp(err)
return
resolved.append((tar, label, tar_spi1))
state = {'steps': [], 'encode_error': None, 'failure': {},
'cntr': body.get('cntr', '00000000'),
'preset_id': body.get('preset_id'), 'kvn': kvn}
sp = {'spi1': spi1, 'spi2': spi2, 'kic': kic, 'kid': kid, 'tar': '000000',
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': kic, 'kid': kid, 'tar': '000000',
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'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, spi2, kvn))
% (len(entries), apdu, spi1_base or '-', spi2, kvn))
_ram_progress_begin('tar-probe', len(entries))
try:
for index, (tar, _label) in enumerate(entries):
for index, (tar, _label, tar_spi1) in enumerate(resolved):
_ram_progress_step(index, 'TAR ' + tar)
sp['tar'] = tar
sp['spi1'] = tar_spi1
_ram_send_gp_apdu(self.server, scc, sp, state, 'TAR ' + tar, apdu)
finally:
_ram_progress_end()
results = []
for (tar, label), step in zip(entries, state['steps']):
for (tar, label, tar_spi1), step in zip(resolved, state['steps']):
results.append({
'tar': tar, 'label': label, 'verdict': _tar_probe_verdict(step),
'tar': tar, 'label': label, 'spi1': tar_spi1,
'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'),
'bytes': step.get('bytes'), 'segments': step.get('segments'),
@@ -7987,7 +8100,7 @@ class PysimHandler(BaseHTTPRequestHandler):
resp = {'success': True, 'results': results, 'steps': state['steps'],
'registered': registered, 'total': len(results),
'final_cntr': state['cntr'], 'kvn': kvn,
'spi1': spi1, 'spi2': spi2, 'apdu': apdu}
'spi1': spi1_base, 'spi2': spi2, 'apdu': apdu}
self._send_json(resp)
self._log_resp(resp)
elif self.path == '/api/counter-probe':