v3.19.1: the compact-PoR decode guard (applet TAR responses reported as data)

pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.

_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.

Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.

Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
This commit is contained in:
2026-10-04 22:03:25 +03:00
parent d5aed563a1
commit 14c43f37b3
8 changed files with 118 additions and 25 deletions
+28
View File
@@ -507,6 +507,34 @@ class TestDecodePor(unittest.TestCase):
msg='expected None for %r' % bad)
def test_applet_response_falls_back_to_raw(self):
# A third-party applet's own TAR answers with application bytes;
# `80 01 10` cannot be a compact response for a 1-byte command script
# (count 0x80 exceeds it), so the whole secured data is the response
# data and no status word is invented (live 2026-10-04).
raw = '027100000e0aaf4d0100020000190000800110'
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw, cmd_len=1)
self.assertEqual(r['response_type'], 'raw')
self.assertEqual(r['secured_data'], '800110')
self.assertEqual(r['decoded']['last_status_word'], '')
self.assertEqual(r['decoded']['last_response_data'], '800110')
self.assertIsNone(r['decoded']['number_of_commands'])
# without the command length the compact parse is kept (unknown
# context - the pre-fix behaviour)
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw)
self.assertEqual(r['response_type'], 'compact')
self.assertEqual(r['decoded']['number_of_commands'], 128)
def test_rfm_chain_response_stays_compact(self):
# a chained RFM command script is long enough for its count: the real
# SW (6a86) is decoded as before
raw = '027100000e0ab0000000020000180000016a86'
r = _decode_por('16', '01', '15', '15', '0000000001', K, K, raw, cmd_len=68)
self.assertEqual(r['response_type'], 'compact')
self.assertEqual(r['decoded']['last_status_word'], '6a86')
self.assertEqual(r['secured_data'], '016A86')
class TestProactiveDecode(unittest.TestCase):
"""Server-side proactive command/TR decode helpers (v1.8.0 log feature)."""