fix: decode the Response Scripting template and the compact listings (v3.6.5)

Explore still missed the F0414C46416101 package on a card whose responses
wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template
(`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a
compact response, so the frontend got `last_status_word` 81d0/7680 and data
starting `80 01 01 23 ...` instead of the listing.

- server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00
  indefinite) extracts the executed-command count and the last R-APDU's SW
  and data; `_decode_por` exposes it as `response_type: scripting` in the
  same `decoded` shape as compact, so the RAM explore paging and the RAM
  install `por_sw` see the real 9000/6310.
- frontend: the compact listing walk is deterministic on the AID length
  (`len AID life ver`; P1=10 adds `module_count (len module_AID)*`).
  `_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte
  A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans
  for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page
  parsed to 1 entry with no F0414C46416101).
- tests: exact trace fixtures - the ELF page lists F0414C46416101 (11
  entries), the P1=10 page attaches its F0414C4641610101 module, the app page
  keeps the 16-byte A113 AIDs; Python covers the scripting template
  (definite/indefinite) against the live `AB 12` ISD and listing vectors.

610 frontend / 495 Python green; version 3.6.5; sw simple-v278.
This commit is contained in:
2026-09-28 00:30:49 +03:00
parent 9e85f522f6
commit 40f20d539e
7 changed files with 180 additions and 81 deletions
+56 -46
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.4'
VERSION = '3.6.5'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1171,6 +1171,46 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
return step, error
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).
Returns (count, sw, rapdu_data_hex) from the last R-APDU, or None when the
data is not a scripting template."""
if not data:
return None
if data[0] == 0xAF:
if len(data) < 4 or data[1] != 0x80 or data[-2:] != b'\x00\x00':
return None
body = data[2:-2]
elif data[0] == 0xAB:
ln, voff = _ber_len_at(data, 1)
if ln <= 0 or voff + ln > len(data):
return None
body = data[voff:voff + ln]
else:
return None
count = None
last = None
off = 0
while off < len(body) - 1:
tag = body[off]
ln, voff = _ber_len_at(body, off + 1)
if ln < 0 or voff + ln > len(body):
break
val = body[voff:voff + ln]
if tag == 0x80 and val:
count = int.from_bytes(val, 'big')
elif tag == 0x23 and len(val) >= 2:
last = (val[-2:].hex().upper(), val[:-2].hex().upper())
off = voff + ln
if last is None:
return None
return (count, last[0], last[1])
def _sms_submit_por(submit_handler):
"""Response packet carried by an actual-response SMS-SUBMIT, in the
DELIVER-style form `_decode_por` expects.
@@ -1215,53 +1255,23 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon
'raw': response_hex,
}
# Try ExpandedRemoteResponse first (TS 102 226 §5.2.2)
# TS 102 226 5.2.2 Response Scripting template (AB/AF): cards wrap the
# R-APDU(s) of the executed remote command(s) this way instead of the
# plain compact response. The R-APDU's own SW and data are the useful
# result (a bare CompactRemoteResp parse would read `AB` as the command
# count and produce garbage).
if res.response_status == 'por_ok' and len(res['secured_data']):
expanded_response_data = ''
try:
from construct import Struct, Int8ub, Bytes, GreedyBytes, Optional, Array, this
ExpandedRemoteResponse = Struct(
'response_count'/Int8ub,
'responses'/Array(this.response_count, Struct(
'command_number'/Int8ub,
'status_word'/Bytes(2),
'response_data'/GreedyBytes,
'error_details'/Optional(Struct(
'error_code'/Int8ub,
'error_info'/GreedyBytes
)),
'chaining_context'/Optional(Struct(
'script_id'/Bytes(4),
'is_first'/Int8ub,
'is_last'/Int8ub,
))
))
)
expanded = ExpandedRemoteResponse.parse(res['secured_data'])
out['response_type'] = 'expanded'
out['response_count'] = expanded.response_count
out['responses'] = []
for resp in expanded.responses:
response_data = {
'command_number': resp.command_number,
'status_word': resp.status_word.hex().upper(),
'response_data': b2h(resp.response_data).upper() if resp.response_data else '',
}
if resp.error_details:
response_data['error_code'] = resp.error_details.error_code
response_data['error_info'] = b2h(resp.error_details.error_info).upper()
if resp.chaining_context:
response_data['script_id'] = resp.chaining_context.script_id.hex().upper()
response_data['is_first'] = resp.chaining_context.is_first == 0x01
response_data['is_last'] = resp.chaining_context.is_last == 0x01
out['responses'].append(response_data)
if expanded.response_count > 0 and expanded.responses[0].response_data:
expanded_response_data = b2h(expanded.responses[0].response_data).upper()
except Exception:
pass
if dec is not None:
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,
}
elif dec is not None:
out['response_type'] = 'compact'
# Use compact parser's last_response_data; expanded parser gives wrong results for compact format
out['decoded'] = {
'number_of_commands': dec.number_of_commands,
'last_status_word': str(dec.last_status_word),