Files
otaman/docs/api.md
T
catarrh 20fbfd99a5 scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)
SCP81:
- New listener mode "passthru": no local listener - the card's BIP
  channels connect straight to a configured external platform
  (host/port required), which terminates TLS and runs the dialog.
  The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and
  clears them on stop. PWA mode select, hint, Start validation; TLS PSK
  stays the default.

Proactive command decoding (Phone tab log):
- SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI +
  number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as
  SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and
  the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a
  TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to
  the raw hex line.
- PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223
  V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled
  "Reserved for GSM (Timing Advance)". Fixed in the server dict and
  both frontend tables.

Header:
- Compact ADM badge next to the card indicator: "ADM ✓" green when
  pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a
  card session or when the server is down; updated ahead of the card
  state-key early return so it never disturbs the connect/reset flow.

File manager:
- Sort pills (FID/Name), Probe all files button and progress line are
  pinned above the tree instead of scrolling with it; the tree box cap
  grows from 420px to 65vh.

SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend.
2026-09-16 15:29:01 +03:00

24 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 identities, handshake identity)
/api/scp81/log GET HTTP OTA event log (?after=<seq>)
/api/scp81/log-clear POST Clear the HTTP OTA event log
/api/scp81/queue POST Replace the SCP81 command script (optionally force-restart)
/api/scp81/script GET Active command script + execution state and R-APDUs
/api/scp81/psk-map POST Replace the PSK table of a running TLS listener
/api/scp81/gen-install POST Generate the RAM APDU list for a .cap (no queueing)

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)
load_block_size no Bytes of load-file payload per LOAD APDU, 1240. When empty/omitted the server auto-fits: the largest size whose SCP80 secured packet still encodes into one SMS (140 octets; e.g. 107 for the 3DES spi1=16/spi2=01 configuration). An explicit value larger than the fitting size is clamped; over SCP80 the default 240 does not fit and used to fail with pySim's "Cannot encode command in a single SMS".

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",
 "load_block_size": 107,
 "load_block_size_requested": null,
 "load_block_size_clamped": false}

load_block_size is the effective size used for the LOAD blocks, load_block_size_requested echoes an explicit load_block_size (null = auto-fit) and load_block_size_clamped is true when the requested size was reduced to fit one SMS.

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 captures whatever the card sends (e.g. its TLS ClientHello) without answering:

{"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443}

Pass-through mode (mode: "passthru") starts no local listener: every BIP channel the card opens is connected to the configured external platform (host/port are required — no defaults), which terminates TLS and runs the administration dialog; the address the card requests is only logged. The status API reports mode: "passthru" with the target while it runs.

{"action": "start", "mode": "passthru", "host": "203.0.113.10", "port": 10174}

TLS mode runs the Phase B PSK TLS server (GPC v2.2 Amendment B): the PSK table is 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_map is the lookup table for the identity the card presents in the TLS handshake — the PWA sends it from the card presets ({identity, psk_hex} objects or an {identity: psk_hex} map); a handshake whose identity is not listed fails with the log entry tls-psk-unknown. The legacy single-key form psk_hex (with optional psk_identity, empty = accept any identity) is still accepted; when both are omitted the table of the previous start is reused. Keys are never stored or logged.

{"action": "start", "mode": "tls", "host": "127.0.0.1", "port": 8443,
 "psk_map": [{"identity": "89012345678901234567",
              "psk_hex": "00112233445566778899aabbccddeeff"}],
 "script": ["80CAFF2100", "80F28002024F0000"], "script_kind": "Explore"}

script is the APDU list served to the card (an explicit list, or none); the server is agnostic to what the APDUs do. script_kind is an optional label for the logs/results. Omitting script keeps the configured script and its run progress.

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_identities": ["89012345678901234567"], "psk_wildcard": false,
              "identity_seen": "89012345678901234567", "identity_matched": true}}

Listener modes: tls (local PSK TLS server), dump (capture-only TCP listener) and passthru (no local listener; the BIP channels go straight to host:port, e.g. an external HTTP OTA platform — reported as {"mode": "passthru", "host": ..., "port": ..., "target": "host:port"}).

psk_identities lists the identities the listener accepts (keys are never exposed); psk_wildcard marks the legacy single-key mode. identity_seen / identity_matched reflect the last handshake: an unknown identity is logged as tls-psk-unknown and the handshake fails.

POST /api/scp81/psk-map

Replaces the PSK table of the running TLS listener (the PWA pushes card-preset edits without a listener restart):

{"psk_map": [{"identity": "89012345678901234567",
              "psk_hex": "00112233445566778899aabbccddeeff"}]}

Returns {"ok": true, "identities": [...], "listener": {...}}; entries without an identity or a valid key are skipped, and an empty table is rejected.

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.

POST /api/scp81/queue

Replace the SCP81 command script (used by the Remote APDU tab's RAM chain "Queue in SCP81" and the PWA's "Restart script"). Body {"apdus": ["80E60C002E...", ...]} (or a single apdu), optional kind and force. Entries that already are Command Scripting templates (AA.../AE80..., the expanded format) are sent verbatim instead of being wrapped again. Refused while a script is mid-run unless forced; queuing resets the execution progress.

POST /api/scp81/gen-install

Generate the RAM (GP) APDU sequence for a .cap without touching the listener or the running script; the PWA's "Install from .cap" script template stores the returned list. The .cap is parsed server-side (same parser as /api/ram-install) and expanded to INSTALL [for load] -> LOAD blocks (240-byte payloads) -> INSTALL [for install]; the file itself is never stored.

{"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00",
 "install_params": "", "stk_params": "", "make_selectable": true}

sd_aid empty = the ISD. Responds with {"ok": true, "apdus": [...], "load_file_aid": ..., "module_aid": ...}.

GET /api/scp81/script

Returns the configured command script and the execution state:

{"script": ["80CAFF2100", "80F28002024F0000"], "next": 2, "total": 2,
 "done": [0, 1], "kind": "Explore",
 "pending": {"index": 17, "pos": null, "page": true, "apdu": "80F28003024F0000"},
 "pages": 11, "pages_queued": 0, "complete": false,
 "results": [{"index": 1, "pos": 0, "page": false, "sw": "9000",
              "apdu": "80CAFF2100", "rapdu": "FF210C810102..."}]}

next is the index of the next script APDU to send; done lists the script indices the card reported. pending describes the C-APDU awaiting the card's X-Admin-Script-Status report as {index, pos, page, apdu} (pos = script index, null for an auto continuation page) or null; pages counts the continuation pages queued so far and pages_queued those not yet sent. complete is true when every configured APDU was reported and nothing is in flight — a script can therefore be complete while a listing page is still being fetched (pending.page = true), which is tracked separately from the script's own progress. results entries carry the send order (index), the script position (pos, null for continuation pages) and the page flag.

Execution tracking and resume: an APDU counts as executed only when the card reports it in the next POST's Response Scripting template. A POST with X-Admin-Resume continues with the unexecuted tail (the pending APDU is resent if its report never arrived), a POST without it is a fresh dialog where the script runs from the start, and a completed script closes the session with 204.

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). Long GET STATUS listings that answer 63 10 / CA FE ("more data available") are auto-continued with the same command carrying P2.b1=1.

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.