Low-counter reporting, counter sync probe, Bad-format decode, expanded forms
Live findings that drove this: the "card counter 0000000000" the low-counter notice showed is not the card's counter at all (the PoR's CNTR is a copy of the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it zeroed); and an earlier "no card accepts the expanded format" observation was a hand-made template whose declared length did not match its body - the card validates the lengths and answers with a Bad format TLV. - _cntr_low_fields(status) reports the verdict as a flag; card_cntr/ suggested_cntr and the UI's "(card counter ...)" text are gone - the value is not observable. The notice offers a bounded Sync counter probe instead. - POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...) with a read-only command until the card accepts one, then accepted+1 is persisted (monotonic, source counter-probe). Rejected attempts change nothing on the card; the search stops at a ceiling (default FFFFFFFF, far below the 40-bit maximum where the counter blocks) and after max_attempts (default 40, clamped 1-64); a non-cntr_low failure stops it at once. - _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table 7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR carries bad_format/bad_format_name and _ram_step_result fails the step with "bad format 02 (wrong length)" instead of falling through to the compact decoder (which read the AB tag as 171 commands). - _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite (AE 80 ... 00 00) scripting template; the RAM command formats are compact/ expanded/expanded-ae and _ram_detect_format probes compact, then both expanded codings (the expanded probes use Le='00' and no chained GET RESPONSE, TS 102 226 5.2.1.1). The RAM form gains a Format selector. - Live-verified: the verdict carries no value; the probe walked 0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000) and a length-mismatched template answers bad_format 02 - decoded now. - Tests: the ladder, the probe walk + persistence, params validation, both template forms, the Bad-format TLV (definite and plain tag styles), the live ISD response through _decode_por, the step-result failure, and the PWA guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
This commit is contained in:
+320
-61
@@ -1407,25 +1407,20 @@ def _por_remote_sw(por):
|
||||
return ''
|
||||
|
||||
|
||||
def _cntr_low_fields(status, cntr_hex):
|
||||
"""The card's `cntr_low` verdict as plain data: the card's own counter and
|
||||
the next valid value (the packet counter must be above it). {} when the
|
||||
verdict is not a low counter.
|
||||
def _cntr_low_fields(status):
|
||||
"""The card's `cntr_low` verdict as a flag - the packet's counter was not
|
||||
above the card's for that key version - or {} for any other verdict.
|
||||
|
||||
A low counter is NOT recoverable by retrying: every further packet with a
|
||||
counter <= the card's is rejected again, so operations must stop and the
|
||||
user must raise the preset counter (v3.6.58)."""
|
||||
The card's own counter is deliberately not reported: the response
|
||||
packet's CNTR is "a copy of the contents of the CNTR in the Command
|
||||
Packet" (TS 102 225 §5.2, Table 3) and a cntr_low rejection leaves it
|
||||
zeroed, so no usable value can be derived from it (v3.6.58). A low
|
||||
counter is not recoverable by retrying: the operation stops and the user
|
||||
raises the preset counter above the card's (the `counter-probe` endpoint
|
||||
can do that in bounded steps)."""
|
||||
if str(status or '') != 'cntr_low':
|
||||
return {}
|
||||
card = str(cntr_hex or '').upper()
|
||||
suggested = None
|
||||
if card:
|
||||
try:
|
||||
width = 4 * len(card)
|
||||
suggested = '%0*X' % (len(card), (int(card, 16) + 1) % (1 << width))
|
||||
except ValueError:
|
||||
suggested = None
|
||||
return {'cntr_low': True, 'card_cntr': card, 'suggested_cntr': suggested}
|
||||
return {'cntr_low': True}
|
||||
|
||||
|
||||
def _ram_next_cntr(cntr, advance):
|
||||
@@ -1514,36 +1509,59 @@ def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
|
||||
# and never stored: cards differ batch to batch and may behave differently
|
||||
# later, so every operation re-checks (v3.6.24).
|
||||
_RAM_PROBE_APDU = '80F28000024F0000C0000000' # GET STATUS [ISD], read-only
|
||||
# The expanded format does not use GET RESPONSE (TS 102 226 5.2.1.1): with
|
||||
# Le='00' the whole response comes back in the R-APDU, so the expanded probes
|
||||
# drop the chained GET RESPONSE of the compact form.
|
||||
_RAM_PROBE_APDU_LE = '80F28000024F0000'
|
||||
|
||||
|
||||
def _wrap_expanded_apdu(apdu_hex):
|
||||
"""Wrap a C-APDU in the expanded remote-management format: the Command
|
||||
TLV '22' inside the 'AA' template (TS 102 226 5.2.1), the exact form of
|
||||
the reference trace `AA0A220880F24000024F0000`."""
|
||||
def _wrap_expanded_apdu(apdu_hex, form='definite'):
|
||||
"""Wrap a C-APDU in the expanded remote-management format (TS 102 226
|
||||
5.2.1): the Command TLV '22' inside the Command Scripting template.
|
||||
|
||||
form='definite' -> `AA <len> 22 <len> <apdu>` (TS 101 220 table 7.19
|
||||
definite length coding; the reference trace's form
|
||||
`AA0A220880F24000024F0000`)
|
||||
form='indefinite' -> `AE 80 22 <len> <apdu> 00 00` (indefinite length
|
||||
coding, the variant the spec recommends for RAM/RFM
|
||||
over HTTPS; some cards accept only this one)."""
|
||||
cmd = '22' + _ber_len(len(apdu_hex) // 2) + apdu_hex
|
||||
if form == 'indefinite':
|
||||
return ('AE80' + cmd + '0000').upper()
|
||||
return ('AA' + _ber_len(len(cmd) // 2) + cmd).upper()
|
||||
|
||||
|
||||
def _ram_format_apdu(apdu_hex, ram_format):
|
||||
"""Apply the RAM command format ('compact' or 'expanded')."""
|
||||
return _wrap_expanded_apdu(apdu_hex) if ram_format == 'expanded' else apdu_hex
|
||||
"""Apply the RAM command format (TS 102 226 5.2.1): the bare C-APDU
|
||||
(compact), the definite-length scripting template (expanded) or the
|
||||
indefinite-length one (expanded-ae)."""
|
||||
if ram_format == 'expanded':
|
||||
return _wrap_expanded_apdu(apdu_hex, 'definite')
|
||||
if ram_format == 'expanded-ae':
|
||||
return _wrap_expanded_apdu(apdu_hex, 'indefinite')
|
||||
return apdu_hex
|
||||
|
||||
|
||||
def _ram_normalize_format(value):
|
||||
"""'auto' | 'compact' | 'expanded'; anything else (or missing) is
|
||||
'auto' for callers that opted in to the format handling."""
|
||||
"""'auto' | 'compact' | 'expanded' | 'expanded-ae'; anything else (or
|
||||
missing) is 'auto' for callers that opted in to the format handling."""
|
||||
v = str(value or 'auto').strip().lower()
|
||||
return v if v in ('auto', 'compact', 'expanded') else 'auto'
|
||||
return v if v in ('auto', 'compact', 'expanded', 'expanded-ae') else 'auto'
|
||||
|
||||
|
||||
def _ram_detect_format(server, scc, sp, state):
|
||||
"""Detect the card's RAM command format for one operation: send the
|
||||
read-only GET STATUS [ISD] probe compact, then wrapped; the first format
|
||||
whose remote SW succeeds wins (compact preferred). Probe packets are
|
||||
recorded as steps and consume counters only when accepted; the result is
|
||||
used for this operation only - the next operation re-checks (v3.6.24)."""
|
||||
read-only GET STATUS [ISD] probe compact, then in the two expanded
|
||||
codings; the first format whose remote SW succeeds wins (compact
|
||||
preferred). Cards implement different subsets - the live ISD answers the
|
||||
definite form with a Bad format TLV (wrong length) and executes the
|
||||
indefinite one, while an RFM applet took the definite form. Probe
|
||||
packets are recorded as steps and consume counters only when accepted;
|
||||
the result is used for this operation only - the next operation re-checks
|
||||
(v3.6.24)."""
|
||||
for fmt, apdu in (('compact', _RAM_PROBE_APDU),
|
||||
('expanded', _wrap_expanded_apdu(_RAM_PROBE_APDU))):
|
||||
('expanded', _wrap_expanded_apdu(_RAM_PROBE_APDU_LE, 'definite')),
|
||||
('expanded-ae', _wrap_expanded_apdu(_RAM_PROBE_APDU_LE, 'indefinite'))):
|
||||
scratch = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': state['cntr'], 'preset_id': state.get('preset_id')}
|
||||
ok = _ram_send_gp_apdu(server, scc, sp, scratch,
|
||||
@@ -1555,6 +1573,187 @@ def _ram_detect_format(server, scc, sp, state):
|
||||
return 'compact'
|
||||
|
||||
|
||||
# Counter synchronisation probe (v3.9.x): a bounded upward search for a
|
||||
# counter value the card accepts. The card only accepts a counter *above* its
|
||||
# own (TS 102 225 5.1.1 b5b4 = 10) and updates it only when the packet is
|
||||
# accepted, so rejected attempts change nothing; the search walks a doubling
|
||||
# ladder (start+1, +2, +4, ...) and stops at a ceiling far below the 40-bit
|
||||
# maximum, where the counter would get blocked.
|
||||
COUNTER_PROBE_DEFAULT_APDU = '00A40000023F00' # SELECT MF: read-only, applet-visible
|
||||
COUNTER_PROBE_CEILING = 'FFFFFFFF' # 32-bit default
|
||||
COUNTER_PROBE_MAX_ATTEMPTS = 40 # doubling covers 40 bits
|
||||
|
||||
|
||||
def _counter_probe_candidates(start_hex, ceiling_hex, max_attempts):
|
||||
"""The cautious counter ladder: start + 1, +2, +4, ... (doubling).
|
||||
|
||||
Returns ``(candidates, reason)``: reason is 'ceiling' when the next value
|
||||
would exceed the ceiling, else 'attempts' when the attempt budget ran
|
||||
out. Doubling keeps the packet count low (<= ~40 across the whole 40-bit
|
||||
range) and the accepted value below ~2x the card's counter - only the
|
||||
accepted packet advances anything, every rejected one is a no-op."""
|
||||
try:
|
||||
value = int(str(start_hex or '0'), 16)
|
||||
ceiling = int(str(ceiling_hex or COUNTER_PROBE_CEILING), 16)
|
||||
max_attempts = int(max_attempts)
|
||||
except (TypeError, ValueError):
|
||||
return [], 'attempts'
|
||||
candidates = []
|
||||
step = 1
|
||||
while len(candidates) < max_attempts:
|
||||
value += step
|
||||
if value > ceiling:
|
||||
return candidates, 'ceiling'
|
||||
candidates.append('%010X' % value)
|
||||
step *= 2
|
||||
return candidates, 'attempts'
|
||||
|
||||
|
||||
def _counter_probe_params(preset, body):
|
||||
"""Resolve a counter probe's parameters from the named preset: the keyset
|
||||
(by `kvn`, else the first), its keys and counter, the preset's SPI/TAR
|
||||
(overridable), the ceiling and the attempt budget. Raises ValueError
|
||||
with a user-facing message."""
|
||||
keysets = _preset_keysets(preset)
|
||||
if not keysets:
|
||||
raise ValueError('the preset defines no keyset')
|
||||
want = body.get('kvn')
|
||||
if want not in (None, ''):
|
||||
try:
|
||||
want = int(want)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError('invalid keyset number')
|
||||
keyset = next((k for k in keysets if presets.keyset_kvn(k) == want), None)
|
||||
if keyset is None:
|
||||
raise ValueError('keyset %d is not defined in the preset' % want)
|
||||
else:
|
||||
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()
|
||||
kid_key = str(keyset.get('kidKey') or '').strip()
|
||||
if not (kic and kid and kic_key and kid_key):
|
||||
raise ValueError('the keyset needs KIc, KID and both keys')
|
||||
kvn, err = _kvn_of(kic, kid)
|
||||
if err:
|
||||
raise ValueError(err)
|
||||
start = str(body.get('cntr') or keyset.get('cntr') or '').strip().upper()
|
||||
if not _counter_valid(start):
|
||||
raise ValueError('the keyset needs a counter to start from')
|
||||
ceiling = str(body.get('ceiling') or COUNTER_PROBE_CEILING).strip().upper()
|
||||
try:
|
||||
ceiling_value = int(ceiling, 16)
|
||||
except ValueError:
|
||||
raise ValueError('invalid ceiling')
|
||||
if ceiling_value < int(start, 16):
|
||||
raise ValueError('the ceiling is below the current counter')
|
||||
if ceiling_value > 0xFFFFFFFFFE:
|
||||
raise ValueError('the ceiling must stay below the 40-bit maximum')
|
||||
try:
|
||||
max_attempts = int(body.get('max_attempts') or COUNTER_PROBE_MAX_ATTEMPTS)
|
||||
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()
|
||||
if not _counter_tracked(spi1):
|
||||
raise ValueError('the probe needs a counter check - SPI1 %s has none '
|
||||
'(b5b4 = 00)' % spi1)
|
||||
return {
|
||||
'kvn': kvn, 'kic': kic, 'kid': kid,
|
||||
'kic_key': kic_key, 'kid_key': kid_key,
|
||||
'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(),
|
||||
'apdu': str(body.get('apdu') or COUNTER_PROBE_DEFAULT_APDU).replace(' ', '').upper(),
|
||||
}
|
||||
|
||||
|
||||
def _counter_probe(server, scc, preset, body, send_fn=None):
|
||||
"""Sync a preset's counter with the card: send a read-only probe command
|
||||
with increasing counter values until the card accepts one (PoR `por_ok`),
|
||||
then persist accepted+1 (monotonic).
|
||||
|
||||
Every rejected attempt is a no-op on the card; the ceiling and the attempt
|
||||
budget bound the search away from the 40-bit maximum, where the counter
|
||||
would be blocked. Returns a result dict with the attempt list - the
|
||||
caller (UI) retries the failed operation afterwards. `send_fn` is a test
|
||||
seam (defaults to the `_send_secured_packet` path)."""
|
||||
p = _counter_probe_params(preset, body)
|
||||
candidates, stop = _counter_probe_candidates(p['start'], p['ceiling'],
|
||||
p['max_attempts'])
|
||||
out = {'success': False, 'preset_id': preset.get('id'),
|
||||
'preset_name': preset.get('name'), 'kvn': p['kvn'],
|
||||
'start': p['start'], 'ceiling': p['ceiling'],
|
||||
'max_attempts': p['max_attempts'], 'tar': p['tar'],
|
||||
'spi1': p['spi1'], 'spi2': p['spi2'], 'apdu': p['apdu'],
|
||||
'packets': 0, 'attempts': [], 'stopped': stop}
|
||||
if not candidates:
|
||||
out['error'] = 'the counter is already at the ceiling %s' % p['ceiling']
|
||||
return out
|
||||
for cntr in candidates:
|
||||
if send_fn is not None:
|
||||
send, por = send_fn(p, cntr)
|
||||
else:
|
||||
sp_hex, _ = _build_secured_packet(p['spi1'], p['spi2'], p['kic'], p['kid'],
|
||||
p['tar'], cntr, p['apdu'],
|
||||
p['kic_key'], p['kid_key'])
|
||||
submit_handler = PoRSubmitHandler()
|
||||
old_proactive = None
|
||||
if hasattr(scc, '_tp'):
|
||||
old_proactive = scc._tp.proactive_handler
|
||||
scc._tp.proactive_handler = submit_handler
|
||||
try:
|
||||
send = _send_secured_packet(scc, sp_hex, oa_number=server.sms_oa,
|
||||
sm_sc=server.sms_sc, include_cpi=True,
|
||||
submit_handler=submit_handler)
|
||||
finally:
|
||||
if hasattr(scc, '_tp'):
|
||||
scc._tp.proactive_handler = old_proactive
|
||||
por_hex = send.get('response_data') or ''
|
||||
submit_hex = _sms_submit_por(submit_handler)
|
||||
if submit_hex:
|
||||
por_hex = submit_hex
|
||||
por = _decode_por(p['spi1'], p['spi2'], p['kic'], p['kid'], cntr,
|
||||
p['kic_key'], p['kid_key'], por_hex) if por_hex else None
|
||||
pstatus = str((por or {}).get('response_status') or '')
|
||||
out['packets'] += 1
|
||||
attempt = {'cntr': cntr, 'por_status': pstatus or None,
|
||||
'sw': send.get('sw'), 'bytes': send.get('bytes'),
|
||||
'segments': send.get('segments')}
|
||||
remote_sw = _por_remote_sw(por)
|
||||
if remote_sw:
|
||||
attempt['remote_sw'] = remote_sw
|
||||
if (por or {}).get('bad_format'):
|
||||
attempt['bad_format'] = por['bad_format']
|
||||
out['attempts'].append(attempt)
|
||||
sys.stderr.write('COUNTER-PROBE: %s -> %s\n'
|
||||
% (cntr, pstatus or ('ENVELOPE %s' % (send.get('sw') or '?'))))
|
||||
if pstatus == 'por_ok':
|
||||
stored = _ram_next_cntr(cntr, True)
|
||||
_preset_counter_persist(server, preset.get('id'), stored,
|
||||
'counter-probe', p['kvn'])
|
||||
out.update({'success': True, 'stopped': 'accepted',
|
||||
'accepted_cntr': cntr, 'stored_cntr': stored})
|
||||
sys.stderr.write('COUNTER-PROBE: accepted %s, preset stores %s\n'
|
||||
% (cntr, stored))
|
||||
return out
|
||||
if pstatus == 'cntr_low':
|
||||
continue
|
||||
out['stopped'] = 'error'
|
||||
out['error'] = send.get('error') or ('card verdict: %s' % (pstatus or 'no PoR'))
|
||||
return out
|
||||
if stop == 'ceiling':
|
||||
out['error'] = ('no counter up to the ceiling %s was accepted - the card\'s '
|
||||
'counter is above it, raise the preset counter manually'
|
||||
% p['ceiling'])
|
||||
else:
|
||||
out['error'] = ('no counter above %s was accepted in %d attempts'
|
||||
% (p['start'], p['max_attempts']))
|
||||
return out
|
||||
|
||||
|
||||
# Live progress of the RAM operation currently running. The long install
|
||||
# chains hold _CARD_LOCK for the whole request, so the PWA cannot learn the
|
||||
# step from the response - /api/status (lock-free cached state, polled every
|
||||
@@ -1607,7 +1806,13 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
||||
step['por_raw'] = por.get('raw')
|
||||
if remote_sw:
|
||||
step['por_sw'] = remote_sw
|
||||
if pstatus != 'por_ok':
|
||||
if por.get('bad_format'):
|
||||
# the expanded script was rejected as malformed before executing:
|
||||
# there is no remote status word
|
||||
step['por_bad_format'] = por['bad_format']
|
||||
error = 'bad format %s (%s)' % (por['bad_format'],
|
||||
por.get('bad_format_name') or '')
|
||||
elif pstatus != 'por_ok':
|
||||
error = 'PoR %s' % (pstatus or 'unknown')
|
||||
elif remote_sw and not _ram_remote_sw_ok(remote_sw):
|
||||
error = 'remote SW %s' % remote_sw
|
||||
@@ -1755,14 +1960,25 @@ def _ram_nv_fields(nv_before, nv_after):
|
||||
return {'nv_before': nv_before, 'nv_after': nv_after, 'nv_delta': delta}
|
||||
|
||||
|
||||
_BAD_FORMAT_NAMES = {
|
||||
'01': 'unknown tag',
|
||||
'02': 'wrong length',
|
||||
'03': 'length not found',
|
||||
}
|
||||
|
||||
|
||||
def _parse_response_scripting(data):
|
||||
"""Parse a Response Scripting template (TS 102 226 5.2.2, tables
|
||||
5.10/5.10a): `AB <len>` (definite) or `AF 80 ... 00 00` (indefinite),
|
||||
containing the executed-command-count TLV `80` and one or more R-APDU
|
||||
TLVs `23` (COMPREHENSION-TLV; the last two bytes are SW1 SW2).
|
||||
containing the executed-command-count TLV `80`, one or more R-APDU TLVs
|
||||
`23` (COMPREHENSION-TLV; the last two bytes are SW1 SW2) and/or the Bad
|
||||
format TLV `90` (TS 101 220 table 7.20; error type 01 unknown tag,
|
||||
02 wrong length, 03 length not found - TS 102 226 table 5.12).
|
||||
|
||||
Returns (count, sw, rapdu_data_hex) from the last R-APDU, or None when the
|
||||
data is not a scripting template."""
|
||||
Returns {'count', 'sw', 'data', 'bad_format'} or None when the data is
|
||||
not a scripting template. `bad_format` is the error type hex when the
|
||||
card aborted the script on a malformed command (then `sw` is None and
|
||||
`data` ''); an R-APDU is not required for a bad-format answer."""
|
||||
if not data:
|
||||
return None
|
||||
if data[0] == 0xAF:
|
||||
@@ -1778,6 +1994,7 @@ def _parse_response_scripting(data):
|
||||
return None
|
||||
count = None
|
||||
last = None
|
||||
bad = None
|
||||
off = 0
|
||||
while off < len(body) - 1:
|
||||
tag = body[off]
|
||||
@@ -1789,10 +2006,16 @@ def _parse_response_scripting(data):
|
||||
count = int.from_bytes(val, 'big')
|
||||
elif tag in (0x23, 0xA3) and len(val) >= 2:
|
||||
last = (val[-2:].hex().upper(), val[:-2].hex().upper())
|
||||
elif tag in (0x90, 0x10) and val:
|
||||
# both tag styles: the spec pins the CR flag to 0, cards mix them
|
||||
bad = val[:1].hex().upper()
|
||||
off = voff + ln
|
||||
if last is None:
|
||||
if last is None and bad is None:
|
||||
return None
|
||||
return (count, last[0], last[1])
|
||||
return {'count': count,
|
||||
'sw': last[0] if last else None,
|
||||
'data': last[1] if last else '',
|
||||
'bad_format': bad}
|
||||
|
||||
|
||||
def _sms_submit_por(submit_handler):
|
||||
@@ -1852,13 +2075,18 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon
|
||||
if res.response_status == 'por_ok' and len(res['secured_data']):
|
||||
scripted = _parse_response_scripting(bytes(res['secured_data']))
|
||||
if scripted is not None:
|
||||
count, sw, data_hex = scripted
|
||||
out['response_type'] = 'scripting'
|
||||
out['decoded'] = {
|
||||
'number_of_commands': count,
|
||||
'last_status_word': sw,
|
||||
'last_response_data': data_hex,
|
||||
'number_of_commands': scripted['count'],
|
||||
'last_status_word': scripted['sw'],
|
||||
'last_response_data': scripted['data'],
|
||||
}
|
||||
if scripted['bad_format']:
|
||||
# the card parsed the scripting template but aborted on a
|
||||
# malformed command (TS 102 226 5.2.2 Bad format TLV)
|
||||
out['bad_format'] = scripted['bad_format']
|
||||
out['bad_format_name'] = _BAD_FORMAT_NAMES.get(scripted['bad_format'],
|
||||
'unknown error type')
|
||||
elif dec is not None:
|
||||
out['response_type'] = 'compact'
|
||||
out['decoded'] = {
|
||||
@@ -5239,7 +5467,7 @@ _TEST_BLOCKED_PATHS = frozenset([
|
||||
'/api/command', '/api/cardinfo', '/api/tree', '/api/select', '/api/read',
|
||||
'/api/write', '/api/apdu', '/api/verify-adm', '/api/send-ota',
|
||||
'/api/ram-install', '/api/ram-install-app', '/api/cap-compat',
|
||||
'/api/tar-probe',
|
||||
'/api/tar-probe', '/api/counter-probe',
|
||||
'/api/sp-verify', '/api/menu-select',
|
||||
'/api/menu-respond', '/api/event-send', '/api/net-sim',
|
||||
'/api/net-state-refresh', '/api/status-poll', '/api/rescue',
|
||||
@@ -7061,13 +7289,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
body.get('kidKey', ''), failed_por_hex)
|
||||
if failed_por:
|
||||
resp['por'] = failed_por
|
||||
low = _cntr_low_fields(failed_por.get('response_status'),
|
||||
failed_por.get('cntr'))
|
||||
low = _cntr_low_fields(failed_por.get('response_status'))
|
||||
if low:
|
||||
resp.update(low)
|
||||
sys.stderr.write('OTA CNTR-LOW: card counter %s '
|
||||
'(the packet counter must be above it)\n'
|
||||
% low['card_cntr'])
|
||||
sys.stderr.write('OTA CNTR-LOW: the packet counter is not '
|
||||
'above the card\'s\n')
|
||||
sys.stderr.write('OTA SEND FAILED: %s\n' % result.get('error'))
|
||||
else:
|
||||
resp = {'success': True, 'sw': result['sw'],
|
||||
@@ -7100,16 +7326,19 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
sys.stderr.write('RAM RESPONSE-PACKET: %s\n' % (por_hex if por_hex else 'empty'))
|
||||
if por:
|
||||
resp['por'] = por
|
||||
low = _cntr_low_fields(por.get('response_status'), por.get('cntr'))
|
||||
low = _cntr_low_fields(por.get('response_status'))
|
||||
if low:
|
||||
# a 9000/91xx ENVELOPE can still carry a
|
||||
# cntr_low PoR (the live card does)
|
||||
resp.update(low)
|
||||
sys.stderr.write('OTA CNTR-LOW: card counter %s '
|
||||
'(the packet counter must be above it)\n'
|
||||
% low['card_cntr'])
|
||||
sys.stderr.write('OTA CNTR-LOW: the packet counter is not '
|
||||
'above the card\'s\n')
|
||||
extra = ''
|
||||
if por.get('decoded'):
|
||||
if por.get('bad_format'):
|
||||
extra = ' (expanded: bad format %s - %s)' % (
|
||||
por['bad_format'], por.get('bad_format_name') or '')
|
||||
sys.stderr.write('RAM R-APDU: (bad format %s)\n' % por['bad_format'])
|
||||
elif por.get('decoded'):
|
||||
extra = ' (compact: %s cmd, last SW %s)' % (por['decoded'].get('number_of_commands', '?'),
|
||||
por['decoded'].get('last_status_word', '?'))
|
||||
sys.stderr.write('RAM R-APDU: %s\n' % por['decoded'].get('last_response_data', ''))
|
||||
@@ -7297,12 +7526,10 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'load_block_size_auto': not block_size_req}
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
failed_rec = steps[-1] if steps else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status'),
|
||||
failed_rec.get('por_cntr')))
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
if resp.get('cntr_low'):
|
||||
sys.stderr.write('RAM-INSTALL: low counter - card counter %s, '
|
||||
'the preset counter must be above it\n'
|
||||
% resp.get('card_cntr'))
|
||||
sys.stderr.write('RAM-INSTALL: low counter - the packet '
|
||||
'counter is not above the card\'s\n')
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
@@ -7408,8 +7635,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
or ('%s failed' % step_name))
|
||||
resp['failed_step'] = len(state['steps'])
|
||||
failed_rec = state['steps'][-1] if state['steps'] else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status'),
|
||||
failed_rec.get('por_cntr')))
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
except Exception as e:
|
||||
@@ -7535,8 +7761,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
or 'the card rejected the import list')
|
||||
resp['failed_step'] = len(steps)
|
||||
failed_rec = steps[-1] if steps else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status'),
|
||||
failed_rec.get('por_cntr')))
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
sys.stderr.write('CAP-COMPAT: %s — imports %s (LOADS to %d/%d)\n' % (
|
||||
loadfile_aid, 'OK' if ok else 'REJECTED', boundary, total_blocks))
|
||||
self._send_json(resp)
|
||||
@@ -7748,6 +7973,40 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'spi1': spi1, 'spi2': spi2, 'apdu': apdu}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/counter-probe':
|
||||
scc = self.server.scc
|
||||
if not scc:
|
||||
self._send_json({'error': _err('reader_not_init', lang)}, 503)
|
||||
self._log_resp({'error': _err('reader_not_init', lang)})
|
||||
return
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
if not body.get('preset_id'):
|
||||
resp = {'success': False, 'error': 'preset_id is required'}
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
store = self._preset_store_or_503()
|
||||
if store is None:
|
||||
return
|
||||
preset = store.get(body.get('preset_id'))
|
||||
if not preset:
|
||||
resp = {'success': False, 'error': 'unknown preset'}
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
sys.stderr.write('COUNTER-PROBE: preset %s, start %s, ceiling %s\n'
|
||||
% (preset.get('name'), body.get('cntr', ''),
|
||||
body.get('ceiling', COUNTER_PROBE_CEILING)))
|
||||
try:
|
||||
resp = _counter_probe(self.server, scc, preset, body)
|
||||
except ValueError as e:
|
||||
resp = {'success': False, 'error': str(e)}
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/presets':
|
||||
body = self._read_body()
|
||||
self._log_req(body)
|
||||
|
||||
Reference in New Issue
Block a user