v3.24.0: the EF.ARR access rules in the FCP view

The file manager's decoded FCI shows the '8B' security attribute
reference (EF_ARR 2F06, record 23).  The referenced record's access
rules are now rendered as indented muted lines under that row, decoded
with the existing EF.ARR record decoder (AM_DO/SC_DO conditions).

The server reads the card's ARR copies (MF/2F06 and ADF.USIM/6F06,
best effort) once per card session in _apply_equipped_card and keeps
them as server.arr; GET /api/arr serves the cache without the card
lock, a disconnect clears it, and a write to an ARR copy through
/api/write refreshes it.  The PWA fetches it lazily per card session
(pysimArrEnsure, keyed by card_session) and resolves the reference with
the selected file's DF context: an ADF.USIM file uses the ADF's 6F06
copy, the implicit reference form too (ISO 7816-4 5.4.3.3 lookup
order).  Unresolvable references stay silent.

The '8B' decode now covers all three ISO 7816-4 Table 25 forms:
implicit ARR byte / FID + record / FID + (SEID, ARR) pairs - the pairs
form was mis-decoded as a record before.  A snapshot resolves the same
references offline from its own captured EF.ARR copy.

Tests: the three forms, the resolution (DF preference, out-of-range
silence), the snapshot path, the server read/cache/route/write-refresh.
868 frontend + 856 Python; sw cache simple-v423.
This commit is contained in:
2026-10-10 00:30:45 +03:00
parent 9b4114012b
commit b6537bd54b
10 changed files with 521 additions and 14 deletions
+90 -2
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.23.9'
VERSION = '3.24.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -485,6 +485,74 @@ def _read_iccid(app):
cleanup()
# EF.ARR copies referenced by the FCP security attribute DO '8B' (ISO 7816-4
# Table 25): the MF copy and the ADF.USIM copy (TS 102 221 13.4). The read
# runs once per card session (_refresh_arr_cache); the PWA's file manager
# resolves the referenced record's access rules against the cache.
ARR_READ_PATHS = ('MF/2F06', 'ADF.USIM/6F06')
ARR_RECORD_LIMIT = 64
def _arr_fid(fid):
"""True if the FID is an EF.ARR copy referenced by FCP '8B'."""
return str(fid or '').upper() in ('2F06', '6F06')
def _read_arr(app):
"""Best-effort EF.ARR reads (ARR_READ_PATHS): the access rule records the
FCP '8B' security attribute references point at. Returns a list of
{'fid','path','record_len','num_records','records'} dicts (records as hex,
1-based) or None; never raises."""
if not app or not getattr(app, 'rs', None):
return None
lchan = app.rs.lchan[0]
out = []
for path in ARR_READ_PATHS:
cleanup = None
try:
_, cleanup = _select_path(lchan, path, app)
if _get_file_type(lchan, lchan.selected_file) not in ('linear_fixed', 'cyclic'):
continue
n = min(lchan.selected_file_num_of_rec() or 1, ARR_RECORD_LIMIT)
records = []
for i in range(1, n + 1):
rv = lchan.read_record(i)
data = rv[0] if isinstance(rv, tuple) else rv
records.append((data or '').upper())
out.append({'fid': path.rsplit('/', 1)[-1].upper(),
'path': path,
'record_len': _fcp_value(lchan, 'selected_file_record_len'),
'num_records': len(records),
'records': records})
except Exception:
continue
finally:
if cleanup:
try:
cleanup()
except Exception:
pass
return out or None
def _refresh_arr_cache(server):
"""Read the EF.ARR copies into the session cache (best effort); a failed
read clears the previous session's cache."""
server.arr = _read_arr(getattr(server, 'app', None))
if server.arr:
_tlog('equip: EF.ARR ' + ', '.join(
'%s (%d rec)' % (a['fid'], a['num_records']) for a in server.arr))
def _arr_refresh_after_write(server, app, fid):
"""Re-read the cache after a write to an EF.ARR copy: the referenced
access rules may have changed. Returns True when a refresh happened."""
if not _arr_fid(fid):
return False
server.arr = _read_arr(app)
return True
_MCC_MNC_CACHE = {'path': None, 'data': None}
@@ -4361,6 +4429,7 @@ def _handle_card_disconnect(stale=False):
_server_ref.event_list = None
_server_ref.sim_menu = None
_server_ref.iccid = None
_server_ref.arr = None
_server_ref.net_state = None
_server_ref.equipping = False
_server_ref.card_session = getattr(_server_ref, 'card_session', 0) + 1
@@ -4435,6 +4504,7 @@ def _apply_equipped_card(server):
server.card_present = True
server.card_session = getattr(server, 'card_session', 0) + 1
server.iccid = None
server.arr = None
# Read the ICCID before the TERMINAL PROFILE starts a CAT session: the
# PWA auto-selects the matching card preset (SCP80 views) from it.
server.iccid = _read_iccid(server.app)
@@ -4444,6 +4514,9 @@ def _apply_equipped_card(server):
# ICCID (still before the TERMINAL PROFILE opens a CAT session). A
# card without a readable ICCID is considered unusable - give up.
_netstate_init(server)
# EF.ARR (FCP '8B' references): read once per card session - the
# file manager resolves the referenced record's rules from it.
_refresh_arr_cache(server)
else:
server.net_state = None
_tlog('equip: ICCID not readable - network state skipped')
@@ -5923,7 +5996,7 @@ _CARD_FREE_GET = frozenset([
'/api/stk-status', '/api/poll-status', '/api/proactive-log',
'/api/menu', '/api/events', '/api/terminal-profile',
'/api/pli-qualifiers', '/api/pli-dict', '/api/commands',
'/api/net-state', '/api/mcc-mnc', '/api/presets', '/api/test/scripts',
'/api/net-state', '/api/arr', '/api/mcc-mnc', '/api/presets', '/api/test/scripts',
'/api/test/suites',
'/api/scp81/status', '/api/bip/status', '/api/scp81/script',
'/api/scp81/log', '/api/bip/log',
@@ -7956,6 +8029,17 @@ class PysimHandler(BaseHTTPRequestHandler):
_netstate_compute(self.server)
self._send_json({'available': True, 'state': state})
self._log_resp({'available': True})
elif self.path == '/api/arr':
# EF.ARR session cache (v3.24.0): read once per card session in
# _refresh_arr_cache; the file manager resolves the FCP '8B'
# security attribute references against it. Cache-only - served
# without the card lock (see _CARD_FREE_GET).
self._log_req()
arrs = getattr(self.server, 'arr', None)
self._send_json({'ok': bool(arrs),
'session': getattr(self.server, 'card_session', 0),
'arrs': arrs or []})
self._log_resp({'ok': bool(arrs), 'arrs': len(arrs or [])})
elif self.path == '/api/status':
self._log_req()
app = self.server.app
@@ -8723,6 +8807,10 @@ class PysimHandler(BaseHTTPRequestHandler):
else:
sw = sw_match.group(1) if sw_match else '9000'
resp = {'success': True, 'sw': sw}
# An EF.ARR write changes the referenced access rules:
# refresh the session cache the file manager reads.
_arr_refresh_after_write(self.server, app,
getattr(lchan.selected_file, 'fid', None))
self._send_json(resp)
self._log_resp(resp)
except Exception as e: