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:
@@ -16,6 +16,7 @@ import threading
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from unittest import mock
|
||||
|
||||
from pysim_simple_server.__main__ import _build_http_server
|
||||
from pysim_simple_server.server import PysimHandler, _CARD_FREE_GET, _CARD_LOCK
|
||||
@@ -222,5 +223,91 @@ class CapCompatHttpTests(unittest.TestCase):
|
||||
self.assertIn('import probe', resp['error'])
|
||||
|
||||
|
||||
class RamInstallNvFootprintHttpTests(unittest.TestCase):
|
||||
"""The RAM install response carries the measured NV footprint (v3.6.51):
|
||||
the server reads GET DATA FF21 before and after the chain (best effort)
|
||||
and reports the free-NV values and their delta."""
|
||||
|
||||
def setUp(self):
|
||||
from pysim_simple_server import server as srv
|
||||
self.srv = srv
|
||||
self.server = _build_http_server('127.0.0.1', 0, PysimHandler)
|
||||
self.server.log_requests = False
|
||||
self.server.app = None
|
||||
self.server.sl = None
|
||||
self.server.scc = object()
|
||||
self.server.sms_oa = '12345'
|
||||
self.server.sms_sc = '12345678912'
|
||||
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
||||
self.port = self.server.server_address[1]
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
def _post(self, path, body):
|
||||
req = urllib.request.Request(
|
||||
'http://127.0.0.1:%d%s' % (self.port, path),
|
||||
data=json.dumps(body).encode(),
|
||||
headers={'Content-Type': 'application/json'}, method='POST')
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as res:
|
||||
return res.status, json.loads(res.read() or b'{}')
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b'{}')
|
||||
|
||||
def _install(self, fail_at=None, nv_before=50000, nv_after=20000):
|
||||
def fake_send(server, scc, sp, state, name, apdu, silent=False):
|
||||
if fail_at == name:
|
||||
state['steps'].append({'name': name, 'por_status': 'por_error', 'por_sw': '6A88'})
|
||||
return False
|
||||
state['steps'].append({'name': name, 'por_status': 'por_ok', 'por_sw': '9000'})
|
||||
return True
|
||||
|
||||
def fake_read(server, scc, sp, state, ram_format, label):
|
||||
return nv_before if label == 'NV before' else nv_after
|
||||
|
||||
patches = [
|
||||
mock.patch.object(self.srv, '_cap_parse',
|
||||
lambda cap_hex: ('F0414C46416101', 'F0414C4641610101', b'\x01\x02')),
|
||||
mock.patch.object(self.srv, '_cap_apdu_sequence',
|
||||
lambda *a, **k: ['80E60200', '80E80000', '80E60000']),
|
||||
mock.patch.object(self.srv, '_ram_detect_format', lambda *a, **k: 'compact'),
|
||||
mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_send),
|
||||
mock.patch.object(self.srv, '_ram_read_ff21', fake_read),
|
||||
]
|
||||
for p in patches:
|
||||
p.start()
|
||||
try:
|
||||
return self._post('/api/ram-install', {'cap_hex': 'AA'})
|
||||
finally:
|
||||
for p in reversed(patches):
|
||||
p.stop()
|
||||
|
||||
def test_success_reports_the_nv_delta(self):
|
||||
status, resp = self._install()
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertTrue(resp['success'], resp)
|
||||
self.assertEqual(resp['nv_before'], 50000)
|
||||
self.assertEqual(resp['nv_after'], 20000)
|
||||
self.assertEqual(resp['nv_delta'], 30000)
|
||||
|
||||
def test_failed_install_still_reports_the_nv_delta(self):
|
||||
status, resp = self._install(fail_at='LOAD (1/1)')
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertFalse(resp['success'], resp)
|
||||
self.assertEqual(resp['failed_step'], 2)
|
||||
self.assertEqual(resp['nv_before'], 50000)
|
||||
self.assertEqual(resp['nv_after'], 20000)
|
||||
self.assertEqual(resp['nv_delta'], 30000)
|
||||
|
||||
def test_a_card_without_ff21_has_no_nv_fields(self):
|
||||
status, resp = self._install(nv_before=None, nv_after=None)
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertTrue(resp['success'], resp)
|
||||
for key in ('nv_before', 'nv_after', 'nv_delta'):
|
||||
self.assertNotIn(key, resp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -37,6 +37,9 @@ from pysim_simple_server.server import (
|
||||
_por_remote_sw,
|
||||
_ram_next_cntr,
|
||||
_ram_format_apdu,
|
||||
_ram_free_nv,
|
||||
_ram_read_ff21,
|
||||
_ram_nv_fields,
|
||||
_wrap_expanded_apdu,
|
||||
_ram_remote_sw_ok,
|
||||
_sms_submit_por,
|
||||
@@ -1585,6 +1588,90 @@ class RamSendGpApduLoggingTest(unittest.TestCase):
|
||||
self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)
|
||||
|
||||
|
||||
class RamNvFootprintTests(unittest.TestCase):
|
||||
"""GET DATA FF21 reads around an install (v3.6.51): the free-NV values and
|
||||
their delta. The reads are best-effort - a card without FF21 (or a
|
||||
rejected read) must never fail the operation."""
|
||||
|
||||
def test_free_nv_from_ff21_response(self):
|
||||
# TS 102 226 8.2.1.7.2: '81' applet count, '82' free NV, '83' free
|
||||
# volatile (the length byte follows FF21).
|
||||
data = 'FF210C' + '810102' + '820300C5D6' + '83020064'
|
||||
self.assertEqual(_ram_free_nv(data), 0xC5D6)
|
||||
# CR-set tag variants (0x01/0x02/0x03) decode the same
|
||||
self.assertEqual(_ram_free_nv('FF210C' + '010102' + '020300C5D6' + '03020064'), 0xC5D6)
|
||||
# not an FF21 response / empty / garbage
|
||||
self.assertIsNone(_ram_free_nv('6A88'))
|
||||
self.assertIsNone(_ram_free_nv(''))
|
||||
self.assertIsNone(_ram_free_nv('not-hex'))
|
||||
|
||||
def test_nv_fields(self):
|
||||
self.assertEqual(_ram_nv_fields(None, None), {})
|
||||
self.assertEqual(_ram_nv_fields(50000, 20000),
|
||||
{'nv_before': 50000, 'nv_after': 20000, 'nv_delta': 30000})
|
||||
# a freed delta is negative (memory released by the operation)
|
||||
self.assertEqual(_ram_nv_fields(100, 150),
|
||||
{'nv_before': 100, 'nv_after': 150, 'nv_delta': -50})
|
||||
# only one read available: the delta stays None
|
||||
self.assertEqual(_ram_nv_fields(100, None),
|
||||
{'nv_before': 100, 'nv_after': None, 'nv_delta': None})
|
||||
|
||||
def test_read_ff21_uses_the_silent_sender(self):
|
||||
from pysim_simple_server import server as srv
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_send(server, scc, sp, state, name, apdu, silent=False):
|
||||
calls.append((name, apdu, silent))
|
||||
if not silent:
|
||||
state['steps'].append({'name': name})
|
||||
state['last_step'] = {'por_data': 'FF210C810102820300C5D683020064'}
|
||||
return True
|
||||
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
|
||||
with mock.patch.object(srv, '_ram_send_gp_apdu', fake_send):
|
||||
nv = _ram_read_ff21(None, None, {}, state, 'expanded', 'NV before')
|
||||
self.assertEqual(nv, 0xC5D6)
|
||||
self.assertEqual(calls, [('NV before', _wrap_expanded_apdu('80CAFF2100'), True)])
|
||||
self.assertEqual(state['steps'], [])
|
||||
self.assertEqual(state['failure'], {})
|
||||
|
||||
def test_read_ff21_returns_none_when_the_read_fails(self):
|
||||
from pysim_simple_server import server as srv
|
||||
|
||||
def fake_send(server, scc, sp, state, name, apdu, silent=False):
|
||||
return False
|
||||
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
|
||||
with mock.patch.object(srv, '_ram_send_gp_apdu', fake_send):
|
||||
self.assertIsNone(_ram_read_ff21(None, None, {}, state, 'compact', 'NV after'))
|
||||
|
||||
def test_silent_send_leaves_no_step_or_failure(self):
|
||||
import contextlib
|
||||
import io
|
||||
from pysim_simple_server import server as srv
|
||||
|
||||
class FakeServer:
|
||||
sms_oa = '12345'
|
||||
sms_sc = '12345678912'
|
||||
|
||||
sp = {'spi1': '16', 'spi2': '01', 'kic': '25', 'kid': '25', 'tar': '000000',
|
||||
'kic_key': 'AA', 'kid_key': 'BB', 'include_cpi': True}
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
|
||||
with mock.patch.object(srv, '_build_secured_packet', lambda *a, **k: ('AABB', {})), \
|
||||
mock.patch.object(srv, '_send_secured_packet',
|
||||
lambda *a, **k: {'success': False, 'error': 'stub'}):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stderr(buf):
|
||||
ok = srv._ram_send_gp_apdu(FakeServer(), object(), sp, state,
|
||||
'NV before', '80CAFF2100', silent=True)
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(state['steps'], [])
|
||||
self.assertEqual(state['failure'], {})
|
||||
self.assertIsNone(state['encode_error'])
|
||||
self.assertIn('RAM C-APDU (NV before): 80CAFF2100', buf.getvalue())
|
||||
|
||||
|
||||
class RamCommandFormatTests(unittest.TestCase):
|
||||
"""RAM command format detection helpers (TS 102 226 5.2.1, v3.6.24)."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user