Decode STATUS FCP, REFRESH mode, and length mismatches
This commit is contained in:
1 parent
0ae75bfdb2
commit
f6ce7dfe3e
3 files changed
+133
-3
No files matched your search
+9
-1
@@ -610,6 +610,9 @@ function cmdRows(cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proactive command fields (FETCH)
|
// Proactive command fields (FETCH)
|
||||||
|
if (cmd.refresh_mode) rows.push(['Refresh mode', cmd.refresh_mode]);
|
||||||
|
if (cmd.file_list) rows.push(['File list', cmd.file_list.join(', ')]);
|
||||||
|
if (cmd.aid) rows.push(['AID', cmd.aid]);
|
||||||
if (cmd.title) rows.push(['Title', cmd.title]);
|
if (cmd.title) rows.push(['Title', cmd.title]);
|
||||||
if (cmd.text !== undefined) rows.push(['Text', cmd.text]);
|
if (cmd.text !== undefined) rows.push(['Text', cmd.text]);
|
||||||
if (cmd.items) {
|
if (cmd.items) {
|
||||||
@@ -900,6 +903,11 @@ function showDetail(msg, entry) {
|
|||||||
? `<tr><td class="label">Responding to</td><td class="name">${decoded.response_to}</td></tr>`
|
? `<tr><td class="label">Responding to</td><td class="name">${decoded.response_to}</td></tr>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const lm = decoded.length_mismatch;
|
||||||
|
const lmNote = lm
|
||||||
|
? ` <span style="color:var(--error)">— ${lm.kind} (${lm.actual}/${lm.expected} bytes)</span>`
|
||||||
|
: '';
|
||||||
|
|
||||||
d.innerHTML = `<div class="detail-header">
|
d.innerHTML = `<div class="detail-header">
|
||||||
<strong>${decoded.cat_command || decoded.ins_name}</strong>
|
<strong>${decoded.cat_command || decoded.ins_name}</strong>
|
||||||
<span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span>
|
<span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span>
|
||||||
@@ -911,7 +919,7 @@ function showDetail(msg, entry) {
|
|||||||
<tr><td class="label">INS</td><td class="name">0x${(decoded.ins_hex || '').toUpperCase()} — ${decoded.ins_name}</td></tr>
|
<tr><td class="label">INS</td><td class="name">0x${(decoded.ins_hex || '').toUpperCase()} — ${decoded.ins_name}</td></tr>
|
||||||
${catRow}${respRow}${trRow}
|
${catRow}${respRow}${trRow}
|
||||||
${p1p2}${p1}${p2}
|
${p1p2}${p1}${p2}
|
||||||
<tr><td class="label">P3</td><td class="value">0x${hx(decoded.p3, 2)} — ${decoded.p3} bytes</td></tr>
|
<tr><td class="label">P3</td><td class="value">0x${hx(decoded.p3, 2)} — ${decoded.p3} bytes${lmNote}</td></tr>
|
||||||
${body}
|
${body}
|
||||||
${cmdRows(decoded.cmd)}
|
${cmdRows(decoded.cmd)}
|
||||||
${responseRows(decoded.response)}
|
${responseRows(decoded.response)}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ def _sw_name(sw1, sw2):
|
|||||||
_SW_PATTERNS = {
|
_SW_PATTERNS = {
|
||||||
(0x63,): lambda s1, s2: ('PIN verification failed, %d retries remaining' % (s2 & 0x0f)
|
(0x63,): lambda s1, s2: ('PIN verification failed, %d retries remaining' % (s2 & 0x0f)
|
||||||
if s2 & 0xc0 == 0xc0 else None),
|
if s2 & 0xc0 == 0xc0 else None),
|
||||||
(0x62, 0x00): lambda s1, s2: 'Response: %d more bytes available' % s2,
|
|
||||||
(0x61,): lambda s1, s2: 'Response: %d bytes available (use GET RESPONSE)' % s2,
|
(0x61,): lambda s1, s2: 'Response: %d bytes available (use GET RESPONSE)' % s2,
|
||||||
|
(0x6c,): lambda s1, s2: 'Wrong length (Le): correct length is 0x%02X (%d bytes)' % (s2, s2),
|
||||||
(0x9e,): lambda s1, s2: 'Normal processing, %d bytes of response' % s2,
|
(0x9e,): lambda s1, s2: 'Normal processing, %d bytes of response' % s2,
|
||||||
(0x9f,): lambda s1, s2: 'Normal processing, %d bytes of response' % s2,
|
(0x9f,): lambda s1, s2: 'Normal processing, %d bytes of response' % s2,
|
||||||
}
|
}
|
||||||
@@ -471,11 +471,13 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xB0: {
|
0xB0: {
|
||||||
'name': 'READ BINARY',
|
'name': 'READ BINARY',
|
||||||
|
'le': True,
|
||||||
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
0xB2: {
|
0xB2: {
|
||||||
'name': 'READ RECORD',
|
'name': 'READ RECORD',
|
||||||
|
'le': True,
|
||||||
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
'p2': {
|
'p2': {
|
||||||
'label': 'Mode',
|
'label': 'Mode',
|
||||||
@@ -492,6 +494,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xB1: {
|
0xB1: {
|
||||||
'name': 'READ RECORD (B1)',
|
'name': 'READ RECORD (B1)',
|
||||||
|
'le': True,
|
||||||
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
'p2': {
|
'p2': {
|
||||||
'label': 'Mode',
|
'label': 'Mode',
|
||||||
@@ -580,6 +583,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0x84: {
|
0x84: {
|
||||||
'name': 'GET CHALLENGE',
|
'name': 'GET CHALLENGE',
|
||||||
|
'le': True,
|
||||||
'p1p2': {'unused': True},
|
'p1p2': {'unused': True},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
@@ -591,6 +595,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xC0: {
|
0xC0: {
|
||||||
'name': 'GET RESPONSE',
|
'name': 'GET RESPONSE',
|
||||||
|
'le': True,
|
||||||
'p1p2': {'unused': True},
|
'p1p2': {'unused': True},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
@@ -601,6 +606,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0x12: {
|
0x12: {
|
||||||
'name': 'FETCH',
|
'name': 'FETCH',
|
||||||
|
'le': True,
|
||||||
'p1p2': {'unused': True},
|
'p1p2': {'unused': True},
|
||||||
'body': None,
|
'body': None,
|
||||||
},
|
},
|
||||||
@@ -628,6 +634,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xF2: {
|
0xF2: {
|
||||||
'name': 'STATUS',
|
'name': 'STATUS',
|
||||||
|
'le': True,
|
||||||
'p1': {0x00: 'No indication', 0x01: 'Current DF', 0x02: 'EF under current DF',
|
'p1': {0x00: 'No indication', 0x01: 'Current DF', 0x02: 'EF under current DF',
|
||||||
0x04: 'DF name', 0x0d: 'Applet status'},
|
0x04: 'DF name', 0x0d: 'Applet status'},
|
||||||
'p2': {0x00: 'No indication'},
|
'p2': {0x00: 'No indication'},
|
||||||
@@ -678,6 +685,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xCB: {
|
0xCB: {
|
||||||
'name': 'RETRIEVE DATA',
|
'name': 'RETRIEVE DATA',
|
||||||
|
'le': True,
|
||||||
'p1': {0x00: 'No indication'},
|
'p1': {0x00: 'No indication'},
|
||||||
'p2': {'label': 'Mode', 'bits': {
|
'p2': {'label': 'Mode', 'bits': {
|
||||||
0x80: 'First block',
|
0x80: 'First block',
|
||||||
@@ -706,6 +714,7 @@ APDU_SPEC = {
|
|||||||
},
|
},
|
||||||
0xCA: {
|
0xCA: {
|
||||||
'name': 'GET DATA',
|
'name': 'GET DATA',
|
||||||
|
'le': True,
|
||||||
'p1p2': {'fmt': 'uint16be', 'label': 'Tag'},
|
'p1p2': {'fmt': 'uint16be', 'label': 'Tag'},
|
||||||
'body': {'label': 'TLV data'},
|
'body': {'label': 'TLV data'},
|
||||||
},
|
},
|
||||||
@@ -772,6 +781,21 @@ CAT_COMMAND_TYPES = {
|
|||||||
0x81: 'END OF PROACTIVE SESSION',
|
0x81: 'END OF PROACTIVE SESSION',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# TS 102 223 §8.6 — REFRESH command qualifier (mode)
|
||||||
|
REFRESH_MODES = {
|
||||||
|
0x00: 'NAA Initialization and Full File Change Notification',
|
||||||
|
0x01: 'File Change Notification',
|
||||||
|
0x02: 'NAA Initialization and File Change Notification',
|
||||||
|
0x03: 'NAA Initialization',
|
||||||
|
0x04: 'UICC Reset',
|
||||||
|
0x05: 'NAA Application Reset',
|
||||||
|
0x06: 'NAA Session Reset',
|
||||||
|
0x07: 'Reserved (Steering of Roaming)',
|
||||||
|
0x08: 'Reserved (Steering of Roaming for I-WLAN)',
|
||||||
|
0x09: 'eUICC Profile State Change',
|
||||||
|
0x0A: 'Application Update',
|
||||||
|
}
|
||||||
|
|
||||||
ENVELOPE_TYPES = {
|
ENVELOPE_TYPES = {
|
||||||
0xD1: 'SMS-PP DOWNLOAD',
|
0xD1: 'SMS-PP DOWNLOAD',
|
||||||
0xD2: 'CELL BROADCAST DOWNLOAD',
|
0xD2: 'CELL BROADCAST DOWNLOAD',
|
||||||
@@ -1978,8 +2002,10 @@ _P_ADDRESS = 0x06
|
|||||||
_P_SMS_TPDU = 0x0B
|
_P_SMS_TPDU = 0x0B
|
||||||
_P_TEXT_STRING = 0x0D
|
_P_TEXT_STRING = 0x0D
|
||||||
_P_ITEM = 0x0F
|
_P_ITEM = 0x0F
|
||||||
|
_P_FILE_LIST = 0x12
|
||||||
_P_RESPONSE_LEN = 0x11
|
_P_RESPONSE_LEN = 0x11
|
||||||
_P_EVENT_LIST = 0x19
|
_P_EVENT_LIST = 0x19
|
||||||
|
_P_AID = 0x2F
|
||||||
|
|
||||||
|
|
||||||
def _decode_proactive(body):
|
def _decode_proactive(body):
|
||||||
@@ -1999,12 +2025,17 @@ def _decode_proactive(body):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
cmd_type = None
|
cmd_type = None
|
||||||
|
qualifier = None
|
||||||
result = {}
|
result = {}
|
||||||
items = []
|
items = []
|
||||||
|
file_list = []
|
||||||
|
aid = None
|
||||||
for tag, _length, value in inner:
|
for tag, _length, value in inner:
|
||||||
base = tag & 0x7F
|
base = tag & 0x7F
|
||||||
if base == _P_CMD_DETAILS and len(value) >= 2:
|
if base == _P_CMD_DETAILS and len(value) >= 2:
|
||||||
cmd_type = value[1]
|
cmd_type = value[1]
|
||||||
|
if len(value) >= 3:
|
||||||
|
qualifier = value[2]
|
||||||
elif base == _P_ALPHA_ID and value:
|
elif base == _P_ALPHA_ID and value:
|
||||||
result['title'] = _decode_annex_a(value)
|
result['title'] = _decode_annex_a(value)
|
||||||
elif base == _P_TEXT_STRING and value:
|
elif base == _P_TEXT_STRING and value:
|
||||||
@@ -2021,10 +2052,20 @@ def _decode_proactive(body):
|
|||||||
result['events'] = [EVENT_TYPES.get(e, f'0x{e:02X}') for e in value]
|
result['events'] = [EVENT_TYPES.get(e, f'0x{e:02X}') for e in value]
|
||||||
elif base == _P_ADDRESS and value:
|
elif base == _P_ADDRESS and value:
|
||||||
result['address'] = _decode_bcd_address(value)
|
result['address'] = _decode_bcd_address(value)
|
||||||
|
elif base == _P_FILE_LIST and value:
|
||||||
|
file_list = [value[i:i + 2].hex().upper() for i in range(0, len(value), 2)]
|
||||||
|
elif base == _P_AID and value:
|
||||||
|
aid = value.hex().upper()
|
||||||
|
|
||||||
if cmd_type is None:
|
if cmd_type is None:
|
||||||
return None
|
return None
|
||||||
result['type'] = CAT_COMMAND_TYPES.get(cmd_type, f'0x{cmd_type:02X}')
|
result['type'] = CAT_COMMAND_TYPES.get(cmd_type, f'0x{cmd_type:02X}')
|
||||||
|
if cmd_type == 0x01 and qualifier is not None: # REFRESH
|
||||||
|
result['refresh_mode'] = REFRESH_MODES.get(qualifier, f'0x{qualifier:02X}')
|
||||||
|
if file_list:
|
||||||
|
result['file_list'] = file_list
|
||||||
|
if aid:
|
||||||
|
result['aid'] = aid
|
||||||
if items:
|
if items:
|
||||||
result['items'] = items
|
result['items'] = items
|
||||||
return result
|
return result
|
||||||
@@ -2688,6 +2729,8 @@ def _build_summary(result):
|
|||||||
if cmd:
|
if cmd:
|
||||||
if cmd.get('context'):
|
if cmd.get('context'):
|
||||||
parts.append(cmd['context'])
|
parts.append(cmd['context'])
|
||||||
|
if cmd.get('refresh_mode'):
|
||||||
|
parts.append(cmd['refresh_mode'])
|
||||||
if cmd.get('title'):
|
if cmd.get('title'):
|
||||||
parts.append(cmd['title'])
|
parts.append(cmd['title'])
|
||||||
items = cmd.get('items')
|
items = cmd.get('items')
|
||||||
@@ -2784,7 +2827,7 @@ def decode_message(raw_data, prev=None):
|
|||||||
extra_total = len(remaining) - p3
|
extra_total = len(remaining) - p3
|
||||||
sw_bytes = None
|
sw_bytes = None
|
||||||
|
|
||||||
if extra_total >= 2:
|
if len(remaining) >= 2 and (extra_total >= 2 or extra_total < 0):
|
||||||
sw_candidate = remaining[-2:]
|
sw_candidate = remaining[-2:]
|
||||||
if sw_candidate[0] in (0x60, 0x61, 0x62, 0x63, 0x64, 0x65,
|
if sw_candidate[0] in (0x60, 0x61, 0x62, 0x63, 0x64, 0x65,
|
||||||
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b,
|
0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b,
|
||||||
@@ -2796,6 +2839,12 @@ def decode_message(raw_data, prev=None):
|
|||||||
|
|
||||||
cmd_body_len = len(remaining) - 2 if sw_bytes else len(remaining)
|
cmd_body_len = len(remaining) - 2 if sw_bytes else len(remaining)
|
||||||
|
|
||||||
|
# Length sanity: P3 (Lc/Le) vs actual data captured.
|
||||||
|
if cmd_body_len > p3:
|
||||||
|
result['length_mismatch'] = {'kind': 'excessive', 'expected': p3, 'actual': cmd_body_len}
|
||||||
|
elif cmd_body_len < p3 and not (spec and spec.get('le')):
|
||||||
|
result['length_mismatch'] = {'kind': 'truncated', 'expected': p3, 'actual': cmd_body_len}
|
||||||
|
|
||||||
if cmd_body_len > 0:
|
if cmd_body_len > 0:
|
||||||
body = remaining[:cmd_body_len]
|
body = remaining[:cmd_body_len]
|
||||||
result['body'] = {'hex': body.hex(), 'size': cmd_body_len}
|
result['body'] = {'hex': body.hex(), 'size': cmd_body_len}
|
||||||
@@ -2826,6 +2875,10 @@ def decode_message(raw_data, prev=None):
|
|||||||
result['cmd'] = _decode_envelope(body)
|
result['cmd'] = _decode_envelope(body)
|
||||||
elif ins == 0x12: # FETCH → proactive command body
|
elif ins == 0x12: # FETCH → proactive command body
|
||||||
result['cmd'] = _decode_proactive(body)
|
result['cmd'] = _decode_proactive(body)
|
||||||
|
elif ins == 0xF2: # STATUS → response data is the FCP of the current DF/EF
|
||||||
|
fcp = _decode_fcp(body)
|
||||||
|
if fcp:
|
||||||
|
result['response'] = fcp
|
||||||
|
|
||||||
# File data decode for READ/UPDATE using the current selection.
|
# File data decode for READ/UPDATE using the current selection.
|
||||||
if ins in (0xB0, 0xB2, 0xD6, 0xDC) and prev and prev.get('sel'):
|
if ins in (0xB0, 0xB2, 0xD6, 0xDC) and prev and prev.get('sel'):
|
||||||
|
|||||||
@@ -1180,5 +1180,74 @@ class TestP1P2(unittest.TestCase):
|
|||||||
self.assertEqual(r['p1']['name'], 'Path from current DF')
|
self.assertEqual(r['p1']['name'], 'Path from current DF')
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwWrongLength(unittest.TestCase):
|
||||||
|
def test_6c(self):
|
||||||
|
from simtrace2_pysniff.server.decode import decode_sw
|
||||||
|
self.assertEqual(decode_sw(bytes.fromhex('6c38'))['name'],
|
||||||
|
'Wrong length (Le): correct length is 0x38 (56 bytes)')
|
||||||
|
|
||||||
|
def test_62_unknown_is_unnamed(self):
|
||||||
|
from simtrace2_pysniff.server.decode import decode_sw
|
||||||
|
self.assertIsNone(decode_sw(bytes.fromhex('6205'))['name'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusFcp(unittest.TestCase):
|
||||||
|
def test_status_body_is_fcp(self):
|
||||||
|
raw = bytes.fromhex(
|
||||||
|
'80f2000038'
|
||||||
|
'62368202782183027ff08410a0000000871002ff45ff0189020111008a01058b032f0608c60c90016083010183018183010a81040000039f'
|
||||||
|
'9000')
|
||||||
|
r = decode_message(raw)
|
||||||
|
self.assertEqual(r['ins_name'], 'STATUS')
|
||||||
|
self.assertEqual(r['sw']['sw1'], '90')
|
||||||
|
resp = r['response']
|
||||||
|
self.assertEqual(resp['template'], 'FCP')
|
||||||
|
self.assertEqual(resp['file_id'], '7FF0')
|
||||||
|
self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100')
|
||||||
|
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
|
||||||
|
self.assertEqual(resp['life_cycle'], 'operational state (activated)')
|
||||||
|
|
||||||
|
|
||||||
|
class TestLengthMismatch(unittest.TestCase):
|
||||||
|
def test_lc_truncated(self):
|
||||||
|
# ENVELOPE with Lc=21, body missing (only SW captured).
|
||||||
|
r = decode_message(bytes.fromhex('80c20000159000'))
|
||||||
|
self.assertEqual(r['sw']['sw1'], '90')
|
||||||
|
self.assertEqual(r['sw']['sw2'], '00')
|
||||||
|
self.assertNotIn('body', r)
|
||||||
|
self.assertEqual(r['length_mismatch'],
|
||||||
|
{'kind': 'truncated', 'expected': 21, 'actual': 0})
|
||||||
|
|
||||||
|
def test_lc_excessive(self):
|
||||||
|
# ENVELOPE with Lc=3 but 4 bytes of body + SW.
|
||||||
|
r = decode_message(bytes.fromhex('80c2000003d10102039000'))
|
||||||
|
self.assertEqual(r['length_mismatch'],
|
||||||
|
{'kind': 'excessive', 'expected': 3, 'actual': 4})
|
||||||
|
|
||||||
|
def test_select_well_formed(self):
|
||||||
|
r = decode_message(bytes.fromhex('a0a40000023f009000'))
|
||||||
|
self.assertNotIn('length_mismatch', r)
|
||||||
|
|
||||||
|
def test_le_short_response_not_flagged(self):
|
||||||
|
# READ BINARY (Le) returning an error SW with no data is not "truncated".
|
||||||
|
r = decode_message(bytes.fromhex('00b08f00066a82'))
|
||||||
|
self.assertEqual(r['sw']['sw1'], '6a')
|
||||||
|
self.assertNotIn('length_mismatch', r)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefresh(unittest.TestCase):
|
||||||
|
def test_uicc_reset(self):
|
||||||
|
r = decode_message(bytes.fromhex('801200000bd0098103010104820281829000'))
|
||||||
|
self.assertEqual(r['cat_command'], 'REFRESH')
|
||||||
|
self.assertEqual(r['cmd']['refresh_mode'], 'UICC Reset')
|
||||||
|
self.assertNotIn('file_list', r['cmd'])
|
||||||
|
self.assertNotIn('aid', r['cmd'])
|
||||||
|
|
||||||
|
def test_file_change_notification(self):
|
||||||
|
r = decode_message(bytes.fromhex('8012000011d00f81030101018202818292046f076f209000'))
|
||||||
|
self.assertEqual(r['cmd']['refresh_mode'], 'File Change Notification')
|
||||||
|
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user