Deepen AUTHENTICATE decode: P2 auth context + AUTN/AUTS decomposition

- Add a P2 spec for AUTHENTICATE (auth context bits + specific-reference-data
  flag), so P2 is displayed instead of only leaking through cmd.context.
- Fix P1 name to 'No indication' (was the misleading 'Run GSM algorithm').
- Decompose AUTN into SQN(xor)AK / AMF / MAC and AUTS into SQN(xor)AK / MAC-S
  per TS 33.102; surface 'specific reference data' key flag.
This commit is contained in:
catarrh committed 2026-08-16 21:44:57 +03:00
1 parent 35067f8d9d
commit 0fdaed503e
3 files changed
+66 -22

No files matched your search

+14 -5
View File
@@ -454,7 +454,7 @@ function cmd_description(m) {
function sw_text(m) { function sw_text(m) {
const d = m.decoded; const d = m.decoded;
if (!d || !d.sw) return ''; if (!d || !d.sw) return '';
return d.sw.sw1 + ' ' + d.sw.sw2; return (d.sw.sw1 + ' ' + d.sw.sw2).toUpperCase();
} }
function sw_class(m) { function sw_class(m) {
@@ -536,7 +536,11 @@ function responseRows(resp) {
if (resp.ck) rows.push(['CK', resp.ck]); if (resp.ck) rows.push(['CK', resp.ck]);
if (resp.ik) rows.push(['IK', resp.ik]); if (resp.ik) rows.push(['IK', resp.ik]);
if (resp.kc) rows.push(['Kc', resp.kc]); if (resp.kc) rows.push(['Kc', resp.kc]);
if (resp.auts) rows.push(['AUTS', resp.auts]); if (resp.auts) {
rows.push(['AUTS', resp.auts]);
if (resp.sqn_ak) rows.push(['SQN⊕AK', resp.sqn_ak + ' (concealed)']);
if (resp.mac_s) rows.push(['MAC-S', resp.mac_s]);
}
} }
if (resp.code !== undefined) { if (resp.code !== undefined) {
@@ -564,10 +568,15 @@ function cmdRows(cmd) {
const rows = []; const rows = [];
// AUTHENTICATE command body // AUTHENTICATE command body
if (cmd.context !== undefined) { if (cmd.rand !== undefined || cmd.autn !== undefined || cmd.specific_key) {
rows.push(['Auth context', cmd.context]); if (cmd.specific_key) rows.push(['Key', 'specific reference data']);
if (cmd.rand) rows.push(['RAND', cmd.rand]); if (cmd.rand) rows.push(['RAND', cmd.rand]);
if (cmd.autn) rows.push(['AUTN', cmd.autn]); if (cmd.autn) {
rows.push(['AUTN', cmd.autn]);
if (cmd.sqn_ak) rows.push(['SQN⊕AK', cmd.sqn_ak + ' (concealed)']);
if (cmd.amf) rows.push(['AMF', cmd.amf]);
if (cmd.mac) rows.push(['MAC', cmd.mac]);
}
} }
// ENVELOPE fields // ENVELOPE fields
+28 -15
View File
@@ -552,7 +552,14 @@ APDU_SPEC = {
}, },
0x88: { 0x88: {
'name': 'AUTHENTICATE', 'name': 'AUTHENTICATE',
'p1': {0x00: 'Run GSM algorithm', 0x80: 'Run 3G algo (resynchronisation)'}, 'p1': {0x00: 'No indication'},
'p2': {'label': 'Auth context', 'bits': {
0x00: 'GSM context',
0x01: '3G/EPS/5G context',
0x02: 'VGCS/VBS context',
0x04: 'GBA context',
0x80: 'Specific reference data',
}},
'body': {'label': 'Challenge/session key'}, 'body': {'label': 'Challenge/session key'},
}, },
0x89: { 0x89: {
@@ -1127,9 +1134,15 @@ def _decode_auth_3g(data):
if tag == 0xDC: # synchronisation failure → length byte + AUTS if tag == 0xDC: # synchronisation failure → length byte + AUTS
if len(data) >= 2: if len(data) >= 2:
auts_len = data[1] auts_len = data[1]
return {'type': '3G', 'status': 'sync fail', auts = data[2:2 + auts_len]
'auts': data[2:2 + auts_len].hex().upper()} else:
return {'type': '3G', 'status': 'sync fail', 'auts': data[1:].hex().upper()} auts = data[1:]
result = {'type': '3G', 'status': 'sync fail', 'auts': auts.hex().upper()}
# AUTS = SQN_MS⊕AK (6) || MAC-S (8) (TS 33.102)
if len(auts) >= 14:
result['sqn_ak'] = auts[0:6].hex().upper()
result['mac_s'] = auts[6:14].hex().upper()
return result
# tag 0xDB: success → length-prefixed RES, CK, IK, (KC) # tag 0xDB: success → length-prefixed RES, CK, IK, (KC)
result = {'type': '3G', 'status': 'success'} result = {'type': '3G', 'status': 'success'}
i = 1 i = 1
@@ -1170,18 +1183,12 @@ def _decode_response_for(ins, data):
# ──────────────────── Command-body decoders ──────────────────── # ──────────────────── Command-body decoders ────────────────────
AUTH_CONTEXTS = {
0: 'GSM',
1: '3G (UMTS)',
2: 'VGC/VBS',
4: 'GBA',
}
def _decode_auth_cmd(data, p2): def _decode_auth_cmd(data, p2):
"""Decode an AUTHENTICATE command body (RAND / RAND+AUTN).""" """Decode an AUTHENTICATE command body (RAND / RAND+AUTN)."""
ctx = p2 & 0x07 ctx = p2 & 0x07
result = {'context': AUTH_CONTEXTS.get(ctx, f'unknown ({ctx})')} result = {}
if p2 & 0x80:
result['specific_key'] = True
if ctx in (0, 1) and data: if ctx in (0, 1) and data:
i = 0 i = 0
if i < len(data): if i < len(data):
@@ -1189,10 +1196,16 @@ def _decode_auth_cmd(data, p2):
i += 1 i += 1
result['rand'] = data[i:i + rlen].hex().upper() result['rand'] = data[i:i + rlen].hex().upper()
i += rlen i += rlen
if ctx == 1 and i < len(data): # 3G → AUTN if ctx == 1 and i < len(data): # 3G/EPS/5G → AUTN
alen = data[i] alen = data[i]
i += 1 i += 1
result['autn'] = data[i:i + alen].hex().upper() autn = data[i:i + alen]
result['autn'] = autn.hex().upper()
# AUTN = SQN⊕AK (6) || AMF (2) || MAC-A (8) (TS 33.102)
if len(autn) >= 16:
result['sqn_ak'] = autn[0:6].hex().upper()
result['amf'] = autn[6:8].hex().upper()
result['mac'] = autn[8:16].hex().upper()
return result return result
+24 -2
View File
@@ -361,17 +361,39 @@ class TestAuthCommand(unittest.TestCase):
r = decode_message(bytes.fromhex( r = decode_message(bytes.fromhex(
'0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135')) '0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135'))
cmd = r['cmd'] cmd = r['cmd']
self.assertEqual(cmd['context'], '3G (UMTS)') self.assertNotIn('context', cmd)
self.assertEqual(cmd['specific_key'], True)
self.assertEqual(cmd['rand'], '4A75BA425D438C549EF35BA5E3DD5305') self.assertEqual(cmd['rand'], '4A75BA425D438C549EF35BA5E3DD5305')
self.assertEqual(cmd['autn'], '65E04D80E4DC8000A5C7FD842F41395E') self.assertEqual(cmd['autn'], '65E04D80E4DC8000A5C7FD842F41395E')
self.assertEqual(cmd['sqn_ak'], '65E04D80E4DC')
self.assertEqual(cmd['amf'], '8000')
self.assertEqual(cmd['mac'], 'A5C7FD842F41395E')
def test_auth_p2_context(self):
r = decode_message(bytes.fromhex(
'0088008122104A75BA425D438C549EF35BA5E3DD53051065E04D80E4DC8000A5C7FD842F41395E6135'))
p2 = r['p2']
self.assertEqual(p2['raw'], '81')
self.assertIn('3G/EPS/5G context', p2['bits'])
self.assertIn('Specific reference data', p2['bits'])
def test_gsm_rand_only(self): def test_gsm_rand_only(self):
from simtrace2_pysniff.server.decode import _decode_auth_cmd from simtrace2_pysniff.server.decode import _decode_auth_cmd
r = _decode_auth_cmd(bytes.fromhex('10' + '00' * 16), 0x00) r = _decode_auth_cmd(bytes.fromhex('10' + '00' * 16), 0x00)
self.assertEqual(r['context'], 'GSM') self.assertNotIn('context', r)
self.assertNotIn('specific_key', r)
self.assertEqual(r['rand'], '00' * 16) self.assertEqual(r['rand'], '00' * 16)
self.assertNotIn('autn', r) self.assertNotIn('autn', r)
def test_sync_fail_auts(self):
from simtrace2_pysniff.server.decode import _decode_auth
auts = bytes.fromhex('010203040506' + '0a0b0c0d0e0f1011')
r = _decode_auth(b'\xdc' + bytes([len(auts)]) + auts)
self.assertEqual(r['status'], 'sync fail')
self.assertEqual(r['auts'], auts.hex().upper())
self.assertEqual(r['sqn_ak'], '010203040506')
self.assertEqual(r['mac_s'], '0A0B0C0D0E0F1011')
class TestEventDownload(unittest.TestCase): class TestEventDownload(unittest.TestCase):
def test_location_status(self): def test_location_status(self):