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