TERMINAL CAPABILITY: decode the A9 template (TS 102 221 §11.1.19)
- terminal power supply (tag 80): voltage class per Table 6.1, maximum
current in mA, clock in 0.1 MHz steps ('FF' = not indicated)
- extended logical channels (tag 81) — zero length per §11.1.19.2.2, a
nonzero length is preserved as raw
- additional interfaces (tag 82 b1 = UICC-CLF)
- eUICC device capabilities: tag 83 per GSMA SGP.22 v2.6 §3.4.2 Table 7
(LUId/LPDd/LDSd/LUIe-SCWS); tag 84 preserved raw (no pinned SGP.32 spec)
- unknown/private TLVs preserved via raw_tlv
- concise APDU-list summary; decoded rows in the PWA detail view
- v2.1.0; SW cache simtrace-analyser-v55
- tests: 6 real samples from the local trace sessions (49/55/58/61/206/208)
This commit is contained in:
1 parent
38650b4797
commit
561801a983
9 files changed
+210
-6
No files matched your search
@@ -39,6 +39,10 @@ GSMTAP into this analyser.
|
||||
proactive commands with decoded qualifiers, TERMINAL RESPONSE, ENVELOPE
|
||||
(Menu Selection, Call Control, SMS-PP download…); tolerates known
|
||||
non-compliant legacy-card quirks and preserves unrecognized TLVs
|
||||
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — terminal power supply
|
||||
(voltage class, maximum current, clock), extended logical channels,
|
||||
additional interfaces (UICC-CLF) and eUICC device capabilities
|
||||
(SGP.22/SGP.32)
|
||||
- SMS TPDU — SMS-DELIVER/SUBMIT, SCTS timestamps, UDH information elements
|
||||
- SCP80 OTA secured packets — SPI bits (ciphering, PoR requirement/mode,
|
||||
RC/CC/DS, counter), KIc/KID algorithm + key set, TAR, CNTR/PCNTR;
|
||||
|
||||
@@ -40,6 +40,10 @@ SIM/UICC из UDP-потока GSMTAP (или импортирует файлы
|
||||
proactive-команды с декодированными квалификаторами, TERMINAL RESPONSE,
|
||||
ENVELOPE (Menu Selection, Call Control, SMS-PP download…); учитываются
|
||||
известные несоответствия старых карт, нераспознанные TLV сохраняются
|
||||
- TERMINAL CAPABILITY (TS 102 221 §11.1.19) — питание терминала (класс
|
||||
напряжения, максимальный ток, тактовая частота), расширенные логические
|
||||
каналы, дополнительные интерфейсы (UICC-CLF) и возможности eUICC
|
||||
(SGP.22/SGP.32)
|
||||
- SMS TPDU — SMS-DELIVER/SUBMIT, метки времени SCTS, информационные
|
||||
элементы UDH
|
||||
- SCP80 OTA secured packets — биты SPI (шифрование, требование/режим PoR,
|
||||
|
||||
+26
-1
@@ -1300,6 +1300,31 @@ function cmdRows(cmd) {
|
||||
rows.push(['Encapsulated', cmd.encapsulated.type]);
|
||||
}
|
||||
if (cmd.text_note) rows.push(['Text note', cmd.text_note]);
|
||||
|
||||
// TERMINAL CAPABILITY (TS 102 221 §11.1.19)
|
||||
if (cmd.power_supply) {
|
||||
const ps = cmd.power_supply;
|
||||
const bits = [];
|
||||
bits.push(ps.classes && ps.classes.length
|
||||
? 'class ' + ps.classes.join('+')
|
||||
: 'class 0x' + hx(ps.class_raw || 0, 2));
|
||||
if (ps.max_current_ma !== undefined) bits.push(ps.max_current_ma + ' mA');
|
||||
if (ps.clock_mhz !== undefined && ps.clock_mhz !== null) bits.push(ps.clock_mhz + ' MHz clock');
|
||||
else if (ps.clock_raw !== undefined) bits.push('clock not indicated');
|
||||
rows.push(['Power supply', bits.join(', ')]);
|
||||
}
|
||||
if (cmd.extended_channels) rows.push(['Logical channels', 'extended support']);
|
||||
if (cmd.extended_channels_raw) rows.push(['Logical channels raw', cmd.extended_channels_raw]);
|
||||
if (cmd.interfaces) rows.push(['Additional interfaces',
|
||||
cmd.interfaces.length ? cmd.interfaces.join(', ') : 'none']);
|
||||
if (cmd.interfaces_raw) rows.push(['Interfaces raw', cmd.interfaces_raw]);
|
||||
if (cmd.euicc_sgp22) {
|
||||
const e = cmd.euicc_sgp22;
|
||||
const names = (e.functions || []).join(', ');
|
||||
rows.push(['eUICC (SGP.22)', names ? names + ' \u00b7 0x' + e.raw : '0x' + e.raw]);
|
||||
}
|
||||
if (cmd.euicc_sgp32) rows.push(['eUICC (SGP.32)', '0x' + (cmd.euicc_sgp32.raw || '')]);
|
||||
|
||||
if (cmd.raw_tlv) {
|
||||
for (const t of cmd.raw_tlv) rows.push(['TLV 0x' + t.tag, t.value || '(empty)']);
|
||||
}
|
||||
@@ -2681,7 +2706,7 @@ function onFilterChange() {
|
||||
}
|
||||
|
||||
// ──────────────────── Versioning ────────────────────
|
||||
const PWA_VERSION = '2.0.0';
|
||||
const PWA_VERSION = '2.1.0';
|
||||
const MIN_SERVER_VERSION = '1.0.0';
|
||||
|
||||
function parseVersion(v) {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simtrace-analyser-v54';
|
||||
const CACHE = 'simtrace-analyser-v55';
|
||||
|
||||
const PRECACHE = [
|
||||
'index.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
||||
|
||||
[project]
|
||||
name = "simtrace-analyser"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||
readme = {file = "README.md", content-type = "text/markdown"}
|
||||
license = {text = "GPL-2.0-or-later"}
|
||||
|
||||
+113
-1
@@ -834,7 +834,7 @@ APDU_SPEC = {
|
||||
0xAA: {
|
||||
'name': 'TERMINAL CAPABILITY',
|
||||
'p1p2': {'unused': True},
|
||||
'body': {'label': 'TLV data'},
|
||||
'body': {'label': 'Terminal capability template (A9)'},
|
||||
},
|
||||
0x73: {
|
||||
'name': 'MANAGE SECURE CHANNEL',
|
||||
@@ -2861,6 +2861,86 @@ def _decode_terminal_profile(data):
|
||||
return result
|
||||
|
||||
|
||||
# ──────────────────── TERMINAL CAPABILITY (TS 102 221 §11.1.19) ────────────────────
|
||||
|
||||
# The command data is the constructed terminal capability template (tag 'A9').
|
||||
# Supply voltage class coding per Table 6.1 (same as the ATR class indication);
|
||||
# the eUICC tag '83' bit meanings per GSMA SGP.22 v2.6 §3.4.2 Table 7.
|
||||
_TC_SUPPLY_CLASSES = (
|
||||
(0x01, 'A'),
|
||||
(0x02, 'B'),
|
||||
(0x04, 'C'),
|
||||
(0x08, 'D'),
|
||||
(0x10, 'E'),
|
||||
)
|
||||
|
||||
_TC_EUICC_SGP22_BITS = (
|
||||
(0x01, 'LUId'),
|
||||
(0x02, 'LPDd'),
|
||||
(0x04, 'LDSd'),
|
||||
(0x08, 'LUIe (SCWS)'),
|
||||
)
|
||||
|
||||
|
||||
def _decode_terminal_capability(data):
|
||||
"""Decode a TERMINAL CAPABILITY command body (TS 102 221 §11.1.19).
|
||||
|
||||
TLV objects inside the template (tag 'A9'):
|
||||
'80' terminal power supply, '81' extended logical channels terminal
|
||||
support, '82' additional interfaces support, '83'/'84' eUICC-related
|
||||
device capabilities (GSMA SGP.22 / SGP.32). Unknown and private TLV
|
||||
objects are preserved as ``raw_tlv``.
|
||||
"""
|
||||
inner = None
|
||||
for tag, _length, value in parse_tlv(data):
|
||||
if tag == 0xA9:
|
||||
inner = value
|
||||
break
|
||||
if inner is None:
|
||||
return None
|
||||
|
||||
result = {}
|
||||
raw_tlv = []
|
||||
for tag, _length, value in parse_tlv(inner):
|
||||
if tag == 0x80: # terminal power supply (§11.1.19.2.1)
|
||||
supply = {
|
||||
'class_raw': value[0] if value else None,
|
||||
'classes': [name for bit, name in _TC_SUPPLY_CLASSES
|
||||
if value and value[0] & bit],
|
||||
}
|
||||
if len(value) >= 2:
|
||||
supply['max_current_ma'] = value[1]
|
||||
if len(value) >= 3:
|
||||
supply['clock_raw'] = value[2]
|
||||
supply['clock_mhz'] = (None if value[2] == 0xFF
|
||||
else round(value[2] / 10, 1))
|
||||
result['power_supply'] = supply
|
||||
elif tag == 0x81: # extended logical channels (§11.1.19.2.2)
|
||||
# Only zero length is defined; a UICC treats any length as zero.
|
||||
result['extended_channels'] = True
|
||||
if value:
|
||||
result['extended_channels_raw'] = value.hex().upper()
|
||||
elif tag == 0x82: # additional interfaces support (§11.1.19.2.3)
|
||||
if value:
|
||||
result['interfaces'] = ['UICC-CLF'] if value[0] & 0x01 else []
|
||||
if len(value) > 1: # extra bytes are ignored by the UICC
|
||||
result['interfaces_raw'] = value.hex().upper()
|
||||
elif tag == 0x83: # eUICC indications — GSMA SGP.22 (§11.1.19.2.4)
|
||||
caps = {'raw': value.hex().upper() if value else ''}
|
||||
if value:
|
||||
caps['functions'] = [name for bit, name in _TC_EUICC_SGP22_BITS
|
||||
if value[0] & bit]
|
||||
result['euicc_sgp22'] = caps
|
||||
elif tag == 0x84: # eUICC indications — GSMA SGP.32 (raw, no pinned spec)
|
||||
result['euicc_sgp32'] = {'raw': value.hex().upper() if value else ''}
|
||||
else:
|
||||
raw_tlv.append({'tag': f'{tag:02X}', 'value': value.hex().upper()})
|
||||
|
||||
if raw_tlv:
|
||||
result['raw_tlv'] = raw_tlv
|
||||
return result or None
|
||||
|
||||
|
||||
# ──────────────────── Decode entry point ────────────────────
|
||||
|
||||
# ──────────────────── File data decoders (READ/UPDATE) ────────────────────
|
||||
@@ -3906,6 +3986,34 @@ def _build_summary(result):
|
||||
s += f" \u00ab{tpdu['text']}\u00bb"
|
||||
parts.append(s)
|
||||
|
||||
supply = cmd.get('power_supply')
|
||||
interfaces = cmd.get('interfaces')
|
||||
sgp22 = cmd.get('euicc_sgp22')
|
||||
sgp32 = cmd.get('euicc_sgp32')
|
||||
if supply or cmd.get('extended_channels') or interfaces or sgp22 or sgp32:
|
||||
tc = []
|
||||
if supply:
|
||||
if supply.get('classes'):
|
||||
tc.append('Class ' + '+'.join(supply['classes']))
|
||||
if supply.get('max_current_ma') is not None:
|
||||
tc.append(f"{supply['max_current_ma']} mA")
|
||||
if supply.get('clock_mhz') is not None:
|
||||
tc.append(f"CLK {supply['clock_mhz']:g} MHz")
|
||||
elif supply.get('clock_raw') is not None:
|
||||
tc.append('CLK n/a')
|
||||
if cmd.get('extended_channels'):
|
||||
tc.append('extended channels')
|
||||
if interfaces:
|
||||
tc.append(', '.join(interfaces))
|
||||
if sgp22:
|
||||
funcs = sgp22.get('functions') or []
|
||||
tc.append('eUICC: ' + (', '.join(funcs) if funcs
|
||||
else '0x' + sgp22.get('raw', '')))
|
||||
if sgp32:
|
||||
tc.append('eUICC SGP.32: 0x' + sgp32.get('raw', ''))
|
||||
if tc:
|
||||
parts.append(' \u00b7 '.join(tc))
|
||||
|
||||
if result.get('response_to'):
|
||||
parts.append('\u2192 ' + result['response_to'])
|
||||
response = result.get('response')
|
||||
@@ -4052,6 +4160,10 @@ def decode_message(raw_data, prev=None):
|
||||
tp = _decode_terminal_profile(body)
|
||||
if tp:
|
||||
result['tp'] = tp
|
||||
elif ins == 0xAA: # TERMINAL CAPABILITY → terminal capability template
|
||||
caps = _decode_terminal_capability(body)
|
||||
if caps:
|
||||
result['cmd'] = caps
|
||||
|
||||
# File data decode for READ/UPDATE using the current selection.
|
||||
p1p2 = result.get('p1p2') or {}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Single source of truth for the SIMtrace Analyser version."""
|
||||
|
||||
__version__ = '2.0.0'
|
||||
__version__ = '2.1.0'
|
||||
@@ -1079,6 +1079,65 @@ class TestTerminalProfile(unittest.TestCase):
|
||||
self.assertEqual(tp['summary'], 'handset \u00b7 BIP')
|
||||
|
||||
|
||||
class TestTerminalCapability(unittest.TestCase):
|
||||
"""TERMINAL CAPABILITY (TS 102 221 §11.1.19) — real samples captured
|
||||
from the local trace sessions (49/55/58/61/206/208)."""
|
||||
|
||||
def _dec(self, body_hex):
|
||||
raw = '80AA0000%02x%s9000' % (len(body_hex) // 2, body_hex)
|
||||
return decode_message(bytes.fromhex(raw))
|
||||
|
||||
def test_power_supply(self):
|
||||
# A9 0A 80 03 04 3C FF 81 00 82 01 00 — class C, 60 mA, clock n/a.
|
||||
r = self._dec('A90A8003043CFF8100820100')
|
||||
self.assertEqual(r['body']['label'], 'Terminal capability template (A9)')
|
||||
ps = r['cmd']['power_supply']
|
||||
self.assertEqual(ps['classes'], ['C'])
|
||||
self.assertEqual(ps['class_raw'], 0x04)
|
||||
self.assertEqual(ps['max_current_ma'], 60)
|
||||
self.assertIsNone(ps['clock_mhz'])
|
||||
self.assertTrue(r['cmd']['extended_channels'])
|
||||
self.assertEqual(r['cmd']['interfaces'], [])
|
||||
self.assertEqual(r['summary'],
|
||||
'Class C \u00b7 60 mA \u00b7 CLK n/a \u00b7 extended channels')
|
||||
|
||||
def test_interfaces_and_euicc(self):
|
||||
# A9 08 81 00 82 01 01 83 01 87 — UICC-CLF + SGP.22 LPA functions
|
||||
# (b8 is an RFU bit and must not affect the function list).
|
||||
r = self._dec('A9088100820101830187')
|
||||
cmd = r['cmd']
|
||||
self.assertNotIn('power_supply', cmd)
|
||||
self.assertEqual(cmd['interfaces'], ['UICC-CLF'])
|
||||
self.assertEqual(cmd['euicc_sgp22']['raw'], '87')
|
||||
self.assertEqual(cmd['euicc_sgp22']['functions'], ['LUId', 'LPDd', 'LDSd'])
|
||||
self.assertEqual(r['summary'],
|
||||
'extended channels \u00b7 UICC-CLF \u00b7 '
|
||||
'eUICC: LUId, LPDd, LDSd')
|
||||
|
||||
def test_interfaces_only(self):
|
||||
# A9 05 81 00 82 01 01 — phones that omit the power supply TLV.
|
||||
r = self._dec('A9058100820101')
|
||||
self.assertNotIn('power_supply', r['cmd'])
|
||||
self.assertEqual(r['summary'], 'extended channels \u00b7 UICC-CLF')
|
||||
|
||||
def test_extended_channels_nonzero_length(self):
|
||||
# A UICC shall interpret any length as zero (TS 102 221 §11.1.19.2.2).
|
||||
r = self._dec('A906810100820101')
|
||||
self.assertTrue(r['cmd']['extended_channels'])
|
||||
self.assertEqual(r['cmd']['extended_channels_raw'], '00')
|
||||
|
||||
def test_private_tlv_preserved(self):
|
||||
# Unknown/private TLV ('DD') inside the A9 template must survive.
|
||||
r = self._dec('A9088100820101DD017F')
|
||||
self.assertEqual(r['cmd']['raw_tlv'], [{'tag': 'DD', 'value': '7F'}])
|
||||
self.assertEqual(r['summary'], 'extended channels \u00b7 UICC-CLF')
|
||||
|
||||
def test_no_a9_template(self):
|
||||
# Command data without the A9 template stays undecoded (no crash).
|
||||
r = self._dec('8003043CFF')
|
||||
self.assertNotIn('cmd', r)
|
||||
|
||||
|
||||
class TestSummary(unittest.TestCase):
|
||||
def test_select_path_from_mf(self):
|
||||
r = decode_message(bytes.fromhex('a0a40804022fe29000'))
|
||||
|
||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
||||
class TestVersion(unittest.TestCase):
|
||||
def test_package_version(self):
|
||||
from simtrace2_pysniff import __version__
|
||||
self.assertEqual(__version__, '2.0.0')
|
||||
self.assertEqual(__version__, '2.1.0')
|
||||
|
||||
def test_status_includes_version(self):
|
||||
import os
|
||||
|
||||
Reference in new issue
Block a user