docs: help/README catch up with v3.8.0; keyset-number wording; RU "пресет"

Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
  section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
  Keyset selector next to the preset selector (and that a successful PoR
  advances the counter only when SPI1 carries a counter check); §4 describes
  the keyset editor rows, the derived number and the per-keyset counters; the
  probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
  "TAR-сканер" section (standard list, verdicts, counter persistence); the
  Cards section is rewritten for the server-side store - keysets, server-owned
  per-keyset counters with the audit trail, JSON export/import as the migration
  path, the keyset editor, the equipped-card mismatch banner - and the field
  table, "Add a card" and "Edit / remove" follow.

Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
  strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
  the messages say "keyset N ...".  The TS 102 225 A.1/A.2 quotes and the GP
  PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
  "не является keyset пресета", ...) and uses "пресет" instead of
  "предустановка" throughout the dictionary, the help and README_RUS.

sw cache simple-v340.  Tests: 742 Node / 619 Python.
This commit is contained in:
2026-09-30 23:48:32 +03:00
parent 31715ba58a
commit 9f7876b5fa
13 changed files with 207 additions and 180 deletions
+27 -27
View File
@@ -9,13 +9,13 @@ 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 one **keyset per GlobalPlatform key version** (the b8..b5 nibble
of KIc/KID, TS 102 225 5.1.2/A.2), each with its own KIc/KID keys and its own
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"
(TS 102 225 Annex A.1). The two bytes of a keyset must carry the same key
version (A.2: the card rejects a mismatch with "Unidentified security error");
key version '00' is reserved and not a keyset (it means "no security" in a
packet, which is a form-level choice, not a preset).
(TS 102 225 Annex A.1, the spec's wording). The two bytes of a keyset must
carry the same number (A.2: the card rejects a mismatch with "Unidentified
security error"); number '00' is reserved and not a keyset (it means "no
security" in a packet, which is a form-level choice, not a preset).
Layout (``--card-presets`` overrides the file; ``Path.home()`` resolves the
same way on Linux, macOS and Windows)::
@@ -50,9 +50,9 @@ SCHEMA_VERSION = 2
# 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
# material lives in `keysets` - one entry per GlobalPlatform key version (the
# b8..b5 nibble of KIc/KID), each with its own counter (TS 102 225 Annex A.1:
# "a dedicated counter shall be associated to each key version").
# 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')
# The v3.8.0 flat key fields: converted into a single keyset on load/import.
@@ -63,7 +63,7 @@ KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
TAR_DEFAULTS = {'tar': '000000', 'uiccTar': 'B00000', 'usimTar': 'B00001'}
COUNTER_BITS = 40 # the SCP80 counter is 5 bytes (TS 31.115)
COUNTER_WIDTH = 10 # ... shown as 10 hex digits
KVN_MAX = 0x0F # key version '00' is reserved: no key set, no counter
KVN_MAX = 0x0F # keyset number '00' is reserved: no key set, no counter
def default_path():
@@ -72,9 +72,9 @@ def default_path():
def keyset_kvn(keyset):
"""The key version of a keyset: the high nibble of KIc (bits b8..b5 of
KIc/KID, TS 102 225 5.1.2/A.2 - both bytes must agree, so KIc is enough).
None when the byte is unusable."""
"""The keyset number: the high nibble of KIc (bits b8..b5 of KIc/KID,
TS 102 225 5.1.2/A.2 - both bytes must agree, so KIc is enough). None
when the byte is unusable."""
kic = str((keyset or {}).get('kic') or '').strip().upper()
if not re.fullmatch(r'[0-9A-F]{2}', kic):
return None
@@ -210,7 +210,7 @@ class PresetStore:
pass
def _audit(self, pid, name, old, new, source, kvn=None):
"""One JSONL line per counter change (the key version included - each
"""One JSONL line per counter change (the keyset number included - each
keyset has its own counter): the forensic trail for "did an increment
get lost?" questions."""
try:
@@ -279,7 +279,7 @@ class PresetStore:
self._validate_keyset(ks)
kvn = keyset_kvn(ks)
if kvn in seen:
raise PresetError('duplicate key version %02X in this preset' % kvn)
raise PresetError('duplicate keyset number %02X in this preset' % kvn)
seen.add(kvn)
norm = normalize_iccid(p['iccid'])
if norm:
@@ -292,20 +292,20 @@ class PresetStore:
return p
def _validate_keyset(self, ks):
"""One keyset: KIc/KID carry the key version in b8..b5 and must agree
(TS 102 225 A.2 - a mismatch is rejected by the card), the version is
"""One keyset: KIc/KID carry its number in b8..b5 and must agree
(TS 102 225 A.2 - a mismatch is rejected by the card), the number is
01-0F ('00' means "no security" and has no keys to define, so it is
not a preset keyset), both keys are present and the counter is
1-10 hex digits (the dedicated counter of that key version, A.1)."""
1-10 hex digits (the dedicated counter of that keyset, A.1)."""
for key in ('kic', 'kid'):
if not re.fullmatch(r'[0-9A-F]{2}', ks[key]):
raise PresetError('%s must be one hex byte' % key)
kic_v, kid_v = int(ks['kic'], 16) >> 4, int(ks['kid'], 16) >> 4
if kic_v != kid_v:
raise PresetError('KIc and KID must carry the same key version '
raise PresetError('KIc and KID must carry the same keyset number '
'(TS 102 225 A.2): %s / %s' % (ks['kic'], ks['kid']))
if not 1 <= kic_v <= KVN_MAX:
raise PresetError('key version must be 01-0F: %s' % ks['kic'])
raise PresetError('keyset number must be 01-0F: %s' % ks['kic'])
for key in ('kicKey', 'kidKey'):
if not ks[key]:
raise PresetError('%s is required' % key)
@@ -397,8 +397,8 @@ class PresetStore:
return True
def find_keyset(self, pid, kvn):
"""The keyset of a preset for a key version (the b8..b5 nibble of
KIc/KID), or None when the preset or the key version is unknown."""
"""The keyset of a preset with the given number (the b8..b5 nibble of
KIc/KID), or None when the preset or the number is unknown."""
with self._lock:
p = self._find(pid)
if p is None:
@@ -409,9 +409,9 @@ class PresetStore:
return None
def set_counter(self, pid, cntr, source='operation', kvn=None):
"""Persist the counter an operation accepted, into the keyset of the
key version it used (each key version has its own dedicated counter,
TS 102 225 A.1). Monotonic per keyset: the counter only moves
"""Persist the counter an operation accepted, into the keyset it used
(each keyset has its own dedicated counter, TS 102 225 A.1).
Monotonic per keyset: the counter only moves
forward, so a stale caller can never regress it. `kvn=None` picks the
preset's only keyset (a convenience for single-keyset callers).
Returns the stored preset (or None when the id is unknown)."""
@@ -426,13 +426,13 @@ class PresetStore:
ks = cand
break
if ks is None:
sys.stderr.write('PRESETS: no key version %s in %s (%s)\n'
sys.stderr.write('PRESETS: no keyset %s in %s (%s)\n'
% (kvn, p['name'], source))
return copy.deepcopy(p)
elif len(p['keysets']) == 1:
ks = p['keysets'][0]
else:
sys.stderr.write('PRESETS: %s has several key versions - the '
sys.stderr.write('PRESETS: %s has several keysets - the '
'counter needs one (%s)\n' % (p['name'], source))
return copy.deepcopy(p)
new = str(cntr or '').strip().upper()
+23 -23
View File
@@ -1447,7 +1447,7 @@ def _counter_valid(cntr):
def _kvn_of(kic_hex, kid_hex):
"""The key version a packet uses: the b8..b5 nibble of KIc/KID (TS 102 225
"""The keyset number a packet uses: the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2). Returns ``(kvn, error)``: kvn is 0 when neither byte carries
one ('00' = no security, legal per A.2), error names a mismatch - A.2: the
versions shall be identical when different from 0, else the card rejects
@@ -1460,15 +1460,15 @@ def _kvn_of(kic_hex, kid_hex):
kic_v = int(kic, 16) >> 4 if kic else 0
kid_v = int(kid, 16) >> 4 if kid else 0
if kic_v and kid_v and kic_v != kid_v:
return None, ('KIc and KID must carry the same key version '
return None, ('KIc and KID must carry the same keyset number '
'(TS 102 225 A.2): %s / %s' % (kic, kid))
return (kic_v or kid_v), None
def _preset_keyset_check(server, preset_id, kic, kid):
"""The "the key version must be defined in the preset" rule: an error
string when the packet's (non-zero) key version has no keyset in the named
preset, else None. Without a preset_id (a raw API caller) there is
"""The "the keyset number must be defined in the preset" rule: an error
string when the packet's (non-zero) keyset number has no keyset in the
named preset, else None. Without a preset_id (a raw API caller) there is
nothing to check; '00'/'00' (no security) needs no keyset either."""
if not preset_id:
return None
@@ -1482,19 +1482,19 @@ def _preset_keyset_check(server, preset_id, kic, kid):
return None
if store.find_keyset(preset_id, kvn) is None:
preset = store.get(preset_id) or {}
return ('key version %d is not defined in preset %s - add it in the '
return ('keyset %d is not defined in preset %s - add it in the '
'Cards tab' % (kvn, preset.get('name') or preset_id))
return 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 of the key version it used (each key
version has its own dedicated counter, TS 102 225 A.1). The counter is
store (v3.8.0), into the keyset it used (each keyset has its own dedicated
counter, TS 102 225 A.1). The counter is
card state, so the server writes it with the operation that consumed it - a
closed tab, a lost response or a second browser window can no longer lose
an increment. The store itself is monotonic (a stale value never regresses
it). A key version is mandatory: a packet with no key version (KIc/KID
it). A keyset number is mandatory: a packet with no number (KIc/KID
'00', or a SPI1 without a counter check) has no counter to write. Best
effort: a store failure must never fail the card operation."""
if not preset_id or not cntr or not kvn:
@@ -5313,9 +5313,9 @@ def _test_preset_error(script, preset):
want = int(p['kvn'])
ks = next((k for k in keysets if presets.keyset_kvn(k) == want), None)
if ks is None:
return 'step %d: key version %d is not defined in the preset' % (i + 1, want)
return 'step %d: keyset %d is not defined in the preset' % (i + 1, want)
if not _keyset_complete(ks):
return ('step %d: key version %d is incomplete (KIc, KID, both '
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')):
return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1)
@@ -5445,16 +5445,16 @@ def _test_run_scp80(server, step, ctx):
scc = server.scc
preset = ctx['preset']
p = step['params']
# The keyset: a step may name a key version (`kvn`), otherwise the preset's
# first keyset is used (a preset carries one keyset per key version, each
# with its own keys and counter - TS 102 225 Annex A.1).
# 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).
keysets = _preset_keysets(preset)
keyset = None
if p.get('kvn') is not None:
want = int(p['kvn'])
keyset = next((ks for ks in keysets if presets.keyset_kvn(ks) == want), None)
if keyset is None:
raise testscript.ScriptError('key version %d is not defined in the '
raise testscript.ScriptError('keyset %d is not defined in the '
'selected card preset' % want)
elif keysets:
keyset = keysets[0]
@@ -5823,11 +5823,11 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp(resp)
def _keyset_guard(self, body, kic, kid):
"""The key-version checks shared by the RAM operations: the KIc/KID
versions must agree (TS 102 225 A.2 - the card rejects a mismatch) and a
named preset must define the key version ("the keyset must exist").
Returns ``(kvn, error)``: kvn is None when the packet carries no key
version ('00'/'00' = no security, legal per A.2), error an error string
"""The keyset-number checks shared by the RAM operations: the KIc/KID
numbers must agree (TS 102 225 A.2 - the card rejects a mismatch) and a
named preset must define the number ("the keyset must exist").
Returns ``(kvn, error)``: kvn is None when the packet carries no number
('00'/'00' = no security, legal per A.2), error an error string
the caller answers with 400."""
kvn, err = _kvn_of(kic, kid)
if err:
@@ -6914,7 +6914,7 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json({'error': _err('reader_not_init', lang)}, 503)
self._log_resp({'error': _err('reader_not_init', lang)})
return
# The packet's key version (b8..b5 of KIc/KID, TS 102 225 A.2) must
# The packet's keyset number (b8..b5 of KIc/KID, TS 102 225 A.2) must
# be consistent - a mismatch is invalid input (the card rejects it
# with "Unidentified security error") - and, when a preset is named,
# defined in it. Both are refused before anything is built (and
@@ -7160,7 +7160,7 @@ class PysimHandler(BaseHTTPRequestHandler):
make_selectable = body.get('make_selectable', True)
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
# The key version must be consistent and defined in the preset
# 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)
@@ -7330,7 +7330,7 @@ class PysimHandler(BaseHTTPRequestHandler):
'tar': body.get('tar', '000000'), 'cntr': body.get('cntr', '00000000'),
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'include_cpi': body.get('includeCpi', True)}
# The key version must be consistent and defined in the preset
# 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, sp_state['kic'], sp_state['kid'])