scp81: fix RAM LOAD block splitting (overlapping 1-byte shifts) (v2.1.12)
The block slicer used the block number as a character offset (loadfile_tlv[i * 2:(i + 240) * 2]), so every LOAD block after the first was a 1-byte-shifted copy of the previous one - the cap header repeated every 239 bytes on the wire. A live install accepted three blocks, failed block 4 with SW 6400, then 6985, and INSTALL [for install] answered 6A88. The same slicing was inherited by the SCP81 helper from the SCP80 path, so multi-block caps could not install there either; both are fixed. Tests: blocks are consecutive and reassemble the C4 TLV byte-for-byte (200 python); findings updated; service worker v149.
This commit is contained in:
@@ -213,6 +213,22 @@ 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
|
data TLV fix earlier the same day. Tests cover the 245-byte LOAD body, the
|
||||||
short-form case and the definite variant.
|
short-form case and the definite variant.
|
||||||
|
|
||||||
|
## RESOLVED 2026-09-16d: RAM install - LOAD blocks were overlapping copies
|
||||||
|
|
||||||
|
**Root cause:** the LOAD block slicer indexed the load file TLV with the
|
||||||
|
*block number* (`loadfile_tlv[i * 2:(i + 240) * 2] for i in range(blocks)`)
|
||||||
|
instead of a *character offset*, so every block after the first was a
|
||||||
|
1-byte-shifted copy of its predecessor. On the wire the cap header repeated
|
||||||
|
every 239 bytes. The card accepted the first three blocks and failed block 4
|
||||||
|
with `SW 6400` (execution error), then refused the rest (`6985`) and the
|
||||||
|
final INSTALL answered `6A88`. The same slicing lived in the SCP80
|
||||||
|
/api/ram-install path (the helper was extracted from it), so multi-block caps
|
||||||
|
could never install there either.
|
||||||
|
|
||||||
|
**Fix:** consecutive chunks at char offsets
|
||||||
|
(`range(0, len(tlv), 240 * 2)`), with a reassembly test that pins the joined
|
||||||
|
blocks to the C4 TLV byte for byte.
|
||||||
|
|
||||||
## 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.11</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.12</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-v148';
|
const CACHE = 'otaman-v149';
|
||||||
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.11"
|
version = "2.1.12"
|
||||||
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.11'
|
VERSION = '2.1.12'
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -414,9 +414,12 @@ def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
|||||||
ifl_data = _lv(loadfile_aid) + _lv(sd) + '00' + '00' + '00'
|
ifl_data = _lv(loadfile_aid) + _lv(sd) + '00' + '00' + '00'
|
||||||
apdus = ['80E60200%02X%s00' % (len(ifl_data) // 2, ifl_data)]
|
apdus = ['80E60200%02X%s00' % (len(ifl_data) // 2, ifl_data)]
|
||||||
loadfile_tlv = 'C4' + _ber_len(len(loadfile_data) // 2) + loadfile_data
|
loadfile_tlv = 'C4' + _ber_len(len(loadfile_data) // 2) + loadfile_data
|
||||||
total_bytes = len(loadfile_tlv) // 2
|
# Split the TLV into consecutive 240-byte blocks (char offsets, 2 per
|
||||||
blocks = [loadfile_tlv[i * 2:(i + block_size) * 2]
|
# byte). The earlier form indexed with the block number ('i * 2'), which
|
||||||
for i in range(0, (total_bytes + block_size - 1) // block_size)]
|
# produced overlapping 1-byte-shifted copies - the card failed mid-load
|
||||||
|
# with SW 6400 (live 2026-09-16).
|
||||||
|
blocks = [loadfile_tlv[off:off + block_size * 2]
|
||||||
|
for off in range(0, len(loadfile_tlv), block_size * 2)]
|
||||||
for i, block in enumerate(blocks):
|
for i, block in enumerate(blocks):
|
||||||
p1 = 0x80 if i == len(blocks) - 1 else 0x00
|
p1 = 0x80 if i == len(blocks) - 1 else 0x00
|
||||||
apdus.append('80E8%02X%02X%02X%s00' % (p1, i % 256, len(block) // 2, block))
|
apdus.append('80E8%02X%02X%02X%s00' % (p1, i % 256, len(block) // 2, block))
|
||||||
|
|||||||
@@ -879,10 +879,20 @@ class CapApduSequenceTest(unittest.TestCase):
|
|||||||
self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2])
|
self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2])
|
||||||
|
|
||||||
def test_load_blocks_split_and_counter(self):
|
def test_load_blocks_split_and_counter(self):
|
||||||
from pysim_otaman_server.server import _cap_apdu_sequence
|
from pysim_otaman_server.server import _cap_apdu_sequence, _ber_len as _ber_len_lower
|
||||||
data = 'AB' * 700 # 700 bytes -> C4 TLV 703 -> 3 x 240-byte blocks
|
data = ''.join('%02X' % (i % 256) for i in range(700))
|
||||||
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data)
|
seq = _cap_apdu_sequence('A00000010001', 'A000000100', data)
|
||||||
self.assertEqual(len(seq), 5) # INSTALL + 3 LOAD + INSTALL
|
self.assertEqual(len(seq), 5) # INSTALL + 3 LOAD + INSTALL
|
||||||
self.assertEqual(seq[1][:8], '80E80000')
|
self.assertEqual(seq[1][:8], '80E80000')
|
||||||
self.assertEqual(seq[2][:8], '80E80001')
|
self.assertEqual(seq[2][:8], '80E80001')
|
||||||
self.assertEqual(seq[3][:8], '80E88002') # last block: P1=0x80
|
self.assertEqual(seq[3][:8], '80E88002') # last block: P1=0x80
|
||||||
|
# The blocks are consecutive chunks and reassemble the load file TLV
|
||||||
|
# byte-for-byte (a shifted/overlapping split fails the card mid-load).
|
||||||
|
def payload(apdu):
|
||||||
|
lc = int(apdu[8:10], 16)
|
||||||
|
return apdu[10:10 + lc * 2]
|
||||||
|
joined = payload(seq[1]) + payload(seq[2]) + payload(seq[3])
|
||||||
|
self.assertTrue(joined.startswith('C482'))
|
||||||
|
expected = 'C4' + _ber_len_lower(700) + data # 700 = 0x2BC
|
||||||
|
self.assertEqual(joined.upper(), expected.upper())
|
||||||
|
self.assertEqual(int(seq[3][8:10], 16), len(expected) // 2 - 480)
|
||||||
|
|||||||
Reference in New Issue
Block a user