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:
+115
-13
@@ -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
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user