scp81: correct GET STATUS pagination and registry P1s (v2.1.16)

- continuation repeats the SAME GET STATUS command with P2.b1 set (the
  pagination state lives in the card); changing the 4F criterion is a match
  filter, not a position - P2=03 with the last AID is rejected with 6A80 and
  P2=02 with it returns that single match (the earlier duplicate)
- handle the standard "more data available" warning SW 63 10 (Table 11-38)
  in addition to the live card's proprietary CA FE
- explore script: P1=40 is applications+SDs, P1=20 the ELF registry, P1=10
  ELF+modules (Table 11-33) - the ELF-only registry was never queried, which
  hid the installed package; labels and the results decoder show C4 (ELF AID)
  and CC (SD AID) too
- UICC_SPECS.md: GET STATUS P1/P2 tables made explicit with the pagination
  rule, plus BER length coding notes for the scripting templates and the
  TS 102 223 channel data TLV (the two >127-byte traps)

201 python + 346 frontend; service worker v153
This commit is contained in:
2026-09-16 08:22:59 +03:00
parent 3403abd6b9
commit c341571300
7 changed files with 94 additions and 95 deletions
+43 -30
View File
@@ -667,20 +667,13 @@ class TargetedAppTest(unittest.TestCase):
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
def test_last_aid_parses_complete_entries_only(self):
page = bytes.fromhex(
'FC' # live prefix
'E3114F08A0000000030000009F70010FC50100' # entry 1
'E3104F07A00000015153509F700107C50104' # entry 2
'E3204F08D27600') # truncated
self.assertEqual(server._scp81_last_aid(page),
bytes.fromhex('A0000001515350'))
def test_continuation_builds_next_occurrence_apdu(self):
page = bytes.fromhex('E3114F08A0000000030000009F70010FC50100')
self.assertEqual(server._scp81_continuation('80F24002024F0000', page),
'80F240030A4F08A00000000300000000')
self.assertIsNone(server._scp81_continuation('80CAFF2100', page))
def test_continuation_sets_p2_next_bit(self):
# P2.b1: 0 = first/all, 1 = next batch of the SAME search criteria
self.assertEqual(server._scp81_continuation('80F24002024F0000'),
'80F24003024F0000')
self.assertEqual(server._scp81_continuation('80F21002024F0000'),
'80F21003024F0000')
self.assertIsNone(server._scp81_continuation('80CAFF2100'))
def test_cafe_page_auto_continuation(self):
server._SCP81_SCRIPT = ['80F24002024F0000']
@@ -689,18 +682,15 @@ class TargetedAppTest(unittest.TestCase):
server._SCP81_SCRIPT_INSERTED = []
server._SCP81_PAGES = 0
try:
page = (bytes.fromhex('E3114F08A0000000030000009F70010FC50100')
+ bytes.fromhex('E3104F07A00000015153509F700107C50104')
+ bytes.fromhex('E3204F08D27600'))
page = bytes.fromhex('E3114F08A0000000030000009F70010FC50100')
tlv = bytes([0x23, len(page) + 2]) + page + b'\xCA\xFE'
body = b'\xAF\x80' + tlv + b'\x00\x00'
status, headers, out = server._scp81_script_responder(
'POST', '/api/scp81?req=1', {'x-admin-script-status': 'ok'}, body)
# The continuation was appended and sent as the next command.
self.assertEqual(server._SCP81_SCRIPT[1],
'80F24003094F07A000000151535000')
self.assertEqual(server._SCP81_SCRIPT[1], '80F24003024F0000')
self.assertEqual(status, 200)
self.assertIn(bytes.fromhex('80F24003094F07A000000151535000'), out)
self.assertIn(bytes.fromhex('80F24003024F0000'), out)
finally:
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
@@ -708,21 +698,44 @@ class TargetedAppTest(unittest.TestCase):
server._SCP81_SCRIPT_INSERTED = []
server._SCP81_PAGES = 0
def test_repeated_page_stalls(self):
def test_standard_more_data_sw_also_pages(self):
# GP Table 11-38: SW '63 10' = more data available, continue with
# GET STATUS [next occurrence] - same handling as the card's 'CA FE'.
server._SCP81_SCRIPT = ['80F2 4002 024F 0000'.replace(' ', '')]
server._SCP81_SCRIPT_SENT = 1
server._SCP81_SCRIPT_RESULTS = []
server._SCP81_SCRIPT_INSERTED = []
server._SCP81_PAGES = 0
try:
page = bytes.fromhex('E3114F08A0000000030000009F70010FC50100')
tlv = bytes([0x23, len(page) + 2]) + page + b'\x63\x10'
body = b'\xAF\x80' + tlv + b'\x00\x00'
server._scp81_script_responder(
'POST', '/api/scp81?req=1', {'x-admin-script-status': 'ok'}, body)
self.assertEqual(server._SCP81_SCRIPT[1], '80F24003024F0000')
finally:
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
server._SCP81_SCRIPT_RESULTS = []
server._SCP81_SCRIPT_INSERTED = []
server._SCP81_PAGES = 0
def test_repeated_pages_keep_paging(self):
# The continuation is stateful (P2=03): the same APDU legitimately
# repeats until the card answers 9000; the page counter caps it.
server._SCP81_SCRIPT = ['80F24002024F0000']
server._SCP81_SCRIPT_SENT = 1
server._SCP81_SCRIPT_RESULTS = []
server._SCP81_SCRIPT_INSERTED = ['80F240030A4F08A00000000300000000']
server._SCP81_PAGES = 1
server._SCP81_SCRIPT_INSERTED = []
server._SCP81_PAGES = 0
try:
page = bytes.fromhex('E3114F08A0000000030000009F70010FC50100')
tlv = bytes([0x23, len(page) + 2]) + page + b'\xCA\xFE'
body = b'\xAF\x80' + tlv + b'\x00\x00'
server._scp81_script_responder(
'POST', '/api/scp81?req=2', {'x-admin-script-status': 'ok'}, body)
# Same page again: no new continuation inserted.
self.assertEqual(_count := len(server._SCP81_SCRIPT_INSERTED), 1)
self.assertEqual(server._SCP81_PAGES, 1)
for _ in range(3):
server._scp81_script_responder(
'POST', '/api/scp81?req=2', {'x-admin-script-status': 'ok'}, body)
self.assertEqual(server._SCP81_PAGES, 3)
finally:
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
@@ -731,8 +744,8 @@ class TargetedAppTest(unittest.TestCase):
server._SCP81_PAGES = 0
def test_new_session_drops_inserted_pages(self):
server._SCP81_SCRIPT = ['80F24002024F0000', '80F24002094F07A000000151535000']
server._SCP81_SCRIPT_INSERTED = ['80F24002094F07A000000151535000']
server._SCP81_SCRIPT = ['80F24002024F0000', '80F24003024F0000']
server._SCP81_SCRIPT_INSERTED = ['80F24003024F0000']
server._SCP81_SCRIPT_SENT = 2
try:
server._scp81_script_responder('POST', '/api/scp81', {}, b'')