Extended logical channels: CLA 0-19, ATR card capabilities, MANAGE CHANNEL (v2.6.0)
- CLA decode (TS 102 221 §10.1.1/§10.3): standard channels 0-3 from b2-b1
and the extended channels 4-19 from b4-b1. The old further-format rule
read only b2-b1 (channel 8 read as 6, 19 as 7) and the ETSI '11x0 xxxx'
coding (0xC0-0xEF) was not handled at all; the class ranges now follow the
specs (0x80-0x8F UICC, 0xA0-0xAF SIM, 0xC0-0xEF ETSI extended, 0x20-0x3F
reserved, 0xFF invalid) and SM/chain are decoded per family — the SM
extraction and the chaining bit (b5, not b6) were off by one bit
- per-channel state: database._decode_rows/_replay_context use the shared
cla_channel() helper — before, CLA & 0x03 with no +4, so a channel-5
SELECT clobbered channel 1's selection and a channel-8 one channel 0's
(the local traces already open channels: 29 MANAGE CHANNEL opens, 1-2)
- COMPRION T=0 import: the synthesized GET RESPONSE keeps the channel —
a plain CLA for 0-3 (as real terminals do) and 01x0 for 4-19
- ATR historical bytes: the UICC's COMPACT-TLV objects (TS 102 221 §6.3.1)
with card service data, the card capabilities (ISO 7816-4 §8.1.1.2.7
Table 88: assignment methods and max logical channels — the workspace
cards announce 4 and "8 or more") and the status indicator ('00' trailing
bytes / the '8Y' TLV); a truncated ATR no longer raises
- MANAGE CHANNEL success replies decode the assigned channel byte
- capture/import: a 'tpdu' packet that is not APDU-shaped (shorter than the
header, CLA FF, ISO-reserved class) is skipped and counted, so a
character-level or mis-sampled stream no longer pollutes the decode
(the local DB's outlier sessions are exactly that)
- tests: CLA table vectors, per-channel state isolation on channels 8/12,
COMPRION GR channels, filetrack extended channels, ATR compact-TLV and
card-capability cases, the capture skip counter, the PWA rows; 527 Python
+ 47 Node tests
- README/README_RUS document the extended channels, card capabilities and
the skip counter; PWA_VERSION/pyproject/version.py 2.6.0, SW v72
This commit is contained in:
1 parent
cbeeed3608
commit
64433f9739
18 files changed
+683
-88
No files matched your search
+220
-8
@@ -950,11 +950,101 @@ class TestAtrPps(unittest.TestCase):
|
||||
|
||||
def test_atr_historical_life_cycle(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
# T0 = 0x03 (3 historical bytes); historical = 80 00 05 (life cycle: operational)
|
||||
r = _decode_atr(bytes.fromhex('3B03800005'))
|
||||
# T0 = 0x03 (3 historical bytes). Category '80' (the value a UICC
|
||||
# always uses, TS 102 221 §6.3.1) carries COMPACT-TLV objects; the
|
||||
# trailing '81' object is a status indicator holding the LCS
|
||||
# (ISO 7816-4 §8.1.1.3).
|
||||
r = _decode_atr(bytes.fromhex('3B03808105'))
|
||||
self.assertEqual(r['historical_len'], 3)
|
||||
self.assertEqual(r['historical']['category'], 'status information (life cycle)')
|
||||
self.assertEqual(r['historical']['life_cycle'], 'operational state (activated)')
|
||||
self.assertEqual(r['historical']['category'], 'COMPACT-TLV objects')
|
||||
self.assertEqual(r['historical']['life_cycle'],
|
||||
'operational state (activated)')
|
||||
self.assertEqual(r['historical']['objects'], [])
|
||||
|
||||
def test_atr_historical_status_bytes(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
# '82' = SW1-SW2, '83' = LCS + SW1-SW2 (ISO 7816-4 §8.1.1.3).
|
||||
r = _decode_atr(bytes.fromhex('3B0480829000'))
|
||||
self.assertEqual(r['historical']['sw'], '9000')
|
||||
r = _decode_atr(bytes.fromhex('3B058083059000'))
|
||||
self.assertEqual(r['historical']['life_cycle'],
|
||||
'operational state (activated)')
|
||||
self.assertEqual(r['historical']['sw'], '9000')
|
||||
|
||||
def test_atr_historical_status_category_00(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
# Category '00': the last three bytes are the status indicator, the
|
||||
# bytes before it optional COMPACT-TLVs.
|
||||
r = _decode_atr(bytes.fromhex('3B0400059000'))
|
||||
self.assertEqual(r['historical']['category'],
|
||||
'COMPACT-TLV objects + status indicator')
|
||||
self.assertEqual(r['historical']['life_cycle'],
|
||||
'operational state (activated)')
|
||||
self.assertEqual(r['historical']['sw'], '9000')
|
||||
|
||||
def test_atr_card_capabilities(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
# A real workspace-card ATR's historical bytes: '31 E0' card service
|
||||
# data, '73 FE 21 1B' card capabilities (3 tables: selection methods
|
||||
# FE, data coding 21, 1B = card-assigned channels, 4 maximum) and
|
||||
# '66 ...' pre-issuing data. TS 102 221 §6.3.1 pins the first two
|
||||
# objects.
|
||||
r = _decode_atr(bytes.fromhex('3B0E8031E073FE211B66D0022A761300'))
|
||||
hist = r['historical']
|
||||
self.assertEqual(hist['category'], 'COMPACT-TLV objects')
|
||||
self.assertEqual([o['tag'] for o in hist['objects']], ['3', '7', '6'])
|
||||
cs = hist['objects'][0]['card_service']
|
||||
self.assertEqual(cs['selection'], ['full DF name', 'partial DF name'])
|
||||
self.assertTrue(cs['ber_tlv_in_dir'])
|
||||
self.assertFalse(cs['ber_tlv_in_atr'])
|
||||
self.assertTrue(cs['card_with_mf'])
|
||||
caps = hist['card_capabilities']
|
||||
self.assertEqual(caps['tables'], 3)
|
||||
self.assertEqual(caps['max_channels'], 4)
|
||||
# b5/b4 declare the assignment methods the card supports (here both).
|
||||
self.assertEqual(caps['channel_assignment'],
|
||||
['by the card', 'by the interface device'])
|
||||
self.assertFalse(caps['command_chaining'])
|
||||
self.assertFalse(caps['extended_lc_le'])
|
||||
self.assertIn('full DF name', caps['selection_methods'])
|
||||
self.assertEqual(caps['data_coding']['write'], 'proprietary')
|
||||
|
||||
def test_atr_card_capabilities_extended(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
# '73 FE 21 57': y=z=t=1 → "eight or more" logical channels
|
||||
# (ISO 7816-4 Table 88) — the extended-channel capable cards.
|
||||
r = _decode_atr(bytes.fromhex('3B0E8031E073FE215766D0022A761300'))
|
||||
caps = r['historical']['card_capabilities']
|
||||
self.assertEqual(caps['max_channels'], '8 or more')
|
||||
self.assertEqual(caps['channel_assignment'], ['by the card'])
|
||||
self.assertTrue(caps['extended_lc_le'])
|
||||
|
||||
def test_atr_historical_reserved_and_proprietary(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
r = _decode_atr(bytes.fromhex('3B018F'))
|
||||
self.assertEqual(r['historical']['category'],
|
||||
'reserved (ISO 7816-4)')
|
||||
r = _decode_atr(bytes.fromhex('3B01A0'))
|
||||
self.assertEqual(r['historical']['category'], 'proprietary')
|
||||
r = _decode_atr(bytes.fromhex('3B021001'))
|
||||
self.assertEqual(r['historical']['category'], 'DIR data reference')
|
||||
self.assertEqual(r['historical']['dir_reference'], '01')
|
||||
|
||||
def test_manage_channel_response(self):
|
||||
# MANAGE CHANNEL open: the card returns the assigned channel as the
|
||||
# response data byte (ISO 7816-4 §7.1.5); a close carries none.
|
||||
from simtrace_analyser.decode import decode_message
|
||||
r = decode_message(bytes.fromhex('0070000001019110'))
|
||||
self.assertEqual(r['p1']['name'], 'Open channel')
|
||||
self.assertEqual(r['response'], {'channel': 1})
|
||||
r = decode_message(bytes.fromhex('0070000001029000'))
|
||||
self.assertEqual(r['response'], {'channel': 2})
|
||||
r = decode_message(bytes.fromhex('00708002009000'))
|
||||
self.assertEqual(r['p1']['name'], 'Close channel')
|
||||
self.assertNotIn('response', r)
|
||||
# The extended channels are assigned the same way (1-19).
|
||||
r = decode_message(bytes.fromhex('0070000001139000'))
|
||||
self.assertEqual(r['response'], {'channel': 0x13})
|
||||
|
||||
def test_atr_t15_is_pps_capability(self):
|
||||
from simtrace_analyser.decode import _decode_atr
|
||||
@@ -1659,6 +1749,49 @@ class TestSelectionTracking(unittest.TestCase):
|
||||
# channel 0 still selects EF_MSISDN, not channel 1's EF_IMPI.
|
||||
self.assertEqual(up['decoded']['file']['ef'], 'EF_MSISDN')
|
||||
|
||||
def test_extended_channel_selection_isolated(self):
|
||||
# CLA 0x44 = further-interindustry coding, extended channel 8
|
||||
# (TS 102 221 §10.3: b4-b1 + 4); 0x48 = channel 12. Each channel
|
||||
# keeps its own selection — before the channel fix the state was
|
||||
# keyed by CLA & 0x03, so these all clobbered channel 0.
|
||||
import tempfile
|
||||
from simtrace_analyser.database import Database
|
||||
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
|
||||
db = Database(tmp.name)
|
||||
sid = db.create_session('capture')
|
||||
db.insert_message(sid, 0.0, 'tpdu',
|
||||
bytes.fromhex('00a40804047fff6f409000')) # ch0: MSISDN
|
||||
db.insert_message(sid, 0.1, 'tpdu',
|
||||
bytes.fromhex('44a40804047fff6f029000')) # ch8: OCST
|
||||
db.insert_message(sid, 0.2, 'tpdu',
|
||||
bytes.fromhex('48a40804047fff6fb79000')) # ch12: ECC
|
||||
read = (bytes.fromhex('b2010427') +
|
||||
bytes.fromhex('8109089cbeb920bdbebcb5c0ffffffffffffffffffffffffff07919752738981f7ffffffffffff') +
|
||||
bytes.fromhex('9000'))
|
||||
db.insert_message(sid, 0.3, 'tpdu', bytes([0x44]) + read)
|
||||
db.insert_message(sid, 0.4, 'tpdu', bytes([0x00]) + read)
|
||||
db.insert_message(sid, 0.5, 'tpdu', bytes([0x48]) + read)
|
||||
msgs = db.get_messages(sid)
|
||||
self.assertEqual(msgs[3]['decoded']['file']['ef'], 'EF_OCST')
|
||||
self.assertEqual(msgs[4]['decoded']['file']['ef'], 'EF_MSISDN')
|
||||
self.assertEqual(msgs[5]['decoded']['file']['ef'], 'EF_ECC')
|
||||
self.assertEqual(msgs[3]['decoded']['cla']['channel'], 8)
|
||||
self.assertEqual(msgs[5]['decoded']['cla']['channel'], 12)
|
||||
|
||||
def test_manage_channel_opens_use_their_channel(self):
|
||||
# The MANAGE CHANNEL open/close exchanges themselves run on the
|
||||
# basic channel; a later extended-channel command must not disturb it.
|
||||
import tempfile
|
||||
from simtrace_analyser.database import Database
|
||||
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
|
||||
db = Database(tmp.name)
|
||||
sid = db.create_session('capture')
|
||||
db.insert_message(sid, 0.0, 'tpdu', bytes.fromhex('0070000001059000'))
|
||||
db.insert_message(sid, 0.1, 'tpdu', bytes.fromhex('44a40804047fff6f079000'))
|
||||
msgs = db.get_messages(sid)
|
||||
self.assertEqual(msgs[0]['decoded']['response'], {'channel': 5})
|
||||
self.assertEqual(msgs[1]['decoded']['cla']['channel'], 8)
|
||||
|
||||
|
||||
class TestSfiResolution(unittest.TestCase):
|
||||
def test_sfi_resolved(self):
|
||||
@@ -2721,28 +2854,107 @@ class TestClaDecode(unittest.TestCase):
|
||||
self.assertEqual(r['secure_messaging'], 'SM header authenticated')
|
||||
|
||||
def test_sm_proprietary(self):
|
||||
# 0x0C = 0000 1100: b4-b3 = 01 → proprietary secure messaging
|
||||
r = self._cla(0x0c)
|
||||
# 0x04 = 0000 0100: b4-b3 = 01 → proprietary secure messaging;
|
||||
# 0x0C = b4-b3 = 11 → header authenticated (ISO 7816-4 Table 2).
|
||||
r = self._cla(0x04)
|
||||
self.assertEqual(r['secure_messaging'], 'proprietary')
|
||||
r = self._cla(0x0c)
|
||||
self.assertEqual(r['channel'], 0)
|
||||
self.assertEqual(r['secure_messaging'], 'SM header authenticated')
|
||||
|
||||
def test_chain_bit_b5(self):
|
||||
# 0x20 = 0010 0000: b5=1 → first or continuing command of chain
|
||||
r = self._cla(0x20)
|
||||
# 0x10 = 0001 0000: b5=1 → first or continuing command of chain
|
||||
# (ISO 7816-4 §5.1.1 Table 2).
|
||||
r = self._cla(0x10)
|
||||
self.assertEqual(r['chain'], 'first or continuing')
|
||||
|
||||
def test_reserved_class_001x(self):
|
||||
# 0x20 = 0010 0000: the '001x xxxx' range is reserved by ISO
|
||||
# 7816-4; its bits carry no defined SM/chaining meaning.
|
||||
r = self._cla(0x20)
|
||||
self.assertEqual(r['interclass'], 'reserved (ISO 7816-4)')
|
||||
self.assertEqual(r['chain'], 'n/a')
|
||||
self.assertEqual(r['secure_messaging'], 'n/a')
|
||||
|
||||
def test_further_interindustry_channel_offset(self):
|
||||
# 0x40 = 0100 0000: further format, channel = b4-b1 + 4 = 4
|
||||
r = self._cla(0x40)
|
||||
self.assertEqual(r['interclass'], 'inter-industry (further format)')
|
||||
self.assertEqual(r['channel'], 4)
|
||||
|
||||
def test_extended_channels_iso(self):
|
||||
# TS 102 221 §10.3: the further-interindustry coding carries the
|
||||
# extended channels 4-19 in b4-b1 (ISO 7816-4 §5.1.1 Table 3).
|
||||
for cla, channel in ((0x44, 8), (0x49, 13), (0x4a, 14), (0x4f, 19),
|
||||
(0x60, 4), (0x6f, 19)):
|
||||
r = self._cla(cla)
|
||||
self.assertEqual(r['channel'], channel, f'CLA {cla:#04x}')
|
||||
# b6 of the further format is the SM indication, b5 the chaining bit.
|
||||
self.assertEqual(self._cla(0x60)['secure_messaging'],
|
||||
'SM header not authenticated')
|
||||
self.assertEqual(self._cla(0x70)['chain'], 'first or continuing')
|
||||
|
||||
def test_extended_channels_etsi(self):
|
||||
# TS 102 221 Table 10.4a: the ETSI '11x0 xxxx' coding addresses the
|
||||
# extended channels too (b4-b1 + 4).
|
||||
for cla, channel in ((0xc0, 4), (0xc5, 9), (0xcf, 19), (0xe0, 4),
|
||||
(0xe5, 9), (0xef, 19)):
|
||||
r = self._cla(cla)
|
||||
self.assertEqual(r['interclass'],
|
||||
'ETSI-defined (extended logical channels)',
|
||||
f'CLA {cla:#04x}')
|
||||
self.assertEqual(r['channel'], channel, f'CLA {cla:#04x}')
|
||||
# b6 is the SM indication (Table 10.4b), b5 must be 0.
|
||||
self.assertEqual(self._cla(0xe5)['secure_messaging'],
|
||||
'SM header not authenticated')
|
||||
self.assertEqual(self._cla(0xc5)['secure_messaging'], 'none')
|
||||
|
||||
def test_etsi_uicc(self):
|
||||
r = self._cla(0x80)
|
||||
self.assertEqual(r['interclass'], 'ETSI-defined (UICC/USIM)')
|
||||
self.assertEqual(r['channel'], 0)
|
||||
# The low nibble of the ETSI classes carries SM (b4-b3) and the
|
||||
# standard channel (b2-b1), TS 102 221 Table 10.3/10.4.
|
||||
r = self._cla(0x8c)
|
||||
self.assertEqual(r['interclass'], 'ETSI-defined (UICC/USIM)')
|
||||
self.assertEqual(r['channel'], 0)
|
||||
self.assertEqual(r['secure_messaging'], 'SM header authenticated')
|
||||
r = self._cla(0x83)
|
||||
self.assertEqual(r['channel'], 3)
|
||||
|
||||
def test_etsi_sim(self):
|
||||
r = self._cla(0xa0)
|
||||
self.assertEqual(r['interclass'], 'ETSI-defined (SIM/GSM)')
|
||||
r = self._cla(0xa3)
|
||||
self.assertEqual(r['interclass'], 'ETSI-defined (SIM/GSM)')
|
||||
self.assertEqual(r['channel'], 3)
|
||||
|
||||
def test_proprietary_and_invalid(self):
|
||||
# b5 = 1 contradicts the ETSI class codings (TS 102 221 §10.1.1) and
|
||||
# 0xFF is invalid; both keep the best-effort channel reading.
|
||||
self.assertEqual(self._cla(0x90)['interclass'], 'proprietary')
|
||||
self.assertEqual(self._cla(0xd5)['channel'], 9)
|
||||
self.assertEqual(self._cla(0xff)['interclass'], 'invalid (ISO 7816-4)')
|
||||
|
||||
def test_cla_channel_helper(self):
|
||||
# The tracker/state paths share this helper with decode_cla.
|
||||
from simtrace_analyser.decode import cla_channel
|
||||
self.assertEqual(cla_channel(0x00), 0)
|
||||
self.assertEqual(cla_channel(0x03), 3)
|
||||
self.assertEqual(cla_channel(0x80), 0)
|
||||
self.assertEqual(cla_channel(0x44), 8)
|
||||
self.assertEqual(cla_channel(0x4f), 19)
|
||||
self.assertEqual(cla_channel(0xef), 19)
|
||||
|
||||
def test_looks_like_apdu(self):
|
||||
from simtrace_analyser.decode import looks_like_apdu
|
||||
self.assertTrue(looks_like_apdu(bytes.fromhex('00a40004023f00')))
|
||||
self.assertTrue(looks_like_apdu(bytes.fromhex('80f2000000')))
|
||||
self.assertFalse(looks_like_apdu(b''))
|
||||
self.assertFalse(looks_like_apdu(bytes.fromhex('00a4')))
|
||||
self.assertFalse(looks_like_apdu(bytes.fromhex('ff10957aff10957a')))
|
||||
self.assertFalse(looks_like_apdu(bytes.fromhex('2d6ccfb0ff')))
|
||||
self.assertFalse(looks_like_apdu(bytes.fromhex('31e073fe211b66')))
|
||||
|
||||
|
||||
class TestLineEvents(unittest.TestCase):
|
||||
|
||||
Reference in new issue
Block a user