scp81: RAM install over HTTP OTA (v2.1.8)

- shared _cap_apdu_sequence helper (INSTALL [for load] -> 240-byte LOAD
  blocks -> INSTALL [for install]); the SCP80 /api/ram-install path now uses
  it too (one source of truth; byte-level tests pin the APDUs)
- POST /api/scp81/ram-install: parses the .cap server-side and queues the
  APDU sequence as the SCP81 command script (one C-APDU per POST, runs on the
  card's next push); refused while a script is mid-run unless force
- /api/scp81/script reports the script kind (explore/none/custom/ram-install)
- tab: RAM install row (CAP file + SD AID + Queue button); RU strings
- docs: api.md, findings, AGENTS; service worker v145
This commit is contained in:
2026-09-16 02:05:20 +03:00
parent e255677dd9
commit 3463a68292
8 changed files with 257 additions and 47 deletions
+35
View File
@@ -755,3 +755,38 @@ class TargetedAppTest(unittest.TestCase):
self.assertEqual(rapdus[0][1], '6A88')
# A status-only POST (no body, e.g. unknown-application) parses empty.
self.assertEqual(server._scp81_parse_response(b''), (0, []))
class QueueScriptTest(unittest.TestCase):
def test_queue_replaces_and_resets(self):
server._SCP81_SCRIPT = ['80CAFF2100']
server._SCP81_SCRIPT_SENT = 1
server._SCP81_SCRIPT_RESULTS = [{'index': 1, 'sw': '9000', 'apdu': '80CAFF2100', 'rapdu': ''}]
try:
r = server._scp81_queue_script(['80E6020013' + '00' * 20, '80E88000' + '00' * 4],
kind='ram-install')
self.assertTrue(r['queued'])
self.assertEqual(server._SCP81_SCRIPT_KIND, 'ram-install')
self.assertEqual(server._SCP81_SCRIPT_SENT, 0)
self.assertEqual(server._SCP81_SCRIPT_RESULTS, [])
self.assertEqual(len(server._SCP81_SCRIPT), 2)
finally:
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
server._SCP81_SCRIPT_RESULTS = []
server._SCP81_SCRIPT_KIND = 'explore'
def test_queue_refuses_while_running(self):
server._SCP81_SCRIPT = ['80CAFF2100', '80F28002024F0000']
server._SCP81_SCRIPT_SENT = 1
try:
r = server._scp81_queue_script(['80E60200'], kind='ram-install')
self.assertFalse(r['queued'])
self.assertEqual(r['sent'], 1)
r = server._scp81_queue_script(['80E60200'], kind='ram-install', force=True)
self.assertTrue(r['queued'])
self.assertEqual(server._SCP81_SCRIPT, ['80E60200'])
finally:
server._SCP81_SCRIPT = list(server._SCP81_SCRIPTS['explore'])
server._SCP81_SCRIPT_SENT = 0
server._SCP81_SCRIPT_KIND = 'explore'