diff --git a/docs/scp81-findings.md b/docs/scp81-findings.md index e97c813..effc857 100644 --- a/docs/scp81-findings.md +++ b/docs/scp81-findings.md @@ -271,6 +271,21 @@ invisible. Labels/decoder updated; the remote APDU script builder's P1 map (0x02 load / 0x0C install / 0x08 make-selectable / 0x40 reg-update / 0x10 extradition) was already correct. +## RESOLVED 2026-09-16g: response R-APDU TLV length also needs BER long form + +**Root cause:** `_scp81_parse_response` read the `23` (R-APDU) TLV length as a +raw byte. A listing page above 127 bytes arrives as `AF 80 23 81 FC <252 +bytes> 00 00`; the parser took `0x81` as the length, so every page was +silently cut to 127 bytes with a bogus status word (the data's last two +bytes, e.g. `CAFE`/`0001`/`9F70` instead of the real `63 10`). The bogus SW +also stopped the pagination, so later registry entries - including the +installed package `AA1902BC225801` - never appeared. + +**Fix:** the response template TLVs use `httpota.ber_len_read` (BER length, +same class of bug as the channel data TLV and the command script template +earlier the same day). Regression tests cover a 250-byte page with +`23 81 FC` and the short-form case. + ## Next tests / work 1. **UI:** group the per-page R-APDUs under their logical command in the diff --git a/frontend/index.html b/frontend/index.html index e26e798..111d9d3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v2.1.16

+

OTAMan SIM OTA with a Human Face v2.1.17

diff --git a/frontend/sw.js b/frontend/sw.js index fea8e44..71cdbc5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v153'; +const CACHE = 'otaman-v154'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 58ef5aa..e9abd68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-otaman-server" -version = "2.1.16" +version = "2.1.17" description = "HTTP REST server wrapping pysim for the OTAMan PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 2452b35..f4e80a3 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '2.1.16' +VERSION = '2.1.17' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -1479,10 +1479,16 @@ def _scp81_parse_response(body): content = body count, out = 0, [] off = 0 - while off + 1 < len(content): - tag, tlen = content[off], content[off + 1] - val = content[off + 2:off + 2 + tlen] - off += 2 + tlen + while off < len(content): + tag = content[off] + # BER lengths: an R-APDU TLV above 127 bytes is `23 81 FC ...`; a raw + # byte >0x7F used as a length silently truncated every listing page + # to 127 bytes (live 2026-09-16), hiding later registry entries. + tlen, voff = httpota.ber_len_read(content, off + 1) + val = content[voff:voff + tlen] + if len(val) < tlen: + break + off = voff + tlen if tag == 0x80: count = int.from_bytes(val, 'big') if val else 0 elif tag == 0x23 and len(val) >= 2: diff --git a/tests/test_scp81.py b/tests/test_scp81.py index c79b9c3..7f8ebcf 100644 --- a/tests/test_scp81.py +++ b/tests/test_scp81.py @@ -855,3 +855,22 @@ class VerbatimScriptTest(unittest.TestCase): finally: server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore']) server._SCP81_SCRIPT_SENT = 0 + + +class ResponseTlvLengthTest(unittest.TestCase): + def test_long_form_r_apdu_length(self): + # A page bigger than 127 bytes: `AF 80 23 81 FC <252 bytes> 00 00` + page = bytes.fromhex('E3284F08D276000005AAFFCAFE00019F700101CC08A000000003000000' * 9)[:250] + rapdu = page + b'\x63\x10' + body = b'\xAF\x80\x23' + bytes([0x81, len(rapdu)]) + rapdu + b'\x00\x00' + count, rapdus = server._scp81_parse_response(body) + self.assertEqual(len(rapdus), 1) + data, sw = rapdus[0] + self.assertEqual(sw, '6310') + self.assertEqual(len(data), 250) + + def test_short_form_still_works(self): + page = bytes.fromhex('E3114F08A0000000030000009F70010FC50100') + body = b'\xAF\x80\x23' + bytes([len(page) + 2]) + page + b'\xCA\xFE' + b'\x00\x00' + count, rapdus = server._scp81_parse_response(body) + self.assertEqual(rapdus, [(page, 'CAFE')])