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
+22 -53
View File
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.1.15'
VERSION = '2.1.16'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1380,12 +1380,14 @@ _SCP81_SCRIPTS = {
# The command sequence of the reference administration server
# (samples/HTTP_OTA/httpota_adminserver_php_v2, get_next_apdu), extended
# with the registries: GET DATA FF21 (extended card resources / free
# memory), GET STATUS P1=80 (Issuer Security Domain), GET DATA 0085,
# GET STATUS P1=40 (executable load files / ELF), GET STATUS P1=10
# (applications/applets); P2=02 with data '4F00' selects the TLV format,
# Le=00 so no GET RESPONSE is needed.
# memory), GET DATA 0085, then GET STATUS with P2=02 (TLV structure,
# 'first or all') and data '4F00' (match all): P1=80 (Issuer Security
# Domain), P1=40 (applications and supplementary security domains),
# P1=20 (executable load files), P1=10 (ELF and their modules);
# Le=00 so no GET RESPONSE is needed. Long listings answer SW CAFE and
# are auto-continued with the same command carrying P2.b1=1 ('next').
'explore': ['80CAFF2100', '80F28002024F0000', '80CA008500',
'80F24002024F0000', '80F21002024F0000'],
'80F24002024F0000', '80F22002024F0000', '80F21002024F0000'],
'none': [],
}
_SCP81_SCRIPT = list(_SCP81_SCRIPTS['explore'])
@@ -1508,51 +1510,19 @@ def _scp81_decode_memory(rapdu):
return out or None
def _scp81_last_aid(rapdu):
"""Last complete AID (the '4F' TLV of a GET STATUS entry) in a page.
The page is a stream of 'E3' entries; a 127-byte page may end mid-entry,
so only complete entries count. Unknown leading bytes (seen in live
pages) are skipped."""
last = None
i = 0
while i + 2 <= len(rapdu):
if rapdu[i] != 0xE3:
i += 1
continue
ln = rapdu[i + 1]
off = i + 2
if ln == 0x81 and i + 3 <= len(rapdu):
ln = rapdu[i + 2]
off = i + 3
if off + ln > len(rapdu):
break
content = rapdu[off:off + ln]
if len(content) >= 2 and content[0] == 0x4F:
alen = content[1]
if 2 + alen <= len(content):
last = content[2:2 + alen]
i = off + ln
return last
def _scp81_continuation(apdu, rapdu):
def _scp81_continuation(apdu):
"""Continuation APDU for a truncated GET STATUS page, or None.
GET STATUS P2=02 with the last returned AID as search criterion asks the
card for the next occurrence (GP GET STATUS, next-occurrence mode)."""
GET STATUS P2.b1 distinguishes first/all (0) from the *next* batch (1)
of the matches for the SAME search criteria; the pagination state lives
in the card, so the continuation is the same command with P2.b1 set.
Using a changed search criterion (the last returned AID) was rejected
with SW 6A80 - the criterion is a match filter, not a position."""
u = apdu.upper()
if not u.startswith('80F2'):
if not u.startswith('80F2') or len(u) < 8:
return None
aid = _scp81_last_aid(rapdu)
if not aid:
return None
lc = 2 + len(aid)
# P2=03 = "get next occurrence(s)" (Table 11-34); P2=02 ("first or all")
# made the card return the first listing again, so every continuation
# page repeated its search criterion and the scan stopped early - the
# newly installed package never appeared in the registry.
return '80F2%s03%02X4F%02X%s00' % (u[4:6], lc, len(aid), aid.hex().upper())
p2 = int(u[6:8], 16) | 0x01
return '%s%02X%s' % (u[:6], p2, u[8:])
def _scp81_script_responder(method, target, headers, body):
@@ -1579,12 +1549,11 @@ def _scp81_script_responder(method, target, headers, body):
decoded = _scp81_decode_memory(rapdus[-1][0])
if decoded:
_BIP.log('script-memory', **decoded)
if rapdus[-1][1].upper() == 'CAFE' and _SCP81_PAGES < SCP81_MAX_PAGES:
cont = _scp81_continuation(apdu, rapdus[-1][0])
if cont and cont in _SCP81_SCRIPT_INSERTED:
# The card returned the same page again: stop paging.
_BIP.log('script-page-stalled', index=index, apdu=cont)
elif cont:
# '63 10' = "more data available" (GP Table 11-38); the live
# card uses a proprietary 'CA FE' for the same condition.
if rapdus[-1][1].upper() in ('CAFE', '6310') and _SCP81_PAGES < SCP81_MAX_PAGES:
cont = _scp81_continuation(apdu)
if cont:
_SCP81_PAGES += 1
_SCP81_SCRIPT.insert(_SCP81_SCRIPT_SENT, cont)
_SCP81_SCRIPT_INSERTED.append(cont)