scp81: BER-encode the Command Scripting template lengths (v2.1.11)
The C-APDU TLV length was written as a raw byte: a 245-byte LOAD command
produced 'AE 80 22 F5 ...', which BER reads as a long-form marker, so the
card mis-parsed every script command over 127 bytes. Small commands worked,
which made a RAM install look alive: the card answered the LOAD steps with a
degenerate 'AF 80' body (no R-APDU), and the final INSTALL [for install]
failed with 6A88 because the package never loaded.
Both the indefinite ('22' TLV) and definite ('AA' outer) lengths are now
BER-encoded (same rule as the BIP channel data TLV fix). Tests: 245-byte
LOAD body, short form, definite variant (200 python, 346 frontend);
findings doc updated; service worker v148.
This commit is contained in:
@@ -197,6 +197,22 @@ continuation with `D276000005AA060200000000B00000` -> page 2 `SW 9000`
|
||||
`D276000005AAFFCAFE0001/0010`, `A0000001515350`, `A000000151535041`).
|
||||
Full session: 7/7 commands, all `X-Admin-Script-Status: ok`.
|
||||
|
||||
## RESOLVED 2026-09-16c: RAM install over SCP81 - BER length in the script template
|
||||
|
||||
**Root cause:** `_scp81_command_body` wrote the C-APDU TLV length as a raw
|
||||
byte (`AE 80 22 F5 <245 bytes> 00 00` for a 245-byte LOAD). BER reads a byte
|
||||
above 0x7F as a long-form marker, so the card mis-parsed every LOAD >128
|
||||
bytes; the small INSTALL commands (<128 bytes) executed normally, which made
|
||||
the install look alive. Symptoms: the card accepted the LOAD responses with
|
||||
`X-Admin-Script-Status: ok` but sent a degenerate `AF 80` body, no LOAD
|
||||
R-APDU appeared in the results, and the final INSTALL [for install] answered
|
||||
`6A88` (module not found) because the package was never loaded.
|
||||
|
||||
**Fix:** both the indefinite ("22" TLV) and definite ("AA" outer) template
|
||||
lengths are BER-encoded (`_ber_len_bytes`); the same rule as the BIP channel
|
||||
data TLV fix earlier the same day. Tests cover the 245-byte LOAD body, the
|
||||
short-form case and the definite variant.
|
||||
|
||||
## Next tests / work
|
||||
|
||||
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="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.10</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.11</span></h1>
|
||||
<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-dot" class="text-xs text-gray-400" title="Connecting...">●</span>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v147';
|
||||
const CACHE = 'otaman-v148';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "2.1.10"
|
||||
version = "2.1.11"
|
||||
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.
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.1.10'
|
||||
VERSION = '2.1.11'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1436,16 +1436,27 @@ _SCP81_CHUNKED = False
|
||||
_SCP81_LINK_EVENTS = True
|
||||
|
||||
|
||||
def _ber_len_bytes(n):
|
||||
"""BER-TLV length as bytes (short form up to 127, then 0x81/0x82)."""
|
||||
if n < 0x80:
|
||||
return bytes([n])
|
||||
if n < 0x100:
|
||||
return bytes([0x81, n])
|
||||
return bytes([0x82, n >> 8, n & 0xFF])
|
||||
|
||||
|
||||
def _scp81_command_body(apdu_hex, definite=False, cr_tag=False):
|
||||
"""Command Scripting template with one C-APDU TLV: the indefinite-length
|
||||
variant ('AE 80 22 <len> <apdu> 00 00', recommended for RAM over HTTPS) or
|
||||
the definite-length one ('AA <len> 22 <len> <apdu>'). The C-APDU TLV tag
|
||||
is '22' per TS 101 220 (CR flag 0); some cards expect the CR-set 'A2'
|
||||
instead, so it is configurable."""
|
||||
instead, so it is configurable. Both lengths are BER-encoded: a raw byte
|
||||
above 0x7F is read as a long-form marker by the card, which garbles the
|
||||
script (the 245-byte LOAD commands of a RAM install, live 2026-09-16)."""
|
||||
apdu = bytes.fromhex(re.sub(r'\s', '', apdu_hex))
|
||||
cmd_tlv = bytes([0xA2 if cr_tag else 0x22, len(apdu)]) + apdu
|
||||
cmd_tlv = bytes([0xA2 if cr_tag else 0x22]) + _ber_len_bytes(len(apdu)) + apdu
|
||||
if definite:
|
||||
return bytes([0xAA, len(cmd_tlv)]) + cmd_tlv
|
||||
return bytes([0xAA]) + _ber_len_bytes(len(cmd_tlv)) + cmd_tlv
|
||||
return bytes([0xAE, 0x80]) + cmd_tlv + b'\x00\x00'
|
||||
|
||||
|
||||
|
||||
@@ -790,3 +790,26 @@ class QueueScriptTest(unittest.TestCase):
|
||||
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
|
||||
server._SCP81_SCRIPT_SENT = 0
|
||||
server._SCP81_SCRIPT_KIND = 'explore'
|
||||
|
||||
|
||||
class ScriptBodyLengthTest(unittest.TestCase):
|
||||
def test_long_c_apdu_uses_ber_long_form(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
apdu = '80E80000F0' + 'AB' * 239 + '00' # exactly 245-byte LOAD
|
||||
body = _scp81_command_body(apdu)
|
||||
# AE 80 22 81 F5 <245 bytes> 00 00
|
||||
self.assertEqual(body[:5].hex().upper(), 'AE802281F5')
|
||||
self.assertEqual(body[-2:].hex().upper(), '0000')
|
||||
self.assertEqual(len(body), 5 + 245 + 2)
|
||||
|
||||
def test_short_apdu_stays_short_form(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
self.assertEqual(_scp81_command_body('80CAFF2100').hex().upper(),
|
||||
'AE80220580CAFF21000000')
|
||||
|
||||
def test_definite_variant_ber_lengths(self):
|
||||
from pysim_otaman_server.server import _scp81_command_body
|
||||
apdu = 'AB' * 130
|
||||
body = _scp81_command_body(apdu, definite=True)
|
||||
# AA 81 85 22 81 82 <130 bytes> (outer 1+2+130 = 133 = 0x85)
|
||||
self.assertEqual(body[:6].hex().upper(), 'AA8185228182')
|
||||
|
||||
Reference in New Issue
Block a user