From e957757fda6835bcfb321d4a16c837b16207f110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 16 Sep 2026 08:29:00 +0300 Subject: [PATCH] scp81: BER length for the Response Scripting template TLVs (v2.1.17) The R-APDU TLV length was read as a raw byte, so a listing page above 127 bytes (AF 80 23 81 FC <252 bytes> 00 00) was cut to its first 127 bytes with a bogus status word (the data's last two bytes: CAFE/0001/9F70 instead of the real 63 10 "more data"). The bogus SW also stopped the SW CAFE/6310 pagination, which is why later registry entries - e.g. the installed package AA1902BC225801 - never showed up. Uses httpota.ber_len_read now. Tests: 250-byte long-form page and short-form regression (204 python); findings updated; service worker v154 --- docs/scp81-findings.md | 15 +++++++++++++++ frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_otaman_server/server.py | 16 +++++++++++----- tests/test_scp81.py | 19 +++++++++++++++++++ 6 files changed, 48 insertions(+), 8 deletions(-) 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')])