fix: recover the SMS-SUBMIT listings in RAM Explore (v3.6.4)
Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low. Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:
- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
`8B 81 97 ...` (BER long form) for the big pages, so it returned a
corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
read a bogus UDH and reported no concatenation, so the segments never
assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
The step was marked failed, the counter did not advance, the same counter
was retried and the card answered `cntr_low`, which also blocked P1=10
(modules) where the installed package appears.
- the captured segments now always store the assembled UD (`submit_ud_hex`);
`_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
`_decode_por`. Verified against the live capture: por_ok, remote SW 6310,
438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
the counter advances when the data follows via SMS-SUBMIT.
Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).
Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.
608 frontend / 492 Python green; version 3.6.4; sw simple-v277.
This commit is contained in:
@@ -30,9 +30,13 @@ from pysim_simple_server.server import (
|
||||
_ota_reference,
|
||||
_parse_select_item,
|
||||
_parse_setup_menu_items,
|
||||
_calc_ud_offset,
|
||||
_find_sms_tpdu,
|
||||
_parse_sms_concat,
|
||||
_por_remote_sw,
|
||||
_ram_next_cntr,
|
||||
_ram_remote_sw_ok,
|
||||
_sms_submit_por,
|
||||
_ram_step_result,
|
||||
_record_tr,
|
||||
_send_secured_packet,
|
||||
@@ -1032,7 +1036,7 @@ class TestSmsReassembly(unittest.TestCase):
|
||||
|
||||
def test_single_segment_no_concat(self):
|
||||
"""Single segment without UDH → submit_tpdu_hex is set directly."""
|
||||
from pysim_simple_server.server import PoRSubmitHandler, _find_sms_tpdu, _parse_sms_concat
|
||||
from pysim_simple_server.server import PoRSubmitHandler
|
||||
handler = PoRSubmitHandler()
|
||||
# Build a simple D0 with tag 8B containing an SMS-SUBMIT without UDH
|
||||
sms_tpdu = bytes.fromhex('040005902143F50004' # SMS-SUBMIT header
|
||||
@@ -1311,3 +1315,69 @@ class RamPorStepTest(unittest.TestCase):
|
||||
'responses': [{'status_word': '9000'}, {'status_word': '6A82'}]}
|
||||
self.assertEqual(_por_remote_sw(por), '6A82')
|
||||
|
||||
|
||||
|
||||
class SmsSubmitCaptureTest(unittest.TestCase):
|
||||
"""The actual-response SMS-SUBMIT path: the card answers the ENVELOPE with
|
||||
PoR status 0x0B and delivers the listing in SMS-SUBMIT TPDUs (SEND SHORT
|
||||
MESSAGE proactive commands). The FETCH bytes below are from a live RAM
|
||||
explore (the two 274-byte segments of the ELF listing)."""
|
||||
|
||||
FETCH_1 = ('d081ae81030113008202818305000607812143658719f28b8197510005812143f57ff6'
|
||||
'058c070003130201710000e90a000000000000030600000263100bd276000005aaffcafe'
|
||||
'0001010007a000000151535001000bd276000005aaffcafe001001000bd276000005aaff'
|
||||
'cafe0304010010d2760001180002ff491ff3890000010101000bd276000005aaffcafe00'
|
||||
'02010007a0000000620001010007a0000000620002010007a0000000620101010006')
|
||||
FETCH_2 = ('d0818e81030113008202818305000607812143658719f28b78510005812143f57ff6'
|
||||
'056d050003130202a00000015100010007a0000000620102010007a000000062020101'
|
||||
'0008a000000062020801010009a00000006202080101010010a0000000090003ffffff'
|
||||
'ff8910710001010010a0000000090003ffffffff891071000201000bd276000005aaff'
|
||||
'cafe00030100')
|
||||
|
||||
def test_find_sms_tpdu_reads_ber_long_form_lengths(self):
|
||||
tpdu = bytes.fromhex(_find_sms_tpdu(bytes.fromhex(self.FETCH_1)))
|
||||
self.assertEqual(len(tpdu), 0x97) # 8B 81 97 <151 bytes>
|
||||
self.assertEqual(tpdu[:6].hex(), '510005812143'.lower())
|
||||
tpdu2 = bytes.fromhex(_find_sms_tpdu(bytes.fromhex(self.FETCH_2)))
|
||||
self.assertEqual(len(tpdu2), 0x78) # 8B 78 <120 bytes>
|
||||
|
||||
def test_sms_submit_ud_offset_with_relative_validity(self):
|
||||
# VPF=10 (relative) = 1 byte, not 7: 0x51 & 0x18 = 0x10 -> relative
|
||||
tpdu = bytes.fromhex(_find_sms_tpdu(bytes.fromhex(self.FETCH_1)))
|
||||
self.assertEqual(_calc_ud_offset(tpdu), 11)
|
||||
# synthetic absolute/enhanced forms still take 7 bytes
|
||||
abs_tpdu = bytes.fromhex('5900048111227ff6' + '00' * 7 + '00' + '00' * 8)
|
||||
self.assertEqual(_calc_ud_offset(abs_tpdu), 16)
|
||||
|
||||
def test_parse_sms_concat_segments(self):
|
||||
ref, total, num, payload = _parse_sms_concat(
|
||||
bytes.fromhex(_find_sms_tpdu(bytes.fromhex(self.FETCH_1))))
|
||||
self.assertEqual((ref, total, num), (0x13, 2, 1))
|
||||
self.assertEqual(len(payload), 132)
|
||||
ref2, total2, num2, payload2 = _parse_sms_concat(
|
||||
bytes.fromhex(_find_sms_tpdu(bytes.fromhex(self.FETCH_2))))
|
||||
self.assertEqual((ref2, total2, num2), (0x13, 2, 2))
|
||||
self.assertEqual(len(payload2), 103)
|
||||
|
||||
def test_sms_submit_por_decodes_to_the_listing(self):
|
||||
class Handler:
|
||||
submit_ud_hex = None
|
||||
handler = Handler()
|
||||
segs = {}
|
||||
for hx in (self.FETCH_1, self.FETCH_2):
|
||||
_, tot, num, payload = _parse_sms_concat(bytes.fromhex(_find_sms_tpdu(bytes.fromhex(hx))))
|
||||
segs[num] = payload
|
||||
handler.submit_ud_hex = b''.join(segs[k] for k in sorted(segs)).hex()
|
||||
por_hex = _sms_submit_por(handler)
|
||||
self.assertTrue(por_hex.startswith('027100'))
|
||||
por = _decode_por('15', '21', '25', '25', '0000000306',
|
||||
'00' * 16, '00' * 16, por_hex)
|
||||
self.assertEqual(por['response_status'], 'por_ok')
|
||||
self.assertEqual(por['decoded']['last_status_word'], '6310')
|
||||
data = por['decoded']['last_response_data']
|
||||
self.assertEqual(len(data), 438)
|
||||
self.assertTrue(data.lower().startswith('0bd2760000'), data[:20])
|
||||
# no submit response captured -> empty string, never a bogus packet
|
||||
handler.submit_ud_hex = None
|
||||
self.assertEqual(_sms_submit_por(handler), '')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user