Fix APDU/SPI/SW decoder bugs per TS 102 221/TS 102 225; bump to 1.10.0
Spec-correctness release, verified against TS 102 221 V18.2.0 and
TS 102 225 §5.1.1:
- SPI1 ciphering bit: b3 (0x04), not b4 (0x08) — both the SPI dict
and the KIc-unused check. Real MegaFon captures (SPI1=16) decoded
as unciphered with "KIc unused" before this fix.
- SPI2 PoR-ciphering bit: b5 (0x10), not b6 (0x20); add por_transport
field from SPI2 b6 (SMS-DELIVER-REPORT vs SMS-SUBMIT).
- SW table: add UICC-specific SWs 62F1/F2/F3, 63F1/F2, 6401, 9300
(SAT busy), 9850, 9862/63/64; fix 6500 text ("NV memory changed");
91XX now decodes as "Proactive command pending" via pattern.
- 63CX no longer hardcoded as PIN failure — exact 63F1/F2 entries take
precedence; all other 63CX route through one verification-retry
pattern (_SW_PATTERNS dict → ordered list).
- APDU_SPEC: +9 commands (D7/DD odd-INS update, D2 write record,
82 external auth, 86/87 general auth, 22 MSE, 0E/0C erase, 7C LSI).
- decode_cla: proper ISO 7816-4 §5.1.1 coding — SM bits b4-b3,
chain bit b5, further-interindustry channel offset (+4).
Tests: +27 (TestSwUiccSpecific, TestSpiBits, TestApduSpecCoverage,
TestClaDecode) — 279 total. SW cache bumped to simtrace-analyser-v12.
This commit is contained in:
1 parent
f75a4a4183
commit
ed25fcecf5
7 files changed
+310
-36
No files matched your search
+1
-1
@@ -1851,7 +1851,7 @@ function onFilterChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '1.9.0';
|
const PWA_VERSION = '1.10.0';
|
||||||
const MIN_SERVER_VERSION = '1.0.0';
|
const MIN_SERVER_VERSION = '1.0.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v11';
|
const CACHE = 'simtrace-analyser-v12';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace2-pysniff"
|
name = "simtrace2-pysniff"
|
||||||
version = "1.9.0"
|
version = "1.10.0"
|
||||||
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
|
|||||||
from .pcap import PcapWriter
|
from .pcap import PcapWriter
|
||||||
from .dump import FileDumper, format_message
|
from .dump import FileDumper, format_message
|
||||||
|
|
||||||
__version__ = '1.9.0'
|
__version__ = '1.10.0'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ the SIMtrace2 sniffer into structured dicts for the PWA to display.
|
|||||||
|
|
||||||
SW_NAMES = {
|
SW_NAMES = {
|
||||||
(0x90, 0x00): 'Normal ending',
|
(0x90, 0x00): 'Normal ending',
|
||||||
(0x91, 0x00): 'Normal ending, proactive command pending',
|
|
||||||
(0x92, 0x00): 'Normal ending after x bytes',
|
|
||||||
(0x61, 0x00): 'Response data available',
|
(0x61, 0x00): 'Response data available',
|
||||||
(0x62, 0x00): 'Warning — no information',
|
(0x62, 0x00): 'Warning — no information',
|
||||||
(0x62, 0x81): 'Warning — part of data may be corrupted',
|
(0x62, 0x81): 'Warning — part of data may be corrupted',
|
||||||
(0x62, 0x82): 'Warning — EOF reached before reading Le bytes',
|
(0x62, 0x82): 'Warning — EOF reached before reading Le bytes',
|
||||||
(0x62, 0x83): 'Warning — selected file deactivated',
|
(0x62, 0x83): 'Warning — selected file deactivated',
|
||||||
(0x62, 0x84): 'Warning — FCI not formatted per ISO',
|
(0x62, 0x84): 'Warning — FCI not formatted per ISO',
|
||||||
|
(0x62, 0x85): 'Warning — selected file in termination state',
|
||||||
|
(0x62, 0xf1): 'More data available',
|
||||||
|
(0x62, 0xf2): 'More data available and proactive command pending',
|
||||||
|
(0x62, 0xf3): 'Response data available',
|
||||||
(0x63, 0x00): 'Warning — no information',
|
(0x63, 0x00): 'Warning — no information',
|
||||||
(0x63, 0xc1): 'Warning — 1 retry remaining',
|
(0x63, 0xf1): 'More data expected',
|
||||||
(0x63, 0xc2): 'Warning — 2 retries remaining',
|
(0x63, 0xf2): 'More data expected and proactive command pending',
|
||||||
(0x63, 0xc3): 'Warning — 3 retries remaining',
|
|
||||||
(0x64, 0x00): 'Execution error — no information',
|
(0x64, 0x00): 'Execution error — no information',
|
||||||
(0x65, 0x00): 'Execution error — memory failure',
|
(0x64, 0x01): 'Execution error — immediate response required',
|
||||||
|
(0x65, 0x00): 'Execution error — no information, NV memory changed',
|
||||||
(0x65, 0x81): 'Execution error — memory failure',
|
(0x65, 0x81): 'Execution error — memory failure',
|
||||||
(0x66, 0x00): 'Security error — no information',
|
(0x66, 0x00): 'Security error — no information',
|
||||||
(0x66, 0x81): 'Reserved for security-related issues',
|
(0x66, 0x81): 'Reserved for security-related issues',
|
||||||
@@ -40,6 +42,7 @@ SW_NAMES = {
|
|||||||
(0x69, 0x86): 'Command not allowed (no current EF)',
|
(0x69, 0x86): 'Command not allowed (no current EF)',
|
||||||
(0x69, 0x87): 'Expected SM data objects missing',
|
(0x69, 0x87): 'Expected SM data objects missing',
|
||||||
(0x69, 0x88): 'SM data objects incorrect',
|
(0x69, 0x88): 'SM data objects incorrect',
|
||||||
|
(0x69, 0x89): 'Command not allowed — secure channel security not satisfied',
|
||||||
(0x6a, 0x00): 'Checking error — no information',
|
(0x6a, 0x00): 'Checking error — no information',
|
||||||
(0x6a, 0x80): 'Incorrect data in command data',
|
(0x6a, 0x80): 'Incorrect data in command data',
|
||||||
(0x6a, 0x81): 'Function not supported',
|
(0x6a, 0x81): 'Function not supported',
|
||||||
@@ -56,6 +59,11 @@ SW_NAMES = {
|
|||||||
(0x6d, 0x00): 'Instruction not supported or invalid',
|
(0x6d, 0x00): 'Instruction not supported or invalid',
|
||||||
(0x6e, 0x00): 'Class not supported',
|
(0x6e, 0x00): 'Class not supported',
|
||||||
(0x6f, 0x00): 'No precise diagnosis',
|
(0x6f, 0x00): 'No precise diagnosis',
|
||||||
|
(0x93, 0x00): 'SIM Application Toolkit busy',
|
||||||
|
(0x98, 0x50): 'INCREASE cannot be performed, max value reached',
|
||||||
|
(0x98, 0x62): 'Authentication error, application specific',
|
||||||
|
(0x98, 0x63): 'Security session or association expired',
|
||||||
|
(0x98, 0x64): 'Minimum UICC suspension time too long',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -63,15 +71,17 @@ def _sw_name(sw1, sw2):
|
|||||||
key = (sw1, sw2)
|
key = (sw1, sw2)
|
||||||
return SW_NAMES.get(key)
|
return SW_NAMES.get(key)
|
||||||
|
|
||||||
# pattern match: 63cx, 62xx, etc.
|
# pattern match: 91xx, 63cx, etc.
|
||||||
_SW_PATTERNS = {
|
_SW_PATTERNS = [
|
||||||
(0x63,): lambda s1, s2: ('PIN verification failed, %d retries remaining' % (s2 & 0x0f)
|
((0x91,), lambda s1, s2: 'Proactive command pending (%d bytes)' % s2),
|
||||||
if s2 & 0xc0 == 0xc0 else None),
|
((0x92,), lambda s1, s2: 'Normal ending, TRANSACT DATA info (0x%02X)' % 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),
|
((0x63,), lambda s1, s2: ('Verification failed, %d retries remaining' % (s2 & 0x0f)
|
||||||
(0x9e,): lambda s1, s2: 'Normal processing, %d bytes of response' % s2,
|
if s2 & 0xc0 == 0xc0 else None)),
|
||||||
(0x9f,): lambda s1, s2: 'Normal processing, %d bytes of 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),
|
||||||
|
((0x9f,), lambda s1, s2: 'Normal processing, %d bytes of response' % s2),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def decode_sw(raw_sw):
|
def decode_sw(raw_sw):
|
||||||
@@ -80,13 +90,7 @@ def decode_sw(raw_sw):
|
|||||||
sw1, sw2 = raw_sw[-2], raw_sw[-1]
|
sw1, sw2 = raw_sw[-2], raw_sw[-1]
|
||||||
name = _sw_name(sw1, sw2)
|
name = _sw_name(sw1, sw2)
|
||||||
if name is None:
|
if name is None:
|
||||||
for keys, fn in _SW_PATTERNS.items():
|
for keys, fn in _SW_PATTERNS:
|
||||||
if len(keys) == 1:
|
|
||||||
if sw1 == keys[0]:
|
|
||||||
name = fn(sw1, sw2)
|
|
||||||
if name:
|
|
||||||
break
|
|
||||||
elif len(keys) == 2:
|
|
||||||
if sw1 == keys[0]:
|
if sw1 == keys[0]:
|
||||||
name = fn(sw1, sw2)
|
name = fn(sw1, sw2)
|
||||||
if name:
|
if name:
|
||||||
@@ -101,22 +105,43 @@ def decode_sw(raw_sw):
|
|||||||
# ──────────────────── CLA byte ────────────────────
|
# ──────────────────── CLA byte ────────────────────
|
||||||
|
|
||||||
def decode_cla(cla):
|
def decode_cla(cla):
|
||||||
chain_bits = (cla >> 4) & 0x03
|
|
||||||
chain_names = {0: 'last or only', 1: 'first in chain', 2: 'not last in chain', 3: 'not last'}
|
|
||||||
if cla == 0x80:
|
if cla == 0x80:
|
||||||
interclass = 'ETSI-defined (UICC/USIM)'
|
interclass = 'ETSI-defined (UICC/USIM)'
|
||||||
elif cla == 0xa0:
|
elif cla == 0xa0:
|
||||||
interclass = 'ETSI-defined (SIM/GSM)'
|
interclass = 'ETSI-defined (SIM/GSM)'
|
||||||
elif cla == 0x00:
|
elif cla & 0x80:
|
||||||
|
interclass = 'proprietary'
|
||||||
|
elif (cla >> 6) & 0x03 == 0x00:
|
||||||
interclass = 'inter-industry (ISO 7816-4)'
|
interclass = 'inter-industry (ISO 7816-4)'
|
||||||
|
elif (cla >> 6) & 0x03 == 0x01:
|
||||||
|
interclass = 'inter-industry (further format)'
|
||||||
else:
|
else:
|
||||||
interclass = 'inter-industry' if (cla & 0x80) == 0 else 'proprietary'
|
interclass = 'reserved'
|
||||||
|
if cla & 0x80:
|
||||||
|
# proprietary / ETSI-defined: no standard SM/chaining/channel coding
|
||||||
result = {
|
result = {
|
||||||
'hex': f'{cla:02x}',
|
'hex': f'{cla:02x}',
|
||||||
'interclass': interclass,
|
'interclass': interclass,
|
||||||
'channel': cla & 0x03,
|
'channel': cla & 0x03,
|
||||||
'secure_messaging': 'none' if (cla & 0x0c) == 0 else 'present',
|
'secure_messaging': 'none',
|
||||||
'chain': chain_names.get(chain_bits, f'unknown ({chain_bits})'),
|
'chain': 'last or only',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# ISO 7816-4 §5.1.1 first/further interindustry
|
||||||
|
chain_bit = (cla >> 5) & 0x01
|
||||||
|
chain_names = {0: 'last or only', 1: 'first or continuing'}
|
||||||
|
sm_names = {0: 'none', 1: 'proprietary', 2: 'SM header not authenticated',
|
||||||
|
3: 'SM header authenticated'}
|
||||||
|
sm_ind = (cla >> 3) & 0x03
|
||||||
|
channel = cla & 0x03
|
||||||
|
if (cla >> 6) & 0x03 == 0x01:
|
||||||
|
channel += 4 # further format: b4-b1 = channel + 4
|
||||||
|
result = {
|
||||||
|
'hex': f'{cla:02x}',
|
||||||
|
'interclass': interclass,
|
||||||
|
'channel': channel,
|
||||||
|
'secure_messaging': sm_names.get(sm_ind, 'none'),
|
||||||
|
'chain': chain_names.get(chain_bit, 'last or only'),
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -594,6 +619,37 @@ APDU_SPEC = {
|
|||||||
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
||||||
'body': {'label': 'Data'},
|
'body': {'label': 'Data'},
|
||||||
},
|
},
|
||||||
|
0xD7: {
|
||||||
|
'name': 'UPDATE BINARY (odd INS)',
|
||||||
|
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
||||||
|
'body': {'label': 'BER-TLV data'},
|
||||||
|
},
|
||||||
|
0xDD: {
|
||||||
|
'name': 'UPDATE RECORD (odd INS)',
|
||||||
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
|
'p2': {
|
||||||
|
'label': 'Mode',
|
||||||
|
'bits': {
|
||||||
|
0x02: 'next record',
|
||||||
|
0x03: 'previous record',
|
||||||
|
0x04: 'absolute mode (record number in P1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'body': {'label': 'BER-TLV data'},
|
||||||
|
},
|
||||||
|
0xD2: {
|
||||||
|
'name': 'WRITE RECORD',
|
||||||
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
|
'p2': {
|
||||||
|
'label': 'Mode',
|
||||||
|
'bits': {
|
||||||
|
0x02: 'next record',
|
||||||
|
0x03: 'previous record',
|
||||||
|
0x04: 'absolute mode (record number in P1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'body': {'label': 'Data'},
|
||||||
|
},
|
||||||
0xDC: {
|
0xDC: {
|
||||||
'name': 'UPDATE RECORD',
|
'name': 'UPDATE RECORD',
|
||||||
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
@@ -658,6 +714,28 @@ APDU_SPEC = {
|
|||||||
'name': 'AUTHENTICATE',
|
'name': 'AUTHENTICATE',
|
||||||
'body': {'label': 'Response/resynchronisation data'},
|
'body': {'label': 'Response/resynchronisation data'},
|
||||||
},
|
},
|
||||||
|
0x82: {
|
||||||
|
'name': 'EXTERNAL AUTHENTICATE',
|
||||||
|
'p1': {'label': 'Security level'},
|
||||||
|
'p2': {0x00: 'No indication'},
|
||||||
|
'body': {'label': 'Cryptogram'},
|
||||||
|
},
|
||||||
|
0x86: {
|
||||||
|
'name': 'GENERAL AUTHENTICATE',
|
||||||
|
'p1p2': {'unused': True},
|
||||||
|
'body': {'label': 'Auth data'},
|
||||||
|
},
|
||||||
|
0x87: {
|
||||||
|
'name': 'GENERAL AUTHENTICATE (odd INS)',
|
||||||
|
'p1p2': {'unused': True},
|
||||||
|
'body': {'label': 'BER-TLV auth data'},
|
||||||
|
},
|
||||||
|
0x22: {
|
||||||
|
'name': 'MANAGE SECURITY ENVIRONMENT',
|
||||||
|
'p1': {0x00: 'No indication', 0x01: 'Set', 0x02: 'Verify', 0x03: 'Restore', 0xF3: 'Erase'},
|
||||||
|
'p2': {'label': 'SE reference'},
|
||||||
|
'body': {'label': 'SE parameters'},
|
||||||
|
},
|
||||||
0x84: {
|
0x84: {
|
||||||
'name': 'GET CHALLENGE',
|
'name': 'GET CHALLENGE',
|
||||||
'le': True,
|
'le': True,
|
||||||
@@ -709,6 +787,29 @@ APDU_SPEC = {
|
|||||||
'p2': {0x00: 'No indication'},
|
'p2': {0x00: 'No indication'},
|
||||||
'body': {'label': 'File ID/path'},
|
'body': {'label': 'File ID/path'},
|
||||||
},
|
},
|
||||||
|
0x0E: {
|
||||||
|
'name': 'ERASE BINARY',
|
||||||
|
'p1p2': {'fmt': 'uint16be', 'label': 'Offset'},
|
||||||
|
'body': {'label': 'Erase data'},
|
||||||
|
},
|
||||||
|
0x0C: {
|
||||||
|
'name': 'ERASE RECORDS',
|
||||||
|
'p1': {'fmt': 'uint8', 'label': 'Record number'},
|
||||||
|
'p2': {
|
||||||
|
'label': 'Mode',
|
||||||
|
'bits': {
|
||||||
|
0x02: 'next record',
|
||||||
|
0x03: 'previous record',
|
||||||
|
0x04: 'absolute mode (record number in P1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'body': None,
|
||||||
|
},
|
||||||
|
0x7C: {
|
||||||
|
'name': 'MANAGE LSI',
|
||||||
|
'p1p2': {'unused': True},
|
||||||
|
'body': {'label': 'LSI parameters'},
|
||||||
|
},
|
||||||
0xF2: {
|
0xF2: {
|
||||||
'name': 'STATUS',
|
'name': 'STATUS',
|
||||||
'le': True,
|
'le': True,
|
||||||
@@ -1905,11 +2006,12 @@ def _decode_secured_packet(body):
|
|||||||
'spi': {
|
'spi': {
|
||||||
'hex': spi.hex().upper(),
|
'hex': spi.hex().upper(),
|
||||||
'counter': _OTA_COUNTER.get((spi1 >> 4) & 0x03, 'reserved'),
|
'counter': _OTA_COUNTER.get((spi1 >> 4) & 0x03, 'reserved'),
|
||||||
'ciphering': bool(spi1 & 0x08),
|
'ciphering': bool(spi1 & 0x04),
|
||||||
'rc_cc_ds': _OTA_RC_CC_DS.get(rc_cc_ds_kind, 'reserved'),
|
'rc_cc_ds': _OTA_RC_CC_DS.get(rc_cc_ds_kind, 'reserved'),
|
||||||
'por': _OTA_POR.get(spi2 & 0x03, 'reserved'),
|
'por': _OTA_POR.get(spi2 & 0x03, 'reserved'),
|
||||||
'por_rc_cc_ds': _OTA_RC_CC_DS.get((spi2 >> 2) & 0x03, 'reserved'),
|
'por_rc_cc_ds': _OTA_RC_CC_DS.get((spi2 >> 2) & 0x03, 'reserved'),
|
||||||
'por_ciphered': bool(spi2 & 0x20),
|
'por_ciphered': bool(spi2 & 0x10),
|
||||||
|
'por_transport': 'SMS-DELIVER-REPORT' if not (spi2 & 0x20) else 'SMS-SUBMIT',
|
||||||
},
|
},
|
||||||
'kic': {'key': kic >> 4, 'algo': _KIC_ALGO.get(kic & 0x0F, 'reserved')},
|
'kic': {'key': kic >> 4, 'algo': _KIC_ALGO.get(kic & 0x0F, 'reserved')},
|
||||||
'tar': tar.hex().upper(),
|
'tar': tar.hex().upper(),
|
||||||
@@ -1918,7 +2020,7 @@ def _decode_secured_packet(body):
|
|||||||
'data': data.hex().upper(),
|
'data': data.hex().upper(),
|
||||||
}
|
}
|
||||||
kic_algo = _KIC_ALGO.get(kic & 0x0F, 'reserved')
|
kic_algo = _KIC_ALGO.get(kic & 0x0F, 'reserved')
|
||||||
if not (spi1 & 0x08): # SPI says no ciphering → KIc unused
|
if not (spi1 & 0x04): # SPI says no ciphering → KIc unused
|
||||||
kic_algo += ' (unused)'
|
kic_algo += ' (unused)'
|
||||||
result['kic'] = {'key': kic >> 4, 'algo': kic_algo}
|
result['kic'] = {'key': kic >> 4, 'algo': kic_algo}
|
||||||
if rc_cc_ds_kind == 2: # CC
|
if rc_cc_ds_kind == 2: # CC
|
||||||
|
|||||||
@@ -1545,5 +1545,177 @@ class TestRefresh(unittest.TestCase):
|
|||||||
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
|
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSwUiccSpecific(unittest.TestCase):
|
||||||
|
"""TS 102 221 UICC-specific SWs (tables 10.7-10.15)."""
|
||||||
|
|
||||||
|
def _sw(self, hexstr):
|
||||||
|
from simtrace2_pysniff.server.decode import decode_sw
|
||||||
|
return decode_sw(bytes.fromhex(hexstr))
|
||||||
|
|
||||||
|
def test_91xx_proactive_pending(self):
|
||||||
|
self.assertEqual(self._sw('9107')['name'],
|
||||||
|
'Proactive command pending (7 bytes)')
|
||||||
|
self.assertEqual(self._sw('910b')['name'],
|
||||||
|
'Proactive command pending (11 bytes)')
|
||||||
|
|
||||||
|
def test_62f_more_data(self):
|
||||||
|
self.assertEqual(self._sw('62f1')['name'], 'More data available')
|
||||||
|
self.assertEqual(self._sw('62f2')['name'],
|
||||||
|
'More data available and proactive command pending')
|
||||||
|
self.assertEqual(self._sw('62f3')['name'], 'Response data available')
|
||||||
|
|
||||||
|
def test_63f_not_pin_failure(self):
|
||||||
|
# 63F1/F2 are "more data expected", NOT PIN retries
|
||||||
|
self.assertEqual(self._sw('63f1')['name'], 'More data expected')
|
||||||
|
self.assertEqual(self._sw('63f2')['name'],
|
||||||
|
'More data expected and proactive command pending')
|
||||||
|
|
||||||
|
def test_63cx_still_pin_retries(self):
|
||||||
|
self.assertIn('retries remaining', self._sw('63c3')['name'])
|
||||||
|
self.assertIn('Verification failed', self._sw('63c3')['name'])
|
||||||
|
|
||||||
|
def test_sat_busy_and_auth_errors(self):
|
||||||
|
self.assertEqual(self._sw('9300')['name'], 'SIM Application Toolkit busy')
|
||||||
|
self.assertEqual(self._sw('9850')['name'],
|
||||||
|
'INCREASE cannot be performed, max value reached')
|
||||||
|
self.assertEqual(self._sw('9862')['name'],
|
||||||
|
'Authentication error, application specific')
|
||||||
|
self.assertEqual(self._sw('9863')['name'],
|
||||||
|
'Security session or association expired')
|
||||||
|
self.assertEqual(self._sw('9864')['name'],
|
||||||
|
'Minimum UICC suspension time too long')
|
||||||
|
|
||||||
|
def test_6401_immediate_response(self):
|
||||||
|
self.assertEqual(self._sw('6401')['name'],
|
||||||
|
'Execution error — immediate response required')
|
||||||
|
|
||||||
|
def test_6500_nv_memory_changed(self):
|
||||||
|
# TS 102 221 table 10.10: 6500 = no info given, NV memory CHANGED
|
||||||
|
name = self._sw('6500')['name']
|
||||||
|
self.assertIn('NV memory changed', name)
|
||||||
|
self.assertNotEqual(name, 'Execution error — memory failure')
|
||||||
|
|
||||||
|
def test_existing_patterns_intact(self):
|
||||||
|
self.assertIn('GET RESPONSE', self._sw('6110')['name'])
|
||||||
|
self.assertIn('correct length is 0x38', self._sw('6c38')['name'])
|
||||||
|
self.assertIsNone(self._sw('6205')['name'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpiBits(unittest.TestCase):
|
||||||
|
"""TS 102 225 §5.1.1: SPI1 b3 = ciphering, SPI2 b5 = PoR ciphering."""
|
||||||
|
|
||||||
|
def _spi(self, spi_hex):
|
||||||
|
from simtrace2_pysniff.server.decode import _decode_secured_packet
|
||||||
|
# CPL(2) CHL(1)=0x0D SPI(2) KIc KID TAR(3) CNTR(5) PCNTR + 1 data byte
|
||||||
|
body = bytes.fromhex('000d0d' + spi_hex + '1505000001000000000000aa')
|
||||||
|
return _decode_secured_packet(body)['spi']
|
||||||
|
|
||||||
|
def test_spi1_ciphering_b3(self):
|
||||||
|
# 0x16 = 0001 0110: b3=1 → ciphering ON; old code checked b4 (0x08) → False
|
||||||
|
self.assertTrue(self._spi('1605')['ciphering'])
|
||||||
|
# 0x12 = 0001 0010: b3=0 → ciphering OFF
|
||||||
|
self.assertFalse(self._spi('1205')['ciphering'])
|
||||||
|
# 0x04 = 0000 0100: only b3 set → ciphering ON
|
||||||
|
self.assertTrue(self._spi('0405')['ciphering'])
|
||||||
|
|
||||||
|
def test_spi2_por_ciphered_b5(self):
|
||||||
|
# 0x10 = 0001 0000: b5=1 → PoR ciphered; old code checked b6 (0x20) → False
|
||||||
|
self.assertTrue(self._spi('0510')['por_ciphered'])
|
||||||
|
# 0x20 = 0010 0000: b6=0... wait, b5=0 → NOT ciphered
|
||||||
|
self.assertFalse(self._spi('0520')['por_ciphered'])
|
||||||
|
|
||||||
|
def test_por_transport_field(self):
|
||||||
|
# TS 31.115: SPI2 b6 (0x20) = PoR transport (0 = SMS-DELIVER-REPORT, 1 = SMS-SUBMIT)
|
||||||
|
self.assertEqual(self._spi('0500')['por_transport'], 'SMS-DELIVER-REPORT')
|
||||||
|
self.assertEqual(self._spi('0520')['por_transport'], 'SMS-SUBMIT')
|
||||||
|
|
||||||
|
|
||||||
|
class TestApduSpecCoverage(unittest.TestCase):
|
||||||
|
"""Newly added APDU_SPEC entries decode with names."""
|
||||||
|
|
||||||
|
def _ins(self, cla, ins, p1p2='0000', body='', le=''):
|
||||||
|
raw = bytes.fromhex(cla + ins + p1p2 + '%02x' % (len(body) // 2) + body + le)
|
||||||
|
return decode_message(raw)['ins_name']
|
||||||
|
|
||||||
|
def test_update_binary_odd_ins(self):
|
||||||
|
r = self._ins('00', 'd7', '0010', 'aabb')
|
||||||
|
self.assertEqual(r, 'UPDATE BINARY (odd INS)')
|
||||||
|
|
||||||
|
def test_update_record_odd_ins(self):
|
||||||
|
r = self._ins('00', 'dd', '0104', 'aabb')
|
||||||
|
self.assertEqual(r, 'UPDATE RECORD (odd INS)')
|
||||||
|
|
||||||
|
def test_write_record(self):
|
||||||
|
r = self._ins('00', 'd2', '0104', 'aabb')
|
||||||
|
self.assertEqual(r, 'WRITE RECORD')
|
||||||
|
|
||||||
|
def test_external_authenticate(self):
|
||||||
|
r = self._ins('00', '82', '0100', 'aabbccdd')
|
||||||
|
self.assertEqual(r, 'EXTERNAL AUTHENTICATE')
|
||||||
|
|
||||||
|
def test_general_authenticate(self):
|
||||||
|
r = self._ins('00', '86', '0000', '7c0aa50401020304')
|
||||||
|
self.assertEqual(r, 'GENERAL AUTHENTICATE')
|
||||||
|
|
||||||
|
def test_manage_security_environment(self):
|
||||||
|
r = self._ins('00', '22', '41a4', '')
|
||||||
|
self.assertEqual(r, 'MANAGE SECURITY ENVIRONMENT')
|
||||||
|
|
||||||
|
def test_erase_binary(self):
|
||||||
|
r = self._ins('00', '0e', '0010', '0020')
|
||||||
|
self.assertEqual(r, 'ERASE BINARY')
|
||||||
|
|
||||||
|
def test_erase_records(self):
|
||||||
|
r = self._ins('00', '0c', '0304', '')
|
||||||
|
self.assertEqual(r, 'ERASE RECORDS')
|
||||||
|
|
||||||
|
def test_manage_lsi(self):
|
||||||
|
r = self._ins('80', '7c', '0000', '0201')
|
||||||
|
self.assertEqual(r, 'MANAGE LSI')
|
||||||
|
|
||||||
|
|
||||||
|
class TestClaDecode(unittest.TestCase):
|
||||||
|
"""ISO 7816-4 §5.1.1 first/further interindustry CLA coding."""
|
||||||
|
|
||||||
|
def _cla(self, value):
|
||||||
|
from simtrace2_pysniff.server.decode import decode_cla
|
||||||
|
return decode_cla(value)
|
||||||
|
|
||||||
|
def test_basic_channel_iso(self):
|
||||||
|
r = self._cla(0x00)
|
||||||
|
self.assertEqual(r['channel'], 0)
|
||||||
|
self.assertEqual(r['secure_messaging'], 'none')
|
||||||
|
self.assertEqual(r['chain'], 'last or only')
|
||||||
|
|
||||||
|
def test_sm_header_authenticated(self):
|
||||||
|
# 0x1C = 0001 1100: SM bits b4-b3 = 11 → header authenticated
|
||||||
|
r = self._cla(0x1c)
|
||||||
|
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)
|
||||||
|
self.assertEqual(r['secure_messaging'], 'proprietary')
|
||||||
|
|
||||||
|
def test_chain_bit_b5(self):
|
||||||
|
# 0x20 = 0010 0000: b5=1 → first or continuing command of chain
|
||||||
|
r = self._cla(0x20)
|
||||||
|
self.assertEqual(r['chain'], 'first or continuing')
|
||||||
|
|
||||||
|
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_etsi_uicc(self):
|
||||||
|
r = self._cla(0x80)
|
||||||
|
self.assertEqual(r['interclass'], 'ETSI-defined (UICC/USIM)')
|
||||||
|
|
||||||
|
def test_etsi_sim(self):
|
||||||
|
r = self._cla(0xa0)
|
||||||
|
self.assertEqual(r['interclass'], 'ETSI-defined (SIM/GSM)')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '1.9.0')
|
self.assertEqual(__version__, '1.10.0')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user