Files
otaman/docs/api.md
T
catarrh 175ca934d8 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 <len>) 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
2026-09-16 01:50:29 +03:00

18 KiB
Raw Blame History

pysim-otaman-server — HTTP API reference

The server exposes a JSON HTTP API under /api/*. All responses carry Access-Control-Allow-Origin: * (plus Access-Control-Allow-Private-Network: true on the preflight), so the API is reachable from a separately-hosted PWA.

Version compatibility

Server PWA (OTAMan) Status
1.x.x 1.x.x Compatible
0.x.x 1.x.x Outdated — update server
2.x.x+ 1.x.x ⚠️ Server newer — update PWA

The server reports its version via GET /api/version. The PWA checks this on connect and warns if versions are incompatible.

Endpoints

Endpoint Method Purpose
/api/version GET Server version string
/api/status GET Card reader + card info + current selection
/api/command POST pySim command (equip, status, tree, etc.)
/api/commands GET List available pySim commands
/api/tree POST File tree browser for given FID/name
/api/select POST Select a file by name or FID
/api/read POST Read file content
/api/write POST Write raw hex data to a file
/api/apdu POST Raw APDU send
/api/help POST pySim help for a given command
/api/send-ota POST SCP80 OTA secured packet delivery
/api/ram-install POST Install a Java Card .cap file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install])
/api/sp-verify POST Verify secured packet against pySim reference
/api/menu GET Current STK menu (title + items + active)
/api/menu-select POST ENVELOPE(Menu Selection) with item_id
/api/menu-respond POST TERMINAL RESPONSE for paused STK command
/api/stk-status GET STK session state (active/pending/type)
/api/events GET Event list from SET UP EVENT LIST
/api/event-send POST Send ENVELOPE(Event Download)
/api/proactive-log GET Last 50 proactive commands
/api/status-poll POST Manual STATUS poll + FETCH if 91XX
/api/rescue POST Re-send TERMINAL PROFILE to recover CAT session
/api/poll-status GET Background STATUS polling state
/api/poll-toggle POST Enable/disable background polling
/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=<seq>)
/api/scp81/log-clear POST Clear the HTTP OTA event log

Endpoint details

GET /api/version

Returns server version for compatibility checking.

Example response:

{"version": "2.1.2"}

GET /api/status

Card reader, card type, current selection, and card state.

GET /api/commands

List all available shell commands for the current card profile.

POST /api/command

Execute any pysim-shell command.

{"cmd": "select MF"}

Returns:

{"output": "..."}

POST /api/apdu

Send a raw APDU to the card.

{"apdu": "00A4040000..."}

Returns:

{"response": "...", "sw": "9000"}

POST /api/help

Get structured help for a shell command.

{"cmd": "apdu"}

Returns:

{"usage": "apdu [-h] [--expect-sw EXPECT_SW] [--raw] APDU", "description": "...", "args": [{"name": "APDU", "type": "positional", "help": "..."}]}

POST /api/send-ota

Send an OTA command (SCP80) to the card via SMS-PP-DOWNLOAD ENVELOPE. The secured packet is delivered in an SMS-DELIVER TPDU wrapped in an ENVELOPE command.

Request body:

{
  "sp": "00201516011515b00000...",
  "spi1": "16",
  "spi2": "01",
  "kic": "15",
  "kid": "15",
  "tar": "b00000",
  "cntr": "0000000001",
  "kicKey": "D6FCC023...",
  "kidKey": "1B07E7E0..."
}

Response (delivery PoR):

{"success": true, "sw": "9000", "response_data": "027100000e0a...",
 "por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
         "decoded": {"number_of_commands": 1, "last_status_word": "6e00",
                      "last_response_data": ""}}}

Response (submit PoR): PoR is extracted from the SMS-SUBMIT TPDU fetched via a proactive command (FETCH). The response contains the same por structure if decoding succeeds.

The SPI2 por_in_submit bit (0x20) selects submit-mode PoR.

POST /api/ram-install

Install a Java Card .cap file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE and its PoR is checked; the sequence aborts on the first PoR error. The .cap archive (a ZIP of nested components) is parsed server-side in _cap_parse; no external tooling is required.

Request body:

{
  "cap_hex": "DECAFFED...",
  "sd_aid": "A000000003000000",
  "install_params": "C90000",
  "stk_params": "",
  "nv_quota": 0,
  "volatile_quota": 0,
  "make_selectable": true,
  "spi1": "0E", "spi2": "01",
  "kic": "15", "kid": "15",
  "tar": "000000",
  "cntr": "0000000001",
  "kicKey": "D6FCC023...",
  "kidKey": "1B07E7E0..."
}
Field Req Description
cap_hex yes Even-length hex of the .cap file (zipped Java Card CAP), max 48 kB (98304 hex chars)
sd_aid no Security Domain AID for INSTALL[for load]; empty → default ISD A000000003000000
install_params no Hex C9 TLV install parameters; if empty, gen_install_parameters() is used with the quota/stk params
stk_params no Hex CA TLV (TS 102 226 §8.2.1.3.2.1) for SIM toolkit app-specific params
nv_quota / volatile_quota no Integer memory quotas (bytes) for gen_install_parameters()
make_selectable no If true (default), final INSTALL uses P1=0C (install + make selectable)

Response (success):

{"success": true, "failed_step": null,
 "steps": [{"name": "install_for_load", "apdu": "80E60200...", "por_status": "por_ok", "sw": "9000"},
           {"name": "load_0", "apdu": "80E80000...", "por_status": "por_ok", "sw": "9000"},
           {"name": "install_for_install", "apdu": "80E60C00...", "por_status": "por_ok", "sw": "9000"}],
 "final_cntr": "0000000004",
 "load_file_aid": "A000000003000000",
 "module_aid": "A000000003000000",
 "application_aid": "A000000003000000"}

Response (failure):

{"success": false, "failed_step": "load_1",
 "steps": [{"name": "install_for_load", "por_status": "por_ok", "sw": "9000"},
           {"name": "load_1", "por_status": "rc_error", "sw": null}],
 "error": "..."}

The steps array contains one entry per GP command. final_cntr is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.

POST /api/sp-verify

Cross-check a secured packet against pySim's OtaDialectSms.encode_cmd reference. Returns the JS-generated packet, pySim reference, a match flag, and the decoded SPI fields.

{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
 "cntr": "0000000001", "apdu": "00a40000023f00",
 "kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}

Response:

{"js_sp": "...", "py_sp": "...", "match": true,
 "diffs": [], "spi": {"counter": "counter_must_be_higher", ...}}

GET /api/menu

Returns the SIM Toolkit SETUP MENU captured from the card's TERMINAL PROFILE response at startup. Empty {"items": []} if the card didn't send a menu.

Response:

{"command_number": 1, "items": [{"id": 128, "text": "Настройки/Settings"}],
 "title": "Alfa Mobile", "active": false}

POST /api/menu-select

Sends an ENVELOPE(MENU SELECTION) with the selected item ID, then handles the card's proactive response (DISPLAY TEXT or SELECT ITEM).

{"item_id": 128}

Response:

{"type": "display_text", "text": "Hello", "sw": "9122"}

or

{"type": "select_item", "items": [{"id": 1, "text": "Sub-menu"}], "sw": "9122"}

POST /api/menu-respond

Sends TERMINAL RESPONSE to the current proactive command with the given result code. Continues the proactive chain if the card responds with 91XX. If no response arrives within --menu-timeout seconds (default 60, 0 disables), the server watchdog sends the timeout result itself.

{"result": "ok", "item_id": 1}
result TERMINAL RESPONSE code Meaning
ok 0x00 Command performed successfully
cancel 0x10 Proactive session terminated by the user
back 0x11 Backward move in the proactive session requested by the user
timeout 0x12 No response from the user

GET /api/stk-status

Returns the current STK session state.

{"active": true, "pending": true, "pending_type": "select_item"}

POST /api/read

Read file content. Auto-detects transparent vs record files.

{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"}

Returns transparent data:

{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
 "apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}

Returns records:

{"success": true, "sw": "9000", "file_type": "linear_fixed",
 "records": [{"num": 1, "data": "..."}, {"num": 2, "data": "..."}],
 "apdu_times": [{"type": "select", "ms": 12},
                {"type": "read_record", "ms": 11}, {"type": "read_record", "ms": 13}]}

apdu_times reports each command's duration (command sent to response received) classified as select, read_binary or read_record; the PWA uses it for snapshot timing statistics. Other commands are not reported.

POST /api/write

Write raw hex data to a file.

{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]}

For record files:

{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]}

Returns:

{"success": true, "sw": "9000"}

POST /api/select

Select a file by name or FID, with optional parent selection.

{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"]}

parent_path lists the path segments from MF to the parent (ADF names or FIDs); the legacy single-segment parent_sel is still accepted but is only unambiguous for ADFs. Resolution is strictly parent-scoped: model-known files are selected through the requested parent only (pySim select_file()), never via pySim's global selectables or its probe_file() model injection, so a same-FID file under another parent is never picked and the filesystem model is not modified. allow_probe: true (PWA custom files) additionally allows a model-unknown 4-hex FID to be selected directly; any temporary model object created for it is detached again before the response is sent.

Returns:

{"name": "EF.ICCID", "fid": "2FE2", "file_type": "transparent",
 "file_size": 10, "record_len": null, "num_of_rec": null,
 "fci_hex": "621082024021...",
 "apdu_times": [{"type": "select", "ms": 12}], "exists": true}

fci_hex is the raw FCP template ('62') from the SELECT response, used by the PWA's Exact FCI checks; file_size / record_len / num_of_rec drive the profiler's size and record checks. When the file does not exist the endpoint responds 404 with {"error": "...", "exists": false}.

POST /api/tree

Get directory listing with typed children.

{"name": "MF", "fid": "3F00"}

Use parent_path (or the legacy parent_sel) to list a subdirectory, e.g. {"name": "DF.GSM-ACCESS", "fid": "5F3B", "parent_path": ["MF", "ADF.USIM"]}.

Returns:

{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}

GET /api/events

Returns the event list captured from the card's SET UP EVENT LIST (an array of event byte values, or [] when none was received).

POST /api/event-send

Sends an ENVELOPE(Event Download) for a subscribed event.

{"event_type": 4, "event_data": "01A0"}

event_type is required (the SET UP EVENT LIST event byte); event_data is 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 <status> <info>, where the status byte is the channel id (17) 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 first:

[{"type_hex": "25", "type_name": "SET UP MENU", "elapsed": 3.2, "bytes": 97}]

POST /api/status-poll

Manually sends STATUS (F2) and, if the card answers 91XX, runs the proactive chain (FETCH → TERMINAL RESPONSE) until it settles. Returns:

{"sw": "9000", "proactive": true}

POST /api/rescue

Recovers a stuck CAT session by clearing the pending state and re-sending the TERMINAL PROFILE. Returns whether a menu and event list were captured again:

{"menu": true, "events": [4, 5]}

GET /api/poll-status

Background STATUS polling state.

{"enabled": true, "interval": 300}

POST /api/poll-toggle

Turns background STATUS polling on or off.

{"enabled": true}

Returns the new state ({"enabled": ..., "interval": ...}).

GET /api/pli-qualifiers

Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.

[{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}]

GET /api/pli-dict

Returns the current PLI data dictionary as a qualifier-code map.

{"00": "0291...", "0A": "64"}

POST /api/pli-dict

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:

{"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.

{"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

{"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=<seq> 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:

{"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 <len> <apdu> 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.