Add CAT (TS 102 223) command decoding + fix AUTHENTICATE naming
- CAT_COMMAND_TYPES: complete Type of Command table from TS 102 223 §9.4 - ENVELOPE_TYPES: D1-D7 envelope tags - parse_tlv(): minimal BER-TLV parser - decode_cat(): FETCH → proactive command name (via D0→81 type byte); ENVELOPE → envelope type name - AUTHENTICATE (0x88/0x89) unified to 'AUTHENTICATE' (no suffix) - decoded dict gains 'cat_command' field for FETCH/ENVELOPE - tests/test_decode.py: 14 new tests (TLV, naming, CAT, tables)
This commit is contained in:
1 parent
41b5c0218e
commit
11d0f29b6a
3 files changed
+2752
-2
No files matched your search
+2540
File diff suppressed because it is too large.
Load diff
@@ -267,12 +267,12 @@ APDU_SPEC = {
|
|||||||
'body': {'label': 'PUK + new PIN'},
|
'body': {'label': 'PUK + new PIN'},
|
||||||
},
|
},
|
||||||
0x88: {
|
0x88: {
|
||||||
'name': 'AUTHENTICATE (88)',
|
'name': 'AUTHENTICATE',
|
||||||
'p1': {0x00: 'Run GSM algorithm', 0x80: 'Run 3G algo (resynchronisation)'},
|
'p1': {0x00: 'Run GSM algorithm', 0x80: 'Run 3G algo (resynchronisation)'},
|
||||||
'body': {'label': 'Challenge/session key'},
|
'body': {'label': 'Challenge/session key'},
|
||||||
},
|
},
|
||||||
0x89: {
|
0x89: {
|
||||||
'name': 'AUTHENTICATE (89)',
|
'name': 'AUTHENTICATE',
|
||||||
'body': {'label': 'Response/resynchronisation data'},
|
'body': {'label': 'Response/resynchronisation data'},
|
||||||
},
|
},
|
||||||
0x84: {
|
0x84: {
|
||||||
@@ -390,6 +390,116 @@ APDU_SPEC = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────── CAT (TS 102 223) command types ────────────────────
|
||||||
|
|
||||||
|
CAT_COMMAND_TYPES = {
|
||||||
|
0x01: 'REFRESH',
|
||||||
|
0x02: 'MORE TIME',
|
||||||
|
0x03: 'POLL INTERVAL',
|
||||||
|
0x04: 'POLLING OFF',
|
||||||
|
0x05: 'SET UP EVENT LIST',
|
||||||
|
0x10: 'SET UP CALL',
|
||||||
|
0x11: 'SEND SS',
|
||||||
|
0x12: 'SEND USSD',
|
||||||
|
0x13: 'SEND SHORT MESSAGE',
|
||||||
|
0x14: 'SEND DTMF',
|
||||||
|
0x15: 'LAUNCH BROWSER',
|
||||||
|
0x16: 'GEOGRAPHICAL LOCATION REQUEST',
|
||||||
|
0x20: 'PLAY TONE',
|
||||||
|
0x21: 'DISPLAY TEXT',
|
||||||
|
0x22: 'GET INKEY',
|
||||||
|
0x23: 'GET INPUT',
|
||||||
|
0x24: 'SELECT ITEM',
|
||||||
|
0x25: 'SET UP MENU',
|
||||||
|
0x26: 'PROVIDE LOCAL INFORMATION',
|
||||||
|
0x27: 'TIMER MANAGEMENT',
|
||||||
|
0x28: 'SET UP IDLE MODE TEXT',
|
||||||
|
0x30: 'PERFORM CARD APDU',
|
||||||
|
0x31: 'POWER ON CARD',
|
||||||
|
0x32: 'POWER OFF CARD',
|
||||||
|
0x33: 'GET READER STATUS',
|
||||||
|
0x34: 'RUN AT COMMAND',
|
||||||
|
0x35: 'LANGUAGE NOTIFICATION',
|
||||||
|
0x40: 'OPEN CHANNEL',
|
||||||
|
0x41: 'CLOSE CHANNEL',
|
||||||
|
0x42: 'RECEIVE DATA',
|
||||||
|
0x43: 'SEND DATA',
|
||||||
|
0x44: 'GET CHANNEL STATUS',
|
||||||
|
0x45: 'SERVICE SEARCH',
|
||||||
|
0x46: 'GET SERVICE INFORMATION',
|
||||||
|
0x47: 'DECLARE SERVICE',
|
||||||
|
0x50: 'SET FRAMES',
|
||||||
|
0x51: 'GET FRAMES STATUS',
|
||||||
|
0x60: 'RETRIEVE MULTIMEDIA MESSAGE',
|
||||||
|
0x61: 'SUBMIT MULTIMEDIA MESSAGE',
|
||||||
|
0x62: 'DISPLAY MULTIMEDIA MESSAGE',
|
||||||
|
0x70: 'ACTIVATE',
|
||||||
|
0x71: 'CONTACTLESS STATE CHANGED',
|
||||||
|
0x72: 'COMMAND CONTAINER',
|
||||||
|
0x73: 'ENCAPSULATED SESSION CONTROL',
|
||||||
|
0x81: 'END OF PROACTIVE SESSION',
|
||||||
|
}
|
||||||
|
|
||||||
|
ENVELOPE_TYPES = {
|
||||||
|
0xD1: 'SMS-PP DOWNLOAD',
|
||||||
|
0xD2: 'CELL BROADCAST DOWNLOAD',
|
||||||
|
0xD3: 'MENU SELECTION',
|
||||||
|
0xD4: 'CALL CONTROL',
|
||||||
|
0xD5: 'MO SHORT MESSAGE CONTROL',
|
||||||
|
0xD6: 'EVENT DOWNLOAD',
|
||||||
|
0xD7: 'TIMER EXPIRATION',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tlv(data):
|
||||||
|
"""Parse a BER-TLV structure into a list of (tag, length, value) tuples.
|
||||||
|
|
||||||
|
Handles single-byte tags and single-byte lengths (< 128), which cover
|
||||||
|
the CAT proactive command and ENVELOPE structures we decode.
|
||||||
|
"""
|
||||||
|
tlvs = []
|
||||||
|
i = 0
|
||||||
|
n = len(data)
|
||||||
|
while i < n:
|
||||||
|
if i + 2 > n:
|
||||||
|
break
|
||||||
|
tag = data[i]
|
||||||
|
i += 1
|
||||||
|
length = data[i]
|
||||||
|
i += 1
|
||||||
|
if length & 0x80:
|
||||||
|
# long form length — not needed for top-level CAT decode
|
||||||
|
break
|
||||||
|
if i + length > n:
|
||||||
|
break
|
||||||
|
value = data[i:i + length]
|
||||||
|
i += length
|
||||||
|
tlvs.append((tag, length, value))
|
||||||
|
return tlvs
|
||||||
|
|
||||||
|
|
||||||
|
def decode_cat(ins, body):
|
||||||
|
"""Decode a CAT (TS 102 223) payload, returning the command/event name.
|
||||||
|
|
||||||
|
For FETCH (0x12) the body is a proactive UICC command (D0 TLV) whose
|
||||||
|
Command Details (tag 81) second byte is the Type of Command.
|
||||||
|
For ENVELOPE (0xC2) the first TLV tag is the envelope type.
|
||||||
|
Returns a string name or None.
|
||||||
|
"""
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
if ins == 0x12: # FETCH → proactive command
|
||||||
|
for tag, _length, value in parse_tlv(body):
|
||||||
|
if tag != 0xD0:
|
||||||
|
continue
|
||||||
|
for t2, _l2, v2 in parse_tlv(value):
|
||||||
|
if t2 == 0x81 and len(v2) >= 2:
|
||||||
|
return CAT_COMMAND_TYPES.get(v2[1])
|
||||||
|
elif ins == 0xC2: # ENVELOPE → envelope type
|
||||||
|
return ENVELOPE_TYPES.get(body[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────── Decode entry point ────────────────────
|
# ──────────────────── Decode entry point ────────────────────
|
||||||
|
|
||||||
def decode_message(raw_data):
|
def decode_message(raw_data):
|
||||||
@@ -458,6 +568,10 @@ def decode_message(raw_data):
|
|||||||
if h in fids:
|
if h in fids:
|
||||||
result['body']['note'] = fids[h]
|
result['body']['note'] = fids[h]
|
||||||
|
|
||||||
|
cat_command = decode_cat(ins, body)
|
||||||
|
if cat_command:
|
||||||
|
result['cat_command'] = cat_command
|
||||||
|
|
||||||
# SW
|
# SW
|
||||||
if sw_bytes:
|
if sw_bytes:
|
||||||
result['sw'] = decode_sw(sw_bytes)
|
result['sw'] = decode_sw(sw_bytes)
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Tests for server APDU/CAT decoding."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from simtrace2_pysniff.server.decode import (
|
||||||
|
decode_message,
|
||||||
|
decode_sniff_msg,
|
||||||
|
CAT_COMMAND_TYPES,
|
||||||
|
ENVELOPE_TYPES,
|
||||||
|
parse_tlv,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseTlv(unittest.TestCase):
|
||||||
|
def test_simple(self):
|
||||||
|
data = bytes.fromhex('8103010500')
|
||||||
|
self.assertEqual(parse_tlv(data), [(0x81, 3, bytes.fromhex('010500'))])
|
||||||
|
|
||||||
|
def test_multiple(self):
|
||||||
|
data = bytes.fromhex('810301050082028182')
|
||||||
|
self.assertEqual(parse_tlv(data), [
|
||||||
|
(0x81, 3, bytes.fromhex('010500')),
|
||||||
|
(0x82, 2, bytes.fromhex('8182')),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
self.assertEqual(parse_tlv(b''), [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthenticateNaming(unittest.TestCase):
|
||||||
|
def test_ins_88(self):
|
||||||
|
r = decode_message(bytes.fromhex('0088008122' + '00' * 34 + '6135'))
|
||||||
|
self.assertEqual(r['ins_name'], 'AUTHENTICATE')
|
||||||
|
|
||||||
|
def test_ins_89(self):
|
||||||
|
r = decode_message(bytes.fromhex('0089008122' + '00' * 34 + '6135'))
|
||||||
|
self.assertEqual(r['ins_name'], 'AUTHENTICATE')
|
||||||
|
|
||||||
|
|
||||||
|
class TestCatDecoding(unittest.TestCase):
|
||||||
|
def test_fetch_setup_menu(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'8012000030D02E810301250082028182050B416C6661204D6F62696C65'
|
||||||
|
'8F16808112089DB0C1C2C0BEB9BAB82F53657474696E67739000'))
|
||||||
|
self.assertEqual(r['ins_name'], 'FETCH')
|
||||||
|
self.assertEqual(r['cat_command'], 'SET UP MENU')
|
||||||
|
|
||||||
|
def test_fetch_setup_event_list(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'801200000FD00D810301050082028182990203129000'))
|
||||||
|
self.assertEqual(r['cat_command'], 'SET UP EVENT LIST')
|
||||||
|
|
||||||
|
def test_fetch_poll_interval(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'801200000FD00D8103010300820281820402011E9000'))
|
||||||
|
self.assertEqual(r['cat_command'], 'POLL INTERVAL')
|
||||||
|
|
||||||
|
def test_fetch_provide_local_info(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'801200000BD0098103012600820281829000'))
|
||||||
|
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
||||||
|
|
||||||
|
def test_envelope_event_download(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'80C200000CD60A190103020282811B01029000'))
|
||||||
|
self.assertEqual(r['ins_name'], 'ENVELOPE')
|
||||||
|
self.assertEqual(r['cat_command'], 'EVENT DOWNLOAD')
|
||||||
|
|
||||||
|
def test_tr_no_cat(self):
|
||||||
|
r = decode_message(bytes.fromhex(
|
||||||
|
'801400000C8103010500020282810301009130'))
|
||||||
|
self.assertEqual(r['ins_name'], 'TERMINAL RESPONSE')
|
||||||
|
self.assertNotIn('cat_command', r)
|
||||||
|
|
||||||
|
def test_select_no_cat(self):
|
||||||
|
r = decode_message(bytes.fromhex('a0a40000023f009000'))
|
||||||
|
self.assertEqual(r['ins_name'], 'SELECT')
|
||||||
|
self.assertNotIn('cat_command', r)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandTypeTables(unittest.TestCase):
|
||||||
|
def test_known_values(self):
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x03], 'POLL INTERVAL')
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x05], 'SET UP EVENT LIST')
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x25], 'SET UP MENU')
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x21], 'DISPLAY TEXT')
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x24], 'SELECT ITEM')
|
||||||
|
self.assertEqual(CAT_COMMAND_TYPES[0x26], 'PROVIDE LOCAL INFORMATION')
|
||||||
|
|
||||||
|
def test_envelope_known_values(self):
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xD3], 'MENU SELECTION')
|
||||||
|
self.assertEqual(ENVELOPE_TYPES[0xD6], 'EVENT DOWNLOAD')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user