From 175ca934d89f574626ea00a64ad964b2504c52ca 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 01:50:29 +0300 Subject: [PATCH] scp81: PSK TLS server and command scripting (phases B/C) + BIP fix (v2.1.5) - scp81.py: PSK TLS listener (stdlib ssl PSK callbacks) speaking the GP HTTP administration dialog; configurable framing (chunked/Content-Length, TLS record split, Apache-style/compact headers, Connection header, keep-alive, Next-URI template with %d, TLS version/cipher, answer delay, keylog for capture decryption) - server.py: script responder + Response Scripting parsing (AF/AB, 80/23 TLVs), memory decoder, SCP81 start options, terminal-side timer management, background-mode BIP events, permissive OPEN CHANNEL - BIP fix: the RECEIVE DATA channel-data TLV length is BER long form (36 81 ) above 127 bytes; a raw length byte is mis-parsed on the card, so the large TLS records never reached its stack (a live card fetched the script response and silently never processed it - endless resume). The card now executes scripts and returns R-APDUs: memory (13 applets, 50646 B NV free, 2402 B volatile), ISD, stored HTTP OTA parameters, ELF and application registries - frontend: SCP81 tab (listener, script selection, HTTP OTA log), phone event forms, i18n; service worker v141 - docs: api.md, scp81-findings.md (attempt matrix + root cause analysis); tools/scp81_decrypt.py decrypts listener captures via the keylog - tests: 187 python + 337 frontend --- README.md | 20 +- README_RUS.md | 18 +- docs/api.md | 95 ++++ docs/scp81-findings.md | 200 +++++++++ frontend/index.html | 278 +++++++++++- frontend/sw.js | 2 +- frontend/tests/event_forms.test.js | 70 +++ frontend/tests/html.test.js | 2 +- frontend/tests/scp81.test.js | 59 +++ pyproject.toml | 2 +- pysim_otaman_server/__main__.py | 6 +- pysim_otaman_server/httpota.py | 102 ++++- pysim_otaman_server/scp81.py | 402 +++++++++++++++++ pysim_otaman_server/server.py | 648 +++++++++++++++++++++++++-- tests/test_httpota.py | 194 ++++++++ tests/test_ota_helpers.py | 191 ++++++++ tests/test_scp81.py | 681 +++++++++++++++++++++++++++++ tools/scp81_decrypt.py | 135 ++++++ 18 files changed, 3049 insertions(+), 56 deletions(-) create mode 100644 docs/scp81-findings.md create mode 100644 frontend/tests/event_forms.test.js create mode 100644 frontend/tests/scp81.test.js create mode 100644 pysim_otaman_server/scp81.py create mode 100644 tests/test_scp81.py create mode 100755 tools/scp81_decrypt.py diff --git a/README.md b/README.md index 9317a72..3472219 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ npm run build ## Interface -Five top-level tabs: **Remote APDU**, **SCP80**, **Profiler**, **Card reader**, and **Phone simulator**. **Remote APDU** and **SCP80** use pill sub-tabs; the Card reader tab has three sub-tabs: **File manager**, **pySim command line**, and **Raw APDU**; the Profiler tab lists **Profiles**, **Card snapshots**, and **Custom files**. +Six top-level tabs: **Remote APDU**, **SCP80**, **SCP81**, **Profiler**, **Card reader**, and **Phone simulator**. **Remote APDU** and **SCP80** use pill sub-tabs; the Card reader tab has three sub-tabs: **File manager**, **pySim command line**, and **Raw APDU**; the Profiler tab lists **Profiles**, **Card snapshots**, and **Custom files**. --- @@ -556,12 +556,16 @@ The **Phone simulator** tab provides real-time CAT session interaction. It has t - **Location Status** — dropdown for Normal / Limited / No service - **Access Technology Change** — dropdown for all 13 RAT types - **Card Reader Status, Language, UICC Access** — appropriate inputs +- **Channel Status** — channel selector, link state (not established / TCP + LISTEN / established) and info (no further info / link dropped), per TS 102 223 8.56 - **Network Rejection** — full adaptive form with registration type dropdown (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology selection, and 53-cause unified rejection cause code dropdown covering EMM, GMM, 5GMM, and LU causes -**Proactive Command Log** — chronological list of proactive commands encountered (seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, and PROVIDE LOCAL INFORMATION. +**Proactive Command Log** — chronological list of proactive commands encountered (seconds elapsed, type code, name, byte count). Covers SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, PROVIDE LOCAL INFORMATION, TIMER MANAGEMENT, and the BIP commands (OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA, GET CHANNEL STATUS); BIP commands are decoded with both plain and comprehension-required TLV tags. + +**Timer management** — the server acts as the terminal for TIMER MANAGEMENT (TS 102 223 §6.6.21/§7.4): started timers are tracked per card session, deactivate/get TERMINAL RESPONSEs carry the remaining value, and on expiry the card receives ENVELOPE (TIMER EXPIRATION). The live card uses this to retry the OTA session after a failed OPEN CHANNEL. **TR Config: PLI data dictionary** — editable per-qualifier hex values for all 22 PROVIDE LOCAL INFORMATION qualifiers (TS 102 223 + TS 131 111). 10 qualifiers have inline decode/encode forms (toggle): @@ -580,6 +584,16 @@ The **Phone simulator** tab provides real-time CAT session interaction. It has t Values persist on the server until restart. Apply → hex updates; Save → POSTs to server. The server will use these values to populate TERMINAL RESPONSE data for future PLI proactive commands. +## SCP81 + +The **SCP81** tab drives HTTP OTA (GP RAM over HTTP, GPC v2.2 Amendment B). The card's BIP channel is always redirected to a local listener on the server: + +- **Capture (dump)** — accepts the card's TCP channel and logs whatever it sends (e.g. the TLS ClientHello) without answering. Use it to inspect what the card asks for. +- **PSK TLS server** — answers the handshake with the TLS 1.2 PSK cipher suites of the spec and speaks the GP HTTP administration dialog (`X-Admin-*` headers, `200` with a command string or `204 No Content`). Enter the **PSK Identity** the card uses and the **PSK key (hex)**; the key is only sent to the local server, never stored or logged. +- **Script** — the command script served over the session: **Memory + ELF info** (default) sends `GET DATA FF21` (available non-volatile/volatile memory, applet count) and `GET STATUS P1=20/10` (Executable Load Files and modules registry) as RAM/GP commands in TS 102 226 Command Scripting templates, one C-APDU per request; **None** closes every session with `204`. Custom APDU lists are accepted by the API. + +The state line shows the listener, the negotiated identity and live channels (bytes in/out); the log records OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA and every TLS/HTTP/script step, including each R-APDU (`script-rapdu`, `script-memory`). The same controls are available through `POST /api/scp81/bip` and `GET /api/scp81/script` (see `docs/api.md`). + ## PWA OTAMan is a Progressive Web App and can be installed for offline use. Use the **INSTALL PWA** button in the header, or use the browser's install prompt. @@ -637,7 +651,7 @@ pysim-otaman-server --http-port 8080 | `--apdu-trace` | Log APDU-level traces to stderr | | `--log-requests` | Log request/response payloads to stderr | | `--sms-oa` / `--sms-sm-sc` | SMS-DELIVER originating address / SM-SC for PoR-in-submit | -| `--terminal-profile` | TERMINAL PROFILE payload hex (default 10-byte GSM profile) | +| `--terminal-profile` | TERMINAL PROFILE payload hex (default: 33-byte real-handset profile that advertises BIP events/commands; the live card ignores HTTP OTA without it) | | `--poll-interval` | Idle interval before automatic STATUS polling (default 30s; `0` disables polling) | | `--full-pysim-init` | Use pysim's stock init/equip (redundant card resets). The default init/equip is reset-free — only explicit equip/reset reconnect the card | | `--no-auto-equip` | Do not initialize a card automatically right after it is inserted (default: auto-equip on) | diff --git a/README_RUS.md b/README_RUS.md index 5c9be63..635609a 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -31,7 +31,7 @@ npm run build ## Интерфейс -Пять вкладок: **Remote APDU**, **SCP80**, **Profiler**, **Card reader** и **Phone simulator**. Вкладки Remote APDU и SCP80 используют пиллы-подвкладки; во вкладке Card reader три подвкладки: **File manager**, **pySim command line** и **Raw APDU**; во вкладке Profiler — **Profiles**, **Card snapshots** и **Custom files**. +Шесть вкладок: **Remote APDU**, **SCP80**, **SCP81**, **Profiler**, **Card reader** и **Phone simulator**. Вкладки Remote APDU и SCP80 используют пиллы-подвкладки; во вкладке Card reader три подвкладки: **File manager**, **pySim command line** и **Raw APDU**; во вкладке Profiler — **Profiles**, **Card snapshots** и **Custom files**. --- @@ -529,9 +529,13 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на - **События без данных** (User Activity, Idle Screen и др.) — однократное уведомление - **Location Status** — выпадающий список: Normal / Limited / No service - **Access Technology Change** — 13 типов RAT +- **Channel Status** — выбор канала, состояние линии (не установлена / TCP + LISTEN / установлена) и информация (нет данных / линия разорвана), TS 102 223 8.56 - **Network Rejection** — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля локации (MCC, MNC, LAC, RAC, TAC), доступные технологии, 53-позиционный выпадающий список причин отказа (EMM, GMM, 5GMM, LU) -**Proactive Command Log** — хронологический список проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор. +**Proactive Command Log** — хронологический список проактивных команд. Каждая строка показывает время, код типа, имя и декодированный квалификатор. Поддерживаются SET UP MENU, SET UP EVENT LIST, POLL INTERVAL, DISPLAY TEXT, SELECT ITEM, PROVIDE LOCAL INFORMATION, TIMER MANAGEMENT и BIP-команды (OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA, GET CHANNEL STATUS); BIP-команды декодируются с обычными и comprehension-required TLV-тегами. + +**Управление таймерами** — сервер выполняет роль терминала для TIMER MANAGEMENT (TS 102 223 §6.6.21/§7.4): запущенные картой таймеры отслеживаются в рамках сессии, TERMINAL RESPONSE на deactivate/get содержит остаток, а по истечении карта получает ENVELOPE (TIMER EXPIRATION). Живая карта использует это для повторения OTA-сессии после неудачного OPEN CHANNEL. **Конфигурация TR: словарь PLI** — редактируемые hex-значения для всех 22 квалификаторов PROVIDE LOCAL INFORMATION (TS 102 223 + TS 131 111). 10 квалификаторов имеют встроенные формы декодирования/кодирования: @@ -550,6 +554,16 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на Значения сохраняются на сервере до перезапуска. Apply → hex обновляется; Save → POST на сервер. +## SCP81 + +Вкладка **SCP81** управляет HTTP OTA (GP RAM over HTTP, GPC v2.2 Amendment B). BIP-канал карты всегда перенаправляется на локальный слушатель сервера: + +- **Capture (dump)** — принимает TCP-канал карты и записывает всё, что она отправляет (например, TLS ClientHello), не отвечая. Удобно для изучения запросов карты. +- **PSK TLS server** — отвечает на рукопожатие PSK-наборами TLS 1.2 из спецификации и ведёт HTTP-диалог административной сессии GP (заголовки `X-Admin-*`, `200` со строкой команд или `204 No Content`). Укажите **PSK Identity**, которую использует карта, и **PSK ключ (hex)**; ключ передаётся только локальному серверу, не сохраняется и не записывается в журнал. +- **Script** — сценарий команд, отдаваемых в сессии: **Память + ELF** (по умолчанию) отправляет `GET DATA FF21` (доступная энергонезависимая/энергозависимая память, число апплетов) и `GET STATUS P1=20/10` (реестр Executable Load File и модулей) как RAM/GP-команды в Command Scripting template по TS 102 226, по одной C-APDU на запрос; **None** закрывает каждую сессию ответом `204`. Свой список APDU можно задать через API. + +Строка состояния показывает слушатель, согласованную identity и активные каналы (байты in/out); журнал фиксирует OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA и каждый шаг TLS/HTTP. Те же функции доступны через `POST /api/scp81/bip` (см. `docs/api.md`). + ## PWA OTAMan — Progressive Web App. Можно установить для offline-использования через кнопку **INSTALL PWA** или через браузер. diff --git a/docs/api.md b/docs/api.md index 9ded76d..400dd57 100644 --- a/docs/api.md +++ b/docs/api.md @@ -46,6 +46,10 @@ connect and warns if versions are incompatible. | `/api/pli-qualifiers` | GET | List of qualifier codes with descriptions | | `/api/pli-dict` | GET | Current dictionary (hex values per qualifier) | | `/api/pli-dict` | POST | Update dictionary entries | +| `/api/scp81/bip` | POST | Start/stop the HTTP OTA listener (dump capture or PSK TLS server) | +| `/api/scp81/status` | GET | BIP terminal + listener state (channels, PSK identity seen) | +| `/api/scp81/log` | GET | HTTP OTA event log (`?after=`) | +| `/api/scp81/log-clear` | POST | Clear the HTTP OTA event log | ## Endpoint details @@ -378,6 +382,13 @@ optional hex for events that carry data. Returns the SW and any response data: {"sw": "9000", "data": "..."} ``` +Channel status (event `0x0A`, TS 102 223 §8.56) carries the Channel status TLV +`B8 02 `, where the status byte is the channel id (1–7) OR-ed +with the state bits (0x00 link not established / 0x40 TCP LISTEN / 0x80 link +established) and the info byte is `00` (no further info) or `05` (link +dropped). The server also sends this event automatically when a BIP link drops +outside a proactive command and the card subscribed to `0x0A`. + ### `GET /api/proactive-log` Returns the last 50 proactive commands fetched during CAT sessions, newest @@ -444,3 +455,87 @@ Returns the current PLI data dictionary as a qualifier-code map. Updates dictionary entries. Body is a map of qualifier code to hex value; keys must be known qualifiers and values valid hex, otherwise they are ignored. Returns the updated dictionary. + +### `POST /api/scp81/bip` + +Starts or stops the local target the card's BIP channel is redirected to. + +Dump mode (default) captures whatever the card sends (e.g. its TLS +ClientHello) without answering: + +```json +{"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443} +``` + +TLS mode runs the Phase B PSK TLS server (GPC v2.2 Amendment B): the PSK key +and optional identity are applied to the TLS handshake, and the GP HTTP +administration dialog (`X-Admin-*` headers, 200 with a command string or 204 +No Content) is served. `psk_hex` is required (the previous key is reused when +omitted); `psk_identity` restricts the accepted identity. The key is never +stored or logged. + +```json +{"action": "start", "mode": "tls", "host": "127.0.0.1", "port": 8443, + "psk_hex": "00112233445566778899aabbccddeeff", + "psk_identity": "89012345678901234567"} +``` + +Stop either mode with `{"action": "stop"}` (also disables the BIP terminal). + +### `GET /api/scp81/status` + +```json +{"bip": {"enabled": true, "target": "127.0.0.1:8443", "channels": [], "seq": 12}, + "listener": {"mode": "tls", "host": "127.0.0.1", "port": 8443, + "psk_identity": null, "identity_seen": "89012345678901234567"}} +``` + +### `GET /api/scp81/log` + +Returns the BIP/TLS event log (open/close, SEND/RECEIVE DATA hex, TLS +handshake and HTTP request/response records). `?after=` returns only +newer entries; `seq` echoes the latest sequence number. + +### `GET /api/scp81/script` + +Returns the active command script and the R-APDUs collected so far: + +```json +{"script": ["80CAFF2100", "80F28002024F0000"], "sent": 1, + "results": [{"index": 1, "sw": "9000", "rapdu": "FF210C810102..."}]} +``` + +The script is selected when starting the TLS listener with the `script` +parameter: `explore` (default — the reference administration server's command +sequence: GET DATA FF21 extended resources / free memory, GET STATUS P1=80 +Issuer Security Domain, GET DATA 0085 HTTP administration parameters, GET +STATUS P1=40 executable load files and P1=10 applications), `none` (answer +every POST with 204), or an explicit list of APDU hex strings. Each APDU is +delivered in an `AE 80 22 00 00` Command Scripting template +(TS 102 226 §5.2.1) with `X-Admin-Next-URI`; the card returns its R-APDUs in +the next POST's Response Scripting template, which is parsed and logged +(`script-rapdu`, `script-memory`). + +TLS mode also accepts `chunked` (**default `true`** — the reference server's +chunked framing; the card rejects a chunked response that also carries a +Content-Length) and `chunk_size` (default `0` — the whole response in one TLS +record, as in the decrypted reference session; a positive value writes the +head and each body piece as its own record). Both are echoed by +`GET /api/scp81/status`. + +`keep_alive` (default `true`, matching the reference session: the card sends +all its POSTs on one connection until the 204) ends the TLS connection after +each response +(after the card drained the BIP buffer, with `close_notify`, so the card +processes the script and opens a new connection for its next POST); +`compact_headers` (default `false`) drops the space after each header colon, +`apache_headers` (default `true`) adds Date/Server/X-Powered-By like the +reference servers and puts Transfer-Encoding before Content-Type, +`conn_header` (default `'none'` = omit the header, like the reference) +declares the connection fate, `tls_version` pins `1.1`/`1.0` for cards that +only speak the older record layer, `cipher` pins one suite, `next_uri` +overrides the per-command `X-Admin-Next-URI` (`%d` = command id; empty string +omits the header), `link_events` (default `true`) controls the automatic +Channel status events, `answer_delay` waits before answering a request. `keylog` writes the TLS traffic secrets to +the given file (SSLKEYLOGFILE format) for debugging captures — it contains key +material, use a temporary path. diff --git a/docs/scp81-findings.md b/docs/scp81-findings.md new file mode 100644 index 0000000..34bdbc8 --- /dev/null +++ b/docs/scp81-findings.md @@ -0,0 +1,200 @@ +# SCP81 / HTTP OTA live-card findings + +Living debug log for the HTTP OTA (RAM over HTTP) work against the live UICC. +Purpose: record **every attempted configuration and its outcome**, so the same +variations are not repeated. Add rows as tests are run; keep the confirmed +rules section current. + +Setup: `pysim_otaman_server` with a PC/SC reader, the PWA SCP81 tab (or +`POST /api/scp81/bip`), the card triggered by its SMS-PP push / the Location +status event. Server log at `GET /api/scp81/log`, script state at +`GET /api/scp81/script`, proactive history at `GET /api/proactive-log`. + +## RESOLVED 2026-09-16: the card never received the response - BIP TLV bug + +**Root cause:** our RECEIVE DATA TERMINAL RESPONSE encoded the channel-data +TLV length as a raw byte (`36 ED ...` for a 237-byte chunk). BER requires the +long form for lengths >127: **`36 81 ED ...`** (the reference terminal traces +use exactly that, e.g. `push_3311_success_req2.pcapng`). The card's BIP layer +silently mis-parsed the malformed TLV, so the TLS record bytes never reached +its TLS stack: no alert, no script processing, and the SD kept resuming its +dialog ("no complete script received") forever. Every delivery <=127 bytes +(handshake records, 204 responses) always worked - which is why the handshake +succeeded and only the large script responses "vanished". + +**Fix:** `_handle_bip_command` (cmd 0x42) BER-encodes the channel data length +(`36 81 ` above 127); regression test +`test_receive_data_tlv_long_form_length`. + +**Result with the live card** (one push, `explore` script, 5/5 commands): + +``` +#1 80CAFF2100 SW 9000 FF210B 81010D 8202C5D6 83020962 (13 applets, + free NV 50646 B, free volatile 2402 B) +#2 80F28002024F0000 SW 9000 ISD A000000003000000 + D276000005AAFFCAFE00 +#3 80CA008500 SW 9000 stored HTTP OTA parameters +#4 80F24002024F0000 SW CAFE 127-byte ELF registry page (more available) +#5 80F21002024F0000 SW CAFE 127-byte applications page (more available) +``` + +Every command returned `X-Admin-Script-Status: ok` on the card's own POST to +the incremented `X-Admin-Next-URI`, on the same keep-alive connection, and the +session ended with 204 + mutual close_notify - exactly the reference flow. +`SW CAFE` marks a truncated 127-byte page: the remaining entries need a +continuation GET STATUS (P2=02 with the last AID as search criterion). + +## Live card facts (verified via the reader, 2026-09-16) + +- `80CAFF2100` (GET DATA extended card resources) **works**: + `FF21 0B 81 01 0D 82 02 C5 D6 83 02 09 62` -> 13 applets installed, + free NV memory `0xC5D6` = 50646 B, free volatile `0x0962` = 2402 B. +- `80CA008500` (GET DATA HTTP administration parameters) **works** and returns + the SD's stored OTA configuration: `8A 09 "localhost"`, `8B 14 `, + `8C 01 "/"` (stored URI), `85 14 `, `86 07 00 01 25 03 00 10 00` + (retry counter 1, timer **10 minutes**), `02 40 01` (KVN/KID), APN-ish + `C7 04 03 47 50 42`, destination `BE 05 21 5B D5 05 02` = 91.213.5.2. +- `80F28002/80F24002/80F21002 ...4F0000` return `6985` through the reader when + the ISD is not the current DF; the reference platform sends + `80F28002024F0000` over HTTP, where the SD executes inside the ISD. +- `SELECT` of the ISD (`00A4040008A000000003000000`) returns `6112`; + a subsequent GET RESPONSE (`00C0000012`) returns `6D00`. +- BIP device identities: OPEN CHANNEL uses destination `0x82`; SEND/RECEIVE + DATA carry channel `0x21..0x27` (e.g. `82 02 81 22` = channel 2). +- Subscribed events (`99 03`): `03` location status, `09` data available, + `0A` channel status. +- A Location status event re-triggers the OTA session only while the last + session is incomplete; after a clean session end the card waits for a push. +- The SD stores a 10-minute retry timer (`25 03 00 10 00`). + +## Confirmed rules (with evidence) + +1. **The card needs a clean TLS close, with the close_notify actually + fetched.** Keep-alive (no close) -> fatal `unexpected_message` after it + fetched the response. `close_notify` sent *after* the buffer drained is + never fetched (the card ends the dialog on its own first). Correct order: + send it while the response still waits, then wait for the drain, then + close. +2. **The card's abort alert is `fatal unexpected_message`** - decrypted with + the listener's `keylog` option (see `tools/scp81_decrypt.py`). +3. **A dropped link must be signalled (TS 102 223 7.5.11), and only after the + buffered data was fetched.** Signalling the drop while bytes are still in + the BIP buffer makes the card abort the fetch mid-record and end the + session. Omitting the signal entirely hangs the SD: after a listener + restart dropped the channel silently, the card ignored pushes and location + events for minutes; a manual `ENVELOPE (Channel status, B8 02 02 05)` + immediately made it start a fresh session. +4. **The Next-URI shape matters.** A path-only or absolute Next-URI (`/`, + `/1`, `http://127.0.0.1:8443/api/scp81`) draws the fatal + `unexpected_message`; the reference-style relative path **with a query** + (`/adminserver?PHPSESSID=...&apdu_id=101`) does not. +5. **The reference administration server** (`samples/HTTP_OTA/ + httpota_adminserver_php_v2`) uses: command script + `AE 80 22 00 00`; response `200` with + `X-Admin-Protocol`, `X-Admin-Next-URI: /adminserver?PHPSESSID=&apdu_id=`, + `Content-Type: ...;version=1.0`, **chunked** body (100-byte chunks); + the card returns the R-APDU as the body of its next POST with + `X-Admin-Script-Status: ok`; the server ends with `204`. + Its log proves the card followed the Next-URI three times within 1-2 s per + step (`Got next request ... Script status is 'ok' - storing R-APDU data`). +6. **`chunked=false` (Content-Length) has never produced an R-APDU.** All + sessions that ended silently (clean close, no alert, no POST) used + `Content-Length`. Hypothesis: the card only treats a chunked body as a + command script; with Content-Length it sees an empty script, executes + nothing and ends the session gracefully. + +## The one fully successful session trace (ground truth) + +`traces/HTTPOTA_session_3311_success1.pcap` (2019, **plain HTTP on port 80**, +one TCP connection for the whole session, card `3311` - *not* our UICC): + +``` +POST /server/adminagent?cmd=1 <- card (trigger URI, with query!) +200 OK + Date/Server + X-Admin-Protocol + + X-Admin-Next-URI: /Download?req=1 + Content-Length: 11 + + Content-Type: .../card-content-mgt;version=1.0 + body: ae 80 22 05 80 ca 00 85 00 00 00 (script: GET DATA 0085) +POST /Download?req=1 <- card, SAME connection + X-Admin-Script-Status: ok + Content-Type: .../card-content-mgt-response;version=1.0 + Transfer-Encoding: chunked + body: "8 +" af 80 23 02 6a 88 00 00 "0 + +" (R-APDU SW 6A88) +200 OK + X-Admin-Next-URI: /Download?req=2 + Content-Length: 14 + body: ae 80 22 08 80 f2 80 02 02 4f 00 00 00 00 (GET STATUS P1=80) +POST /Download?req=2 -> X-Admin-Script-Status: ok, chunked + body: "1F +" af 80 23 19 <25-byte R-APDU ... 90 00> 00 00 "0 + +" +200 OK + /Download?req=3 + 11-byte script +POST /Download?req=3 -> status ok, R-APDU 23 02 6d 00 (SW 6D00) +204 No Content <- session ends +``` + +Confirmed from it: the card echoes the `X-Admin-Next-URI` (path *and* query) +verbatim; its response POST goes on the **same TCP connection**; its response +is the `AF 80 23 00 00` indefinite Response Scripting template +(in a chunked body, with `X-Admin-Script-Status`); the server's script +`AE 80 22 00 00` matches ours byte for byte; the server uses +`Content-Length` (not chunked), no `Connection` header (implicit keep-alive), +and ends with 204. + +## Attempt matrix + +| # | transport | framing | Next-URI | close | link events | outcome | +|---|-----------|---------|----------|-------|-------------|---------| +| 1 | dump mode only | - | - | - | off | OPEN CHANNEL + ClientHello captured (Phase A) | +| 2 | TLS, 204 only | - | - | yes | off | session completes cleanly, no alert (Phase B, live) | +| 3 | TLS + script | chunked 100 | `/N` | early (raced fetch) | on | fetch truncated (237/399); card re-opened and repeated its POST with `X-Admin-Resume: true` -> breakdown-resume works | +| 4 | TLS + script | chunked 100 / single | `/1`, `/`, absolute | keep-alive | off | full fetch, then fatal `unexpected_message` (Next-URI shape) | +| 5 | TLS + script | single | none (`""`) | keep-alive | off | no alert, no POST, session left open (spec: no Next-URI -> no response) | +| 6 | TLS + script | chunked 100 | reference | close_notify after drain | off | full fetch, alert (notify never fetched) | +| 7 | TLS + script | chunked 100 | reference | close_notify before drain | off | full fetch, alert (head split into its own record) | +| 8 | TLS + script | **single record** | reference | drain + close_notify | off | **no alert**, card CLOSE CHANNELs, no R-APDU (`chunked=false` -> suspected empty script) | +| 9 | TLS + script | single record | reference | keep-alive (no close) | off | fatal `unexpected_message` (close required) | +| 10 | TLS + script | chunked 100 | reference | drain + close_notify | off | full fetch, then alert; later the SD hung until a manual link-dropped event | +| 11 | TLS + script | single record | reference | keep-alive | off | fatal `unexpected_message` after the full fetch (no close) | +| 12 | TLS + script | single record | reference | drain + close_notify | off | **no alert**, card CLOSE CHANNELs, no R-APDU (`Content-Length`) | +| 13 | TLS + script | chunked100 + single | reference | drain + close_notify | off | no alert, no R-APDU | +| 14 | TLS + script | single record | reference | keep-alive | off | alert again | +| 15 | TLS + script | chunked 100 | reference | keep-alive | on | alert (small records, ruled out record size) | +| 16 | TLS + script | single record | reference | keep-alive, no `Connection` header | on | alert | +| 17 | TLS + script (RFM! `00D6` write-probe) | chunked, single | reference | drain + close_notify | on | no alert, no R-APDU; EF.SPN unchanged - **RFM result is void**: the ISD only accepts RAM commands | + +All script attempts used the `explore` list, except #8-#17 which used only +`80CAFF2100` (or the RFM probe). #3-#17 ran with the card's PSK identity +`89390…903` (push trigger) or `89701…` (event trigger). + +**Status after #17 (superseded by the 2026-09-16 resolution above):** the +failures were caused by the BIP TLV length bug, not by the HTTP/TLS details; +resume mode was a symptom (the working session even started as a resume). The +key working recipe (also now the server default): one keep-alive connection, +Apache-style headers, `Transfer-Encoding: chunked` body with the script in +one TLS record, no Connection header, `X-Admin-Next-URI` with a query whose +command id increments. + +**Also confirmed:** a TLS half-close (close_notify then keep reading for the +card's POST which RFC 5246 leaves open in practice) cannot be done with +CPython's `ssl`: `SSLSocket.unwrap()` with a short timeout raises and poisons +the session (tested), so the `half_close` option is a documented no-op. + +## Next tests / work + +1. **Continuation pages:** follow `SW CAFE` (127-byte listing pages) with + GET STATUS P1=40/10 P2=02 using the last returned AID as the search + criterion, and append the pages to the result set (memory + full ELF and + application registries). +2. **UI:** show the decoded memory/applications results (and page merging) in + the SCP81 tab; expose the framing options there. +3. Load/store operations (RAM INSTALL/LOAD) over SCP81 using the same recipe. + +## Tooling + +- `tools/scp81_decrypt.py ` - decrypts the dialog from + `GET /api/scp81/log` plus the listener's `keylog` file (SSLKEYLOGFILE + format; PSK-AES128-CBC-SHA256, TLS 1.2 PRF + OpenSSL CLI). Shows each + record's plaintext and any alert level/description. +- Start the listener with `"keylog": "/tmp/.../scp81.keys"` to collect the + secrets (contains key material - use a temp path, never commit). diff --git a/frontend/index.html b/frontend/index.html index b08d480..0f9ce0d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v2.1.2

+

OTAMan SIM OTA with a Human Face v2.1.5

@@ -35,6 +35,7 @@
+ @@ -954,6 +955,60 @@
+ +