fix: capture the submit PoR on every card + per-operation RAM format check (v3.6.24)
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow showed why only one card works: - The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries (SPI2=0x01) the fetched TPDU was thrown away - the Explore died with "no data" and the RAM install steps silently degraded to an unverified "no_por". The capture is now unconditional (inline ENVELOPE responses and proactive submits both work), and the STATUS-poll fallback is enabled for every card. - `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step - the server-side half of the v3.6.23 counter report. It now advances the full 40 bits. - RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded `AA` > `22` form) is detected per operation and never stored: /api/ram-install and /api/ram-install-app always probe, /api/send-ota probes when the caller passes `ram_format`. The probe is a read-only GET STATUS [ISD] as the first step (compact, then wrapped) and every command of the operation uses the detected format. `_wrap_expanded_apdu` is pinned to the reference bytes `AA0A220880F24000024F0000`; responses carry `ram_format` and `final_cntr` (accepted probes consume counters, so the views use the server's next counter). Applet-directed payloads stay byte-exact; only RAM views (and RAM/GP chain packs) opt in. 504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
This commit is contained in:
@@ -36,6 +36,8 @@ from pysim_simple_server.server import (
|
||||
_parse_sms_concat,
|
||||
_por_remote_sw,
|
||||
_ram_next_cntr,
|
||||
_ram_format_apdu,
|
||||
_wrap_expanded_apdu,
|
||||
_ram_remote_sw_ok,
|
||||
_sms_submit_por,
|
||||
_ram_step_result,
|
||||
@@ -1359,7 +1361,10 @@ class RamPorStepTest(unittest.TestCase):
|
||||
def test_counter_advances_only_for_accepted_packets(self):
|
||||
self.assertEqual(_ram_next_cntr('0000000010', True), '0000000011')
|
||||
self.assertEqual(_ram_next_cntr('0000000010', False), '0000000010')
|
||||
self.assertEqual(_ram_next_cntr('FFFFFFFF', True), '0000000000')
|
||||
# 5-byte counter: high values keep their top byte (v3.6.24)
|
||||
self.assertEqual(_ram_next_cntr('10000AAAC8', True), '10000AAAC9')
|
||||
self.assertEqual(_ram_next_cntr('0000FFFFFF', True), '0001000000')
|
||||
self.assertEqual(_ram_next_cntr('FFFFFFFFFF', True), '0000000000')
|
||||
|
||||
def test_remote_sw_from_expanded_response(self):
|
||||
por = {'response_status': 'por_ok', 'decoded': {},
|
||||
@@ -1510,3 +1515,76 @@ class RamSendGpApduLoggingTest(unittest.TestCase):
|
||||
out = buf.getvalue()
|
||||
self.assertIn('RAM C-APDU (LOAD (1/2)): 80E8800001AA', out)
|
||||
self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)
|
||||
|
||||
|
||||
class RamCommandFormatTests(unittest.TestCase):
|
||||
"""RAM command format detection helpers (TS 102 226 5.2.1, v3.6.24)."""
|
||||
|
||||
def test_expanded_wrapper_matches_the_reference_trace(self):
|
||||
# TCA Loader trace: GET STATUS [ISD] sent as 'AA' > '22' > C-APDU
|
||||
self.assertEqual(_wrap_expanded_apdu('80F24000024F0000'),
|
||||
'AA0A220880F24000024F0000')
|
||||
|
||||
def test_expanded_wrapper_uses_ber_lengths(self):
|
||||
apdu = '80' + 'AB' * 200
|
||||
wrapped = _wrap_expanded_apdu(apdu)
|
||||
self.assertTrue(wrapped.startswith('AA81'), wrapped[:8])
|
||||
self.assertTrue(wrapped.endswith(apdu))
|
||||
self.assertEqual(_ram_format_apdu(apdu, 'compact'), apdu)
|
||||
self.assertEqual(_ram_format_apdu(apdu, 'expanded'), wrapped)
|
||||
|
||||
def test_detect_prefers_compact_and_records_the_probe(self):
|
||||
from pysim_simple_server import server as srv
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_send(server, scc, sp, state, name, apdu):
|
||||
calls.append((name, apdu))
|
||||
state['steps'].append({'name': name, 'por_status': 'por_ok', 'sw': '9000'})
|
||||
state['cntr'] = srv._ram_next_cntr(state['cntr'], True)
|
||||
return True
|
||||
|
||||
orig = srv._ram_send_gp_apdu
|
||||
srv._ram_send_gp_apdu = fake_send
|
||||
try:
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': '0000000010'}
|
||||
fmt = srv._ram_detect_format(None, None, {}, state)
|
||||
finally:
|
||||
srv._ram_send_gp_apdu = orig
|
||||
self.assertEqual(fmt, 'compact')
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0][0], 'FORMAT CHECK (compact)')
|
||||
self.assertEqual(state['cntr'], '0000000011')
|
||||
self.assertEqual(len(state['steps']), 1)
|
||||
|
||||
def test_detect_falls_back_to_expanded(self):
|
||||
from pysim_simple_server import server as srv
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_send(server, scc, sp, state, name, apdu):
|
||||
calls.append((name, apdu))
|
||||
wrapped = apdu.startswith('AA')
|
||||
state['steps'].append({'name': name,
|
||||
'por_status': 'por_ok' if wrapped else 'por_remote_error',
|
||||
'sw': '9000'})
|
||||
if wrapped:
|
||||
state['cntr'] = srv._ram_next_cntr(state['cntr'], True)
|
||||
return wrapped
|
||||
|
||||
orig = srv._ram_send_gp_apdu
|
||||
srv._ram_send_gp_apdu = fake_send
|
||||
try:
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': '10000AAAC8'}
|
||||
fmt = srv._ram_detect_format(None, None, {}, state)
|
||||
finally:
|
||||
srv._ram_send_gp_apdu = orig
|
||||
self.assertEqual(fmt, 'expanded')
|
||||
self.assertEqual([c[0] for c in calls],
|
||||
['FORMAT CHECK (compact)', 'FORMAT CHECK (expanded)'])
|
||||
self.assertTrue(calls[1][1].startswith('AA'))
|
||||
# only the accepted (expanded) probe advanced the 5-byte counter
|
||||
self.assertEqual(state['cntr'], '10000AAAC9')
|
||||
self.assertEqual(len(state['steps']), 2)
|
||||
|
||||
Reference in New Issue
Block a user