feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle> <privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked against GlobalPlatformPro's populate_legacy (the 4th byte is privileges; 0x00 is required for load files). The parser read it as a 'version' one byte late, so ELF rows showed the module count / the next entry's length as Version: 00/10 - the compact listing has no version at all; it now comes from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table 11-37 Note 1) and merges (the bogus compact value no longer blocked it). New data exposed: - EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled) - unknown registry objects are preserved (rawTlvs) and shown as 'Other registry data: 8F=752B . 90=012D ...' instead of being dropped - 84 parses the GP coding (one Executable Module AID per TLV, repeated; the concatenated 4F<len><aid> form stays as fallback) and the ELF expanded queries add 84 to the tag list (with a tag list the card returns only the listed tags - the fallback path would otherwise lose the module lists and their Install buttons) - life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card, app, SD - detected via the Security Domain privilege - and ELF), with multi-byte 9F70 (02 0700 on live cards) decoding the first byte and surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were aligned with the same tables /api/ram-install measures the NV footprint: a silent GET DATA FF21 read before and after the chain (also on failure) returns nv_before/nv_after/ nv_delta; the reads never fail the operation (no FF21 -> no fields) and the PWA shows 'NV used: ~N B (free A -> B)' in the install result. sw cache simple-v324; tests: 548 Python / 698 Node.
This commit is contained in:
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.6.50'
|
||||
VERSION = '3.6.51'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1479,13 +1479,18 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
||||
return step, error
|
||||
|
||||
|
||||
def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex, silent=False):
|
||||
"""Send one GP APDU as an SCP80 secured packet and append its step result
|
||||
to `state['steps']`. `sp` carries the SCP80 parameters (spi1, spi2, kic,
|
||||
kid, tar, kic_key, kid_key, include_cpi); `state` carries the mutable run
|
||||
state ({'steps', 'encode_error', 'failure', 'cntr'}) and the counter is
|
||||
advanced only for a packet the card accepted. Returns True on success
|
||||
(the step record is the last element of state['steps'])."""
|
||||
(the step record is the last element of state['steps']).
|
||||
|
||||
`silent=True` (the NV footprint reads) leaves no step record and no
|
||||
failure note behind: the card's answer is only used by the caller, and a
|
||||
rejected read must never fail the operation. The decoded step record is
|
||||
still available as `state['last_step']`."""
|
||||
spi1, spi2 = sp['spi1'], sp['spi2']
|
||||
try:
|
||||
sp_hex, _ = _build_secured_packet(spi1, spi2, sp['kic'], sp['kid'], sp['tar'],
|
||||
@@ -1496,9 +1501,10 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
sys.stderr.write('RAM C-APDU (%s): %s\n' % (step_name, apdu_hex))
|
||||
sys.stderr.write('RAM SECURED-PACKET (%s): %s\n' % (step_name, sp_hex))
|
||||
except ValueError as e:
|
||||
state['encode_error'] = str(e)
|
||||
state['steps'].append({'name': step_name, 'por_status': 'encode_error',
|
||||
'sw': str(e)})
|
||||
if not silent:
|
||||
state['encode_error'] = str(e)
|
||||
state['steps'].append({'name': step_name, 'por_status': 'encode_error',
|
||||
'sw': str(e)})
|
||||
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
||||
return False
|
||||
# Capture the PoR from either transport: inline in the ENVELOPE response
|
||||
@@ -1516,10 +1522,11 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
scc, sp_hex, oa_number=server.sms_oa, sm_sc=server.sms_sc,
|
||||
include_cpi=sp['include_cpi'], submit_handler=submit_handler)
|
||||
if not result['success']:
|
||||
state['steps'].append({'name': step_name, 'por_status': 'envelope_error',
|
||||
'sw': result.get('sw') or result.get('error'),
|
||||
'bytes': result.get('bytes'),
|
||||
'segments': result.get('segments')})
|
||||
if not silent:
|
||||
state['steps'].append({'name': step_name, 'por_status': 'envelope_error',
|
||||
'sw': result.get('sw') or result.get('error'),
|
||||
'bytes': result.get('bytes'),
|
||||
'segments': result.get('segments')})
|
||||
sys.stderr.write('RAM-INSTALL: %s send failed: %s\n' % (
|
||||
step_name, result.get('error')))
|
||||
return False
|
||||
@@ -1541,7 +1548,9 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
sp['kic_key'], sp['kid_key'], por_hex)
|
||||
step, step_error = _ram_step_result(step_name, last_sw, por, por_hex,
|
||||
result['bytes'], result['segments'])
|
||||
state['steps'].append(step)
|
||||
state['last_step'] = step
|
||||
if not silent:
|
||||
state['steps'].append(step)
|
||||
sys.stderr.write('RAM-INSTALL: %s PoR[%s] status=%s remote_sw=%s%s (%d B, %d SM)\n' % (
|
||||
step_name, por_src, step.get('por_status', '?'), step.get('por_sw', '-'),
|
||||
(' error=%s' % step_error) if step_error else '',
|
||||
@@ -1550,7 +1559,8 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
state['cntr'] = _ram_next_cntr(
|
||||
state['cntr'], step.get('por_status') in ('por_ok', 'no_por'))
|
||||
if step_error:
|
||||
state['failure']['error'] = '%s: %s' % (step_name, step_error)
|
||||
if not silent:
|
||||
state['failure']['error'] = '%s: %s' % (step_name, step_error)
|
||||
return False
|
||||
return True
|
||||
finally:
|
||||
@@ -1558,6 +1568,41 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||
scc._tp.proactive_handler = old_proactive
|
||||
|
||||
|
||||
def _ram_free_nv(por_data_hex):
|
||||
"""Free non-volatile memory (bytes) from a GET DATA FF21 R-APDU, or None.
|
||||
Reuses the FF21 decoder (TS 102 226 8.2.1.7.2: '81' applet count /
|
||||
'82' free NV / '83' free volatile)."""
|
||||
try:
|
||||
rapdu = bytes.fromhex(por_data_hex or '')
|
||||
except ValueError:
|
||||
return None
|
||||
mem = _scp81_decode_memory(rapdu)
|
||||
return mem.get('free_nv') if mem else None
|
||||
|
||||
|
||||
def _ram_read_ff21(server, scc, sp, state, ram_format, label):
|
||||
"""Best-effort GET DATA FF21 (Extended Card Resources) read around an
|
||||
install: returns the free non-volatile memory in bytes, or None (a card
|
||||
without FF21 or a rejected read must never fail the operation). The read
|
||||
is sent through the normal step sender with `silent=True`, so it consumes
|
||||
and advances the counter correctly but leaves no step record or failure
|
||||
note behind."""
|
||||
apdu = _ram_format_apdu('80CAFF2100', ram_format)
|
||||
if not _ram_send_gp_apdu(server, scc, sp, state, label, apdu, silent=True):
|
||||
return None
|
||||
step = state.get('last_step') or {}
|
||||
return _ram_free_nv(step.get('por_data'))
|
||||
|
||||
|
||||
def _ram_nv_fields(nv_before, nv_after):
|
||||
"""NV footprint fields for the install responses: the free-NV values and
|
||||
their delta (None when the card has no FF21 readout)."""
|
||||
if nv_before is None and nv_after is None:
|
||||
return {}
|
||||
delta = (nv_before - nv_after) if (nv_before is not None and nv_after is not None) else None
|
||||
return {'nv_before': nv_before, 'nv_after': nv_after, 'nv_delta': delta}
|
||||
|
||||
|
||||
def _parse_response_scripting(data):
|
||||
"""Parse a Response Scripting template (TS 102 226 5.2.2, tables
|
||||
5.10/5.10a): `AB <len>` (definite) or `AF 80 ... 00 00` (indefinite),
|
||||
@@ -6765,6 +6810,8 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
# Live progress for the PWA's operation modal (the request holds
|
||||
# _CARD_LOCK until the chain is done; /api/status carries this)
|
||||
_ram_progress_begin('install-cap', len(seq) + 1)
|
||||
nv_before = None
|
||||
nv_after = None
|
||||
try:
|
||||
# RAM command format: detected per operation (a read-only
|
||||
# probe step) unless the caller pinned compact/expanded.
|
||||
@@ -6774,6 +6821,10 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, ram_format, loadfile_aid))
|
||||
# NV footprint: free non-volatile memory before the chain
|
||||
# (best effort - a card without FF21 skips it).
|
||||
nv_before = _ram_read_ff21(self.server, scc, sp_state, state,
|
||||
ram_format, 'NV before')
|
||||
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
if apdu_idx == 0:
|
||||
@@ -6786,6 +6837,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
||||
step_name,
|
||||
_ram_format_apdu(gp_apdu, ram_format)):
|
||||
# What the failed attempt consumed (a rejected load
|
||||
# may keep or discard its partial file - the delta
|
||||
# tells which).
|
||||
nv_after = _ram_read_ff21(self.server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name)),
|
||||
@@ -6794,9 +6850,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
# NV footprint: what the successful chain consumed.
|
||||
nv_after = _ram_read_ff21(self.server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
|
||||
@@ -6805,6 +6865,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'ram_format': ram_format, 'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
||||
loadfile_aid, module_aid, state['cntr']))
|
||||
self._send_json(resp)
|
||||
|
||||
Reference in New Issue
Block a user