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
This commit is contained in:
@@ -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
|
(0x02 load / 0x0C install / 0x08 make-selectable / 0x40 reg-update / 0x10
|
||||||
extradition) was already correct.
|
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
|
## Next tests / work
|
||||||
|
|
||||||
1. **UI:** group the per-page R-APDUs under their logical command in the
|
1. **UI:** group the per-page R-APDUs under their logical command in the
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.1.16</span></h1>
|
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.1.17</span></h1>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
|
||||||
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v153';
|
const CACHE = 'otaman-v154';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-otaman-server"
|
name = "pysim-otaman-server"
|
||||||
version = "2.1.16"
|
version = "2.1.17"
|
||||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
|||||||
from osmocom.tlv import BER_TLV_IE
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -1479,10 +1479,16 @@ def _scp81_parse_response(body):
|
|||||||
content = body
|
content = body
|
||||||
count, out = 0, []
|
count, out = 0, []
|
||||||
off = 0
|
off = 0
|
||||||
while off + 1 < len(content):
|
while off < len(content):
|
||||||
tag, tlen = content[off], content[off + 1]
|
tag = content[off]
|
||||||
val = content[off + 2:off + 2 + tlen]
|
# BER lengths: an R-APDU TLV above 127 bytes is `23 81 FC ...`; a raw
|
||||||
off += 2 + tlen
|
# 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:
|
if tag == 0x80:
|
||||||
count = int.from_bytes(val, 'big') if val else 0
|
count = int.from_bytes(val, 'big') if val else 0
|
||||||
elif tag == 0x23 and len(val) >= 2:
|
elif tag == 0x23 and len(val) >= 2:
|
||||||
|
|||||||
@@ -855,3 +855,22 @@ class VerbatimScriptTest(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
|
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
|
||||||
server._SCP81_SCRIPT_SENT = 0
|
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')])
|
||||||
|
|||||||
Reference in New Issue
Block a user