fix: report the real PoR/remote-SW result of every RAM install step (v3.6.2)

The RAM installer read `por['decoded']['response_status']`, but the compact
response decoder's `decoded` only carries {number_of_commands,
last_status_word, last_response_data} - so the suffix was always empty and
every step showed the useless `por_error_`, even when the PoR was por_ok.
The PoR verdict is the top-level `response_status`.

With that fixed, the decoded details also show that the remote command's own
status word was the real verdict all along: a captured live install returned
por_ok with `last_status_word` 6700/6F00 (the card rejected every RAM APDU)
while the installer reported success.

- new `_ram_step_result()`/`_por_remote_sw()`/`_ram_remote_sw_ok()`: a step
  fails on a non-por_ok PoR, on a remote SW outside the success set (9000,
  61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or on an
  undecodable PoR (a 9000 transport SW with no PoR at all is `no_por`, not a
  failure).  A decoded 61xx is explicitly success, per GP/ISO.
- step records gain por_sw/por_type/por_cntr/por_data/por_raw and
  `por_error`; the response carries `failed_step` and a detailed error such
  as `LOAD (1/9): remote SW 6700`; the log line now prints
  `status=por_ok remote_sw=6700`.
- PWA: new pure `ramStepLine()` renders e.g.
  `❌ Шаг 2: LOAD (1/9) — PoR ok · remote SW 6700 (Wrong length in Lc) · 274 B / 3 SMS`
  (SW meaning via the existing lookupSw decoder) and the transport SW only
  when it is not 9000.
- tests: tests/test_ota_helpers.py RamPorStepTest (success set incl. 61xx,
  the captured 6700 failure, no-PoR/undecodable, expanded responses);
  ram.test.js ramStepLine cases.
- docs/api.md RAM install step fields + failure semantics; AGENTS updated.

603 frontend / 487 Python green; version 3.6.2; sw simple-v275.
This commit is contained in:
2026-09-27 23:21:56 +03:00
parent b92a47cd44
commit 36d2f71bc7
7 changed files with 215 additions and 27 deletions
+80 -12
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.1'
VERSION = '3.6.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1095,6 +1095,69 @@ def _send_secured_packet(scc, sp_hex, oa_number, sm_sc=None, include_cpi=True,
'response_data': data if data else None}
# Remote-command status words that count as success in the RAM dialog:
# 9000 = normal; 61xx = more data available (GET RESPONSE); 62xx/63xx =
# warnings (63xx/63Cx often carry "more data available" too); CAFE =
# GlobalPlatform "more data available" (GET STATUS pages).
_RAM_REMOTE_SW_OK = ('9000', 'CAFE')
def _ram_remote_sw_ok(sw):
s = str(sw or '').upper()
return bool(s) and (s in _RAM_REMOTE_SW_OK or s[:2] in ('61', '62', '63'))
def _por_remote_sw(por):
"""Last remote-command status word from a decoded PoR, '' when none:
compact responses carry last_status_word, expanded ones a status word
per command."""
dec = (por or {}).get('decoded') or {}
sw = str(dec.get('last_status_word') or '').upper()
if sw:
return sw
for r in reversed((por or {}).get('responses') or []):
if r.get('status_word'):
return str(r['status_word']).upper()
return ''
def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
"""Assemble a RAM-install step record and its failure reason.
The PoR verdict is the top-level `response_status`; the remote command's
own status word lives in `decoded.last_status_word` (compact) or in the
expanded `responses` list. A step fails on a non-por_ok PoR, on a remote
SW outside `_RAM_REMOTE_SW_OK`, or when a PoR arrived but could not be
decoded (a 9000 transport SW with no PoR at all is 'no_por', not a
failure)."""
step = {'name': step_name, 'sw': last_sw, 'bytes': bytes_, 'segments': segments}
error = None
if por:
pstatus = str(por.get('response_status') or '')
remote_sw = _por_remote_sw(por)
step['por_status'] = pstatus or 'unknown'
step['por_type'] = por.get('response_type')
step['por_cntr'] = por.get('cntr')
step['por_data'] = (por.get('decoded') or {}).get('last_response_data', '')
step['por_raw'] = por.get('raw')
if remote_sw:
step['por_sw'] = remote_sw
if 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
elif last_sw == '9000' and not por_hex:
step['por_status'] = 'no_por'
else:
step['por_status'] = 'unknown'
if por_hex:
step['por_raw'] = por_hex
error = 'PoR undecodable'
if error:
step['por_error'] = error
return step, error
def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, response_hex):
from pySim.ota import OtaDialectSms, CompactRemoteResp
from osmocom.utils import h2b, b2h
@@ -5990,6 +6053,8 @@ class PysimHandler(BaseHTTPRequestHandler):
spi2_val = int(spi2, 16)
por_in_submit = bool(spi2_val & 0x20)
failure = {}
def _send_gp_apdu(apdu_hex, step_name):
nonlocal cntr, encode_error
try:
@@ -6031,18 +6096,20 @@ class PysimHandler(BaseHTTPRequestHandler):
por_hex = tpdu_b[idx:].hex()
por_src = 'sms-submit'
por = _decode_por(spi1, spi2, kic, kid, cntr, kic_key, kid_key, por_hex)
por_status = 'unknown'
if por and por.get('decoded'):
ps = por['decoded'].get('response_status', '')
por_status = 'por_ok' if ps == '9100' else 'por_error_%s' % ps
sys.stderr.write('RAM-INSTALL: %s PoR[%s] status=%s (%d B, %d SM)\n' % (
step_name, por_src, ps, result['bytes'], result['segments']))
elif last_sw == '9000' and not por_hex:
por_status = 'no_por'
steps.append({'name': step_name, 'por_status': por_status, 'sw': last_sw,
'bytes': result['bytes'], 'segments': result['segments']})
step, step_error = _ram_step_result(
step_name, last_sw, por, por_hex,
result['bytes'], result['segments'])
steps.append(step)
sys.stderr.write('RAM-INSTALL: %s PoR[%s] status=%s remote_sw=%s%s (%d B, %d SM)\n' % (
step_name, por_src, step.get('por_status', '?'),
step.get('por_sw', '-'),
(' error=%s' % step_error) if step_error else '',
result['bytes'], result['segments']))
# Increment counter
cntr = '%010X' % ((int(cntr, 16) + 1) % (2 ** 32))
if step_error:
failure['error'] = '%s: %s' % (step_name, step_error)
return False
return True
finally:
if submit_handler and hasattr(scc, '_tp'):
@@ -6065,7 +6132,8 @@ class PysimHandler(BaseHTTPRequestHandler):
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
if not _send_gp_apdu(gp_apdu, step_name):
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
'error': encode_error or ('%s failed' % step_name),
'error': (encode_error or failure.get('error')
or ('%s failed' % step_name)),
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
'load_block_size': block_size,
'load_block_size_requested': block_size_req,