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:
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user