From 92f646df085f01cb042e3ae1fb02082e424cfe3c 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 07:45:14 +0300 Subject: [PATCH] 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. --- docs/scp81-findings.md | 16 ++++++++++++++++ frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_otaman_server/server.py | 19 +++++++++++++++---- tests/test_scp81.py | 23 +++++++++++++++++++++++ 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/docs/scp81-findings.md b/docs/scp81-findings.md index 473bb11..c2f5772 100644 --- a/docs/scp81-findings.md +++ b/docs/scp81-findings.md @@ -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 diff --git a/frontend/index.html b/frontend/index.html index 49db8de..cd6633d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v2.1.10

+

OTAMan SIM OTA with a Human Face v2.1.11

diff --git a/frontend/sw.js b/frontend/sw.js index 2780346..587101b 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v147'; +const CACHE = 'otaman-v148'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index a948345..5433bef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 42b1d98..2baae70 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.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 00 00', recommended for RAM over HTTPS) or the definite-length one ('AA 22 '). 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' diff --git a/tests/test_scp81.py b/tests/test_scp81.py index 035ceb7..0b59ecf 100644 --- a/tests/test_scp81.py +++ b/tests/test_scp81.py @@ -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')