cdd3820064
release / release (push) Waiting to run
The import created presets with the store's default counter (1). Cards whose counters are already past it had to be edited preset by preset afterwards. - The outfile preview form gains a "Start counter value" field (default 1, the store's value). The value is written into every created keyset of every imported card - parse_outfile's `start_counter`, 1-10 hex digits, whitespace stripped and uppercased; a bad value raises and the endpoint answers 400. Without it the store's default still applies (the field is omitted), so the API keeps its old behaviour. - The preview shows the counter; an import stores the protocol's 10-digit form (counter_hex). - Docs: api.md (the field), help EN/RU (the form). - Version strings + the PWA cache bumped together (simple-v459). Tests: the parser (every keyset, normalisation, unset, the bad values), the endpoint (the preview's keysets, the 400, the stored padded form), the PWA (the field's default, the controls' normalisation, both bodies). 1008 Python + 953 frontend green.
2041 lines
105 KiB
Markdown
2041 lines
105 KiB
Markdown
# pysim-simple-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 (SIMple) | Status |
|
||
|--------|-------------|--------|
|
||
| same major | any | ✅ Compatible |
|
||
| older major | any | ❌ Outdated — update server |
|
||
| newer major | any | ⚠️ Server newer — update PWA |
|
||
|
||
The server reports its version via `GET /api/version`. The PWA checks this on
|
||
connect and compares the major version (a 2.x server is flagged as outdated by
|
||
a 3.x PWA).
|
||
|
||
## 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/cardinfo` | GET | pySim `cardinfo` output |
|
||
| `/api/tree` | POST | File tree browser for given FID/name (the DF holding EF_PBR gains the PBR-referenced files) |
|
||
| `/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/verify-adm` | POST | Verify the card's ADM PIN (from the matched card preset) |
|
||
| `/api/presets` | GET | Card preset store: all presets + the store path |
|
||
| `/api/presets` | POST | Create a card preset (server-side store) |
|
||
| `/api/presets/update` | POST | Update a preset (`{id, fields}`; the counter may be raised) |
|
||
| `/api/presets/delete` | POST | Delete a preset (`{id}`) |
|
||
| `/api/presets/import` | POST | Import presets (`{presets: [...], mode: merge\|replace}`) |
|
||
| `/api/presets/import-outfile` | POST | Import a card vendor's **outfile** (`{text, name?, algorithm16?, tars?, tar_keyset?, mode?}`; without `mode` a dry-run preview) |
|
||
| `/api/tar-probe` | POST | Probe the card's registered OTA applications (one secured packet per TAR, TS 101 220 Annex D) |
|
||
| `/api/counter-probe` | POST | Bounded counter sync: probe increasing counter values (start+1, +2, +4, …) with a read-only command until the card accepts one (ceiling/attempt-capped), then persist accepted+1 |
|
||
| `/api/esim/chip` | GET | eUICC chip details (EID, EUICCInfo1/2, configured addresses) |
|
||
| `/api/esim/profiles` | GET | Installed eSIM profiles with their metadata |
|
||
| `/api/esim/notifications` | GET | Pending eSIM notifications (read-only) |
|
||
| `/api/esim/profile` | POST | Enable/disable an eSIM profile (card re-initialized after the switch) |
|
||
| `/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]); `delete_package` prepends a cascade DELETE on the package AID |
|
||
| `/api/ram-batch/start` | POST | Start a **batch operation** (v3.28.0; kinds v3.29.0): `install-cap` (the same `.cap` per equipped card), `reinstall-app` (DELETE + INSTALL [for install]) or `delete` (one GP DELETE), using each card's preset (matched by ICCID) |
|
||
| `/api/ram-batch/status` | GET | The batch state: phase, current card, counts and the attempts report (card-free) |
|
||
| `/api/ram-batch/stop` | POST | Stop the batch — a running install finishes first (card-free, so it works while the install holds the card lock) |
|
||
| `/api/ram-batch/clear` | POST | Drop the finished batch's report (`409` while a batch is active) |
|
||
| `/api/ram-install-app` | POST | INSTALL [for install] (deletes the instance first, tolerating 'not found') / INSTALL [for make selectable] for an already loaded package (no re-load) |
|
||
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
|
||
| `/api/cap-compat` | POST | CAP compatibility test: LOAD only to the block completing the Import component (nothing committed) |
|
||
| `/api/test/run` | POST | Start a test script (actions + proactive expectations; `script` inline or `script_id` from the store) or a test suite (`suite_id`; ordered scripts, one card session) |
|
||
| `/api/test/status` | GET | Test script/suite run state, per-step results and the suite report |
|
||
| `/api/test/stop` | POST | Request a running test script/suite to stop |
|
||
| `/api/test/clear` | POST | Clear the finished run report |
|
||
| `/api/test/scripts` | GET | Test script store (server-side `test_scripts.json`): all scripts + the store path |
|
||
| `/api/test/scripts` | POST | Create a test script (`{name, steps, suite_id}` or `{script}`; the script joins the suite) |
|
||
| `/api/test/scripts/update` | POST | Update a test script (`{id, script}`; a changed `suite_id` is a move) |
|
||
| `/api/test/scripts/delete` | POST | Delete a test script (`{id}`; detaches it from its suite first) |
|
||
| `/api/test/scripts/move` | POST | Move a script to another suite (`{id, suite_id}`) |
|
||
| `/api/test/scripts/copy` | POST | Copy a script into another suite (`{id, suite_id, name?}`) |
|
||
| `/api/test/scripts/import` | POST | Import scripts (`{scripts: [...], mode: merge\|replace, suite_id?}`; accepts the old localStorage export shape) |
|
||
| `/api/test/suites` | GET | Test suite store (server-side `test_suites.json`): all suites + the store path |
|
||
| `/api/test/suites` | POST | Create a suite (`{name, require_adm?, start_reset?, scripts?}` or `{suite}`) |
|
||
| `/api/test/suites/update` | POST | Update a suite (`{id, suite}`; the member set changes through the script endpoints) |
|
||
| `/api/test/suites/delete` | POST | Delete a suite (`{id}`; refused while it holds scripts) |
|
||
| `/api/test/suites/import` | POST | Import a suite bundle (`{suites, scripts, mode}`; script references remapped) |
|
||
| `/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/net-sim` | POST | Run a network-condition scenario (attach, service loss, roaming, churn, 2G, SMS, CB, AUTHENTICATE) |
|
||
| `/api/net-state` | GET | Cached network-state monitor (service, location, per-file state) |
|
||
| `/api/net-state-refresh` | POST | Re-read the monitored files (optional `files` list) and return the updated monitor state |
|
||
| `/api/arr` | GET | Cached EF.ARR copies (the FCP '8B' security attribute references): records per ARR file |
|
||
| `/api/pbr` | GET | Cached EF.PBR phonebook reference records (the raw TS 31.102 4.4.2.1 structure) |
|
||
| `/api/mcc-mnc` | GET | Search the optional MCC/MNC operator list (`?q=`; `?random=1&exclude=`) |
|
||
| `/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/terminal-profile` | GET | Current TERMINAL PROFILE (hex) + CLI default |
|
||
| `/api/terminal-profile` | POST | Set and re-send the TERMINAL PROFILE at runtime (in-memory) |
|
||
| `/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, `owner`) |
|
||
| `/api/scp81/log` | GET | HTTP OTA event log (`?after=<seq>`) |
|
||
| `/api/scp81/log-clear` | POST | Clear the HTTP OTA event log |
|
||
| `/api/bip/control` | POST | Start/stop the generic BIP session (`sink` / `passthru` / `redirect`) |
|
||
| `/api/bip/status` | GET | BIP session state + owner (same shape as `/api/scp81/status`) |
|
||
| `/api/bip/log` | GET | BIP event log (`?after=<seq>`) |
|
||
| `/api/bip/log-clear` | POST | Clear the BIP 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:**
|
||
```json
|
||
{"version": "3.0.1"}
|
||
```
|
||
|
||
### `GET /api/status`
|
||
|
||
Card reader, card type, current selection, and card state. Reported fields:
|
||
`reader`, `connected`, `card_present`, `card_session` (increments on every
|
||
equip/disconnect), `proactive_seq`, `equipping`, `auto_equip`, `card`,
|
||
`profile`, `eid`, `euicc`, `iccid`, `app_ready`, `adm_verified`, `atr`,
|
||
`cla_byte`, `sel_ctrl`, `current_selection`, `channels`.
|
||
|
||
`eid` is the eUICC identifier read from the ISD-R at equip (`null` for a
|
||
non-eUICC card) and `euicc` says whether the equipped card is an eUICC
|
||
(SGP.22/32); both are used by the PWA's eSIM view.
|
||
|
||
`iccid` is the E.118 digit string read from EF.ICCID (MF/2FE2) when the card is
|
||
equipped, or `null` when it is not connected / the card does not let the
|
||
terminal read it. The equip path reads it **before** the TERMINAL PROFILE, so
|
||
no CAT session is active yet; the read is best-effort and never fails an equip
|
||
(and a card removal clears it). The PWA uses it to auto-select the card preset
|
||
with the same ICCID in both SCP80 views (Secured Packet and RAM).
|
||
|
||
### `GET /api/commands`
|
||
|
||
List all available shell commands for the current card profile.
|
||
|
||
### `GET /api/cardinfo`
|
||
|
||
Runs pySim's `cardinfo` command and returns its output as `{"output": "..."}`
|
||
(card type, ATR, ICCID and other information pySim reports for the equipped
|
||
card). A shortcut for `POST /api/command` with `{"cmd": "cardinfo"}`.
|
||
|
||
### `GET /api/mcc-mnc`
|
||
|
||
Searches the optional worldwide operator list loaded from
|
||
`--mcc-mnc-list` (default `<workspace>/samples/mcc-mnc-list.json`).
|
||
`?q=<text>` matches country, country code, MCC/MNC, brand and operator
|
||
(compact results, max 50); `?random=1[&exclude=MCCMNC]` returns one random
|
||
entry (for roaming tests). When the list is not configured or missing the
|
||
response is `{"available": false}`.
|
||
|
||
### `POST /api/command`
|
||
|
||
Execute any pysim-shell command.
|
||
|
||
```json
|
||
{"cmd": "select MF"}
|
||
```
|
||
|
||
Returns:
|
||
|
||
```json
|
||
{"output": "..."}
|
||
```
|
||
|
||
### `POST /api/apdu`
|
||
|
||
Send a raw APDU to the card.
|
||
|
||
```json
|
||
{"apdu": "00A4040000..."}
|
||
```
|
||
|
||
Returns:
|
||
|
||
```json
|
||
{"response": "...", "sw": "9000"}
|
||
```
|
||
|
||
### `POST /api/verify-adm`
|
||
|
||
Verify the card's ADM PIN (TS 102 221 VERIFY, CHV number from the card
|
||
model). The key comes from the PWA's matched card preset and is never stored;
|
||
it is redacted from the request log. Short keys (4-16 hex digits) are padded
|
||
to the 8 CHV bytes with `f`, like pySim's `verify_adm`. The response is
|
||
structured so the UI can warn about the remaining attempts before retrying.
|
||
|
||
```json
|
||
{"adm": "0011223344556677"}
|
||
```
|
||
|
||
Returns on success:
|
||
|
||
```json
|
||
{"ok": true, "sw": "9000"}
|
||
```
|
||
|
||
On a wrong key (`63Cx`, x attempts left):
|
||
|
||
```json
|
||
{"ok": false, "sw": "63C2", "attempts_left": 2}
|
||
```
|
||
|
||
A blocked ADM (`6983`/`9804`) reports `{"ok": false, "sw": "9804",
|
||
"blocked": true}` and cannot be recovered without the card's unblock key.
|
||
|
||
### Card presets (server-side store)
|
||
|
||
Card presets (keysets, a TAR table with per-TAR MSLs, PSK pair, ADM) live on the server
|
||
in `~/.pysim-simple-server/card_presets.json` (`--card-presets PATH` overrides
|
||
it; `Path.home()` resolves the same way on Linux, macOS and Windows). The file
|
||
is written atomically and every counter change is appended to
|
||
`card_presets.json.audit.jsonl`.
|
||
|
||
**Keysets.** A preset carries several keysets — a keyset's number is the
|
||
`b8..b5` nibble of its KIc/KID (TS 102 225 §5.1.2/A.2); the number is implicit in
|
||
the bytes, there is no separate field:
|
||
|
||
```json
|
||
{"id": "…", "name": "Card A", "iccid": "…",
|
||
"pin1": "", "puk1": "", "pin2": "", "puk2": "",
|
||
"pskIdentity": "", "pskKey": "", "adm": "",
|
||
"tars": [
|
||
{"role": "isd", "tar": "000000", "msl": "16", "desc": ""},
|
||
{"role": "uiccRfm", "tar": "B00000", "msl": "16", "desc": ""},
|
||
{"role": "usimRfm", "tar": "B00001", "msl": "16", "desc": ""},
|
||
{"tar": "AF4D01", "msl": "1A", "desc": "my applet"}
|
||
],
|
||
"keysets": [
|
||
{"kic": "15", "kid": "15", "kicKey": "…", "kidKey": "…", "cntr": "0000000001"},
|
||
{"kic": "29", "kid": "29", "kicKey": "…", "kidKey": "…", "cntr": "0000000005"}
|
||
]}
|
||
```
|
||
|
||
Rules (all refused with `400`): KIc and KID must carry the **same** keyset number
|
||
(A.2 — the card rejects a mismatch with "Unidentified security error"), the
|
||
number must be `01`–`0F` (`00` means "no security" and is a packet-level
|
||
choice, not a preset keyset), no two keysets may share a number, both keys and
|
||
a counter (1–10 hex digits) are required, and the counters are stored in the
|
||
fixed-width 10-hex form. A v3.8.0 flat preset (`kic`/`kid`/`kicKey`/`kidKey`/
|
||
`cntr` at the top level) converts into a single keyset on load and on import,
|
||
so old files and exports keep working.
|
||
|
||
**TAR table.** Every Remote Management application is addressed by its TAR and
|
||
each entry carries its own **MSL** (Minimum SPI1, TS 102 226 §8.2.1.3.2.4): the
|
||
card checks it before the security processing and answers response status `0A`
|
||
"Insufficient security level" when a packet's SPI1 is below it. The three role
|
||
entries (`isd`, `uiccRfm`, `usimRfm` — GP ISD `000000`, TS 31.116 UICC RFM
|
||
`B00000`, TS 31.115 ADF RFM `B00001`) are **mandatory** with a non-empty TAR
|
||
and MSL; further TARs can be added with an optional `desc`. A TAR is three hex
|
||
bytes, an MSL one, and a TAR may appear once (a duplicate is refused). A
|
||
v3.9.1 preset's card-wide `spi1`/`tar`/`uiccTar`/`usimTar` convert into the
|
||
three role entries, the `spi1` as their MSL. Operations **derive a packet's
|
||
SPI1 from the MSL of its TAR**; a request naming a TAR outside the preset must
|
||
pass `spi1` itself, otherwise it is refused with `400` and a hint.
|
||
|
||
**Card codes.** The optional `pin1`/`puk1`/`pin2`/`puk2`/`adm` fields are
|
||
stored for later use (nothing consumes them yet). PIN/PUK must be **4–8
|
||
decimal digits** when set (empty is fine; anything else is refused with `400`
|
||
naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine).
|
||
|
||
**CAT (USAT) release.** The optional `release` field (4–18) names the release
|
||
the card implements; the release guard hides CAT items introduced after it
|
||
(the Simulator's event parameters, see `catrelease`). Empty means unspecified
|
||
and resolves to Rel-12 (`catrelease.DEFAULT_RELEASE`) — an unset release never
|
||
filters anything away.
|
||
|
||
**Counters.** The store is the source of truth for the SCP80 counters, one per
|
||
keyset ("a dedicated counter shall be associated to each key version",
|
||
Annex A.1 — the spec's wording). The operations below accept a `preset_id` and **persist the
|
||
counter they consumed themselves** (monotonic per keyset — a stale value never
|
||
regresses it), so a closed tab, a lost response or a second browser window can
|
||
no longer lose an increment. A request whose KIc/KID name a **non-zero key
|
||
number the preset does not define** is refused (`400 {"error": "keyset N
|
||
is not defined in preset '…' - add it in the Cards tab"}`), and so is a
|
||
KIc/KID number mismatch. The Cards tab's export/import uses this API; the
|
||
import also accepts presets exported by the older localStorage-based builds.
|
||
|
||
- `GET /api/presets` — `{"path": "…", "count": 2, "version": 5, "presets": [{…}]}`
|
||
(card-free: it answers while a long card operation runs).
|
||
- `POST /api/presets` — body = the preset fields (`keysets` included); returns
|
||
`{"ok": true, "preset": {…}}`. A duplicate ICCID (digits, spaced digits,
|
||
digits with the odd-nibble F pad, or raw EF hex — all normalised to one
|
||
form) is refused with
|
||
`400 {"error": "card with this ICCID already exists: …"}`.
|
||
- `POST /api/presets/update` — `{"id": "…", "fields": {…}}` (partial update;
|
||
a plain `{id, name, …}` body works too, `keysets` replaces the list and
|
||
`tars` the TAR table — a non-list `tars` value is refused).
|
||
A counter is written as given — this is the deliberate human edit; unknown
|
||
ids answer `404`.
|
||
- `POST /api/presets/delete` — `{"id": "…"}` → `{"ok": true, "removed": true}`.
|
||
- `POST /api/presets/import` — `{"presets": […], "mode": "merge"}` (or
|
||
`"replace"`) → `{"ok": true, "added": N, "skipped": M, "errors": […]}`;
|
||
invalid entries and duplicates are skipped and reported, never fatal.
|
||
|
||
### `POST /api/presets/import-outfile`
|
||
|
||
Import many cards at once from a card vendor's **outfile** (v3.27.0). An
|
||
outfile is the vendor personalization tool's plain-text dump: a banner and a
|
||
configuration header (customer, batch, profile, …) followed by the actual
|
||
data. The data starts at a **column-name line** — whitespace-separated
|
||
tokens that include `ICCID` (the first token may carry a `var_out:`-style
|
||
prefix) — and continues with one whitespace-separated row per card;
|
||
everything before the column line and every column the presets do not use is
|
||
ignored.
|
||
|
||
Without a `mode` the call is a **dry run**: the parsed presets and the report
|
||
come back for the preview (nothing is stored). With `mode: "merge"` /
|
||
`"replace"` the same parse runs and the presets are imported through the
|
||
store (`replace` wipes the store first, like `/api/presets/import`); any other
|
||
`mode` value is refused with `400` — a typo must not silently turn an import
|
||
into a preview.
|
||
|
||
```json
|
||
{"text": "…", "name": "Batch 0002", "algorithm16": "3des2",
|
||
"tars": [{"role": "isd", "tar": "000000", "msl": "16", "desc": ""}, …],
|
||
"tar_keyset": 1, "mode": "merge"}
|
||
```
|
||
|
||
- `text` (required) — the outfile contents. A file without a column line
|
||
answers `{"ok": true, "preview": true, "presets": [], "report": {"error":
|
||
"no column-name line found - …"}}` (a preview, not a `400`: the panel
|
||
shows the message). The same report error covers a structurally invalid
|
||
file (v3.29.2): a data row whose token count differs from the header's column
|
||
count (the format is positional, so an empty value drops its token - including
|
||
in the middle, which would otherwise shift the columns), or a
|
||
`KICn`/`KIDn` pair declared in the header without a usable hex value in some
|
||
row. An import (`mode`) of such a file answers `400` with that error - it is
|
||
never partially imported.
|
||
- `name` — one common name applied to every imported card; empty falls back
|
||
to the card's ICCID.
|
||
- `algorithm16` — the algorithm nibble for a **16-byte** key: `"3des2"`
|
||
(default, KIc/KID nibble `5`) or `"aes"` (nibble `2`); any other value is
|
||
refused with `400` (a misspelled algorithm must never silently pick one —
|
||
the nibble decides whether the card accepts the packet). The key size alone
|
||
decides the others: 8 bytes → DES (`1`), 24 bytes → 3DES3 (`9`); any other
|
||
size skips that keyset with a report line.
|
||
- `tars` — the TAR table every imported card gets (defaults to the three role
|
||
entries with their spec TARs and MSL 16). A malformed table (a non-3-byte
|
||
TAR, a non-1-byte MSL, a duplicate) is refused with `400`.
|
||
- `tars` entries may carry their own `keyset` (1–15 — the preview's per-row
|
||
choice, v3.29.1): it wins; the entries without one fall back to
|
||
`tar_keyset`, and without that to the card's first keyset (a multi-keyset
|
||
card would otherwise fail the store's "a non-zero MSL needs a default
|
||
keyset" rule). A keyset outside 1–15 is refused with `400`.
|
||
- `tar_keyset` — an optional **global default keyset** (1–15) for the TAR
|
||
entries that do not name one (the API's shorthand; the PWA sends per-row
|
||
keysets instead).
|
||
- `start_counter` — the counter **every created keyset starts from** (1–10 hex
|
||
digits; whitespace stripped, uppercased). Without it the store's default
|
||
(`1`) applies. A bad value is a `400`; the import stores the protocol's
|
||
10-digit form.
|
||
|
||
Recognised columns (case-insensitive): `ICCID` (the digits + odd-nibble F
|
||
pad, the plain digits, spaced digits or the raw nibble-swapped EF hex all
|
||
normalise to the same value), `PIN1`/`PIN2`, `PUK1`/`PUK2` (4–8 digits; an
|
||
invalid value is dropped and reported, the card is kept), `ADM` (the key's
|
||
**hex value**, 4–16 hex digits in byte pairs — the rule `/api/verify-adm`
|
||
applies), `PSK` (the SCP81 key,
|
||
exactly 32 hex chars) and `KICn`/`KIDn` — one keyset per numbered pair, up to
|
||
15. The **KIc/KID bytes are never in the file**: the keyset number is the
|
||
column number (`KIC2` → keyset 2) and the algorithm nibble follows the key
|
||
size, both bytes carrying the same number (TS 102 225 §A.2). A `PSKID` /
|
||
`PSK_ID` column names the PSK identity; without one the card's ICCID is used
|
||
(the identity the card sends in the ClientKeyExchange is ICCID-like). `IMSI*`,
|
||
`EKI` and `KIKn` (the key-management key, unused) are ignored.
|
||
|
||
The response carries the parsed presets (a dry run) or the import result
|
||
(`added`/`skipped`/`errors`) plus the `report`:
|
||
|
||
```json
|
||
{"ok": true, "preview": false, "added": 5, "skipped": 0, "errors": [],
|
||
"report": {"columns": ["IMSI", "ICCID", "KIC1", …], "header_lines": 11,
|
||
"rows": 5, "cards": 5, "keysets": ["KIC1/KID1", …], "tars": 3,
|
||
"skipped": ["line 4: keyset 2 has an unsupported key size (10 bytes)"],
|
||
"skipped_more": 0}}
|
||
```
|
||
|
||
### `POST /api/counter-probe`
|
||
|
||
Sync a preset's SCP80 counter with the card. The card accepts only a counter
|
||
*above* its own (TS 102 225 §5.1.1, SPI1 b5b4 = `10`) and updates it only when
|
||
the packet is accepted, so rejected attempts change nothing; the probe walks a
|
||
doubling ladder (`start+1`, `+2`, `+4`, …) with a fixed read-only command until one
|
||
is accepted, then persists `accepted+1` (monotonic) into the keyset. The
|
||
search stops at a ceiling and after a bounded number of packets, so it can
|
||
never run towards the 40-bit maximum, where the card blocks the counter. The
|
||
card's own counter is never reported by the card (the PoR's CNTR is a copy of
|
||
the packet's, TS 102 225 §5.2 Table 3), which is why this probe exists.
|
||
|
||
```json
|
||
{"preset_id": "…", "kvn": 2, "tar": "000000", "spi1": "16", "spi2": "21",
|
||
"cntr": "0001000000", "ceiling": "FFFFFFFF", "max_attempts": 40}
|
||
```
|
||
|
||
All fields except `preset_id` are optional: `kvn` picks a keyset (the first
|
||
otherwise), `tar` defaults to the preset's ISD entry and `spi1` to that TAR's
|
||
MSL, `spi2` to the RAM listing transport `21`, `cntr` to the
|
||
keyset's counter, `ceiling` to `FFFFFFFF` (must stay below `FFFFFFFFFF`) and
|
||
`max_attempts` to 40 (clamped to 1–64). The probe needs the PoR verdict, so
|
||
bit b1 of `spi2` is always set (a caller's `00` becomes `01`; the transport
|
||
bits are kept) - without a PoR the probe is blind. The probe always sends the
|
||
read-only `SELECT MF` - the counter advances on the packet's security
|
||
acceptance whatever the applet answers (`6A86`/`6D00` are normal there). A
|
||
SPI1 without a counter check is refused (400).
|
||
|
||
The ladder only advances on a `cntr_low` verdict (the card's "your counter is
|
||
not above mine"); **any other verdict stops the search**. A stop on a
|
||
non-`por_ok` verdict (`rc_cc_ds_failed`, `tar_unknown`, …) *after* at least one
|
||
`cntr_low` keeps the counter value anyway - the earlier rejection proves the
|
||
value is above the card's counter - but the response reports `success: false`,
|
||
`counter_saved: true`, the `verdict` and the saved counter, stating that
|
||
something is likely wrong with the security settings. Without a prior
|
||
`cntr_low` nothing is saved (the value is not established), and a missing PoR
|
||
is an error too.
|
||
|
||
**Response:** `{"success": true, "accepted_cntr": "0001000000",
|
||
"stored_cntr": "0001000001", "packets": 3, "stopped": "accepted",
|
||
"attempts": [{"cntr": "…", "por_status": "cntr_low"}, …],
|
||
"start": "…", "ceiling": "FFFFFFFF", "kvn": 2}`
|
||
|
||
`stopped` is `accepted`, `ceiling`, `attempts` or `error`; on a non-`cntr_low`
|
||
failure (refused ENVELOPE, wrong TAR, security error) the probe stops at the
|
||
first attempt instead of hammering the card.
|
||
|
||
### `POST /api/tar-probe`
|
||
|
||
Probe which OTA applications the card has registered: one **secured packet per
|
||
TAR of the named preset** (v3.26.0 — the probe checks the preset's TAR list
|
||
only; a `tars` list may select a subset, and a TAR outside the preset is
|
||
refused with `400`), each carrying a harmless C-APDU (`SELECT MF` by default).
|
||
Each row uses **its own MSL** (the request's `spi1` is only a fallback for a
|
||
TAR the preset does not carry) and **its own default keyset** — the keyset
|
||
resolved from that TAR's entry, so the request does not carry KIc/KID/keys and
|
||
the probe's keyset selector is gone. A TAR whose MSL requires security
|
||
(non-zero) without a default keyset, or one naming an undefined keyset, is
|
||
refused with a hint **before any packet is sent**. The resolved SPI1 must
|
||
carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet
|
||
consumes a counter, which is persisted into that row's keyset like any other
|
||
operation (each keyset has its own counter, TS 102 225 A.1); an explicit
|
||
`cntr` seeds the first row only. The delivered SMS carries the OTA CPI IE by
|
||
default (`includeCpi`, as in `/api/send-ota`).
|
||
|
||
```json
|
||
{"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00",
|
||
"tars": ["000000", "B00000"]}
|
||
```
|
||
|
||
**Response:** `{"success": true, "registered": N, "total": M,
|
||
"final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01",
|
||
"results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2,
|
||
"verdict": "registered", "sw": "9000",
|
||
"por_status": "por_ok",
|
||
"por_sw": "6D00", "por_data": ""}], "steps": [...]}`
|
||
|
||
Verdicts: `registered` (ENVELOPE 9000 + a PoR carrying the application's own
|
||
status word/data), `no_answer` (accepted, PoR without an R-APDU), `no_por`
|
||
(accepted, no PoR at all — the packet was silently dropped), `refused` (a
|
||
non-9000 ENVELOPE, typically `6200` for a TAR without a registered
|
||
application). Observed on a live card: `000000`, `B00000` and `B00001`
|
||
registered (compact-format applications), the expanded-format TARs, USAT,
|
||
multiplexing and CASD refused.
|
||
|
||
### eSIM / LPA (local ES10 operations)
|
||
|
||
These endpoints work only when the equipped card is an eUICC (SGP.22/32);
|
||
otherwise they answer `400` with `{"error": "The equipped card is not an
|
||
eUICC"}`. They use pySim's ES10 static API — no lpac, no SM-DP+ contact, no
|
||
profile downloads and no notification processing. All run under the card
|
||
lock; `GET /api/status` reports `euicc` and `eid` for the PWA.
|
||
|
||
- `GET /api/esim/chip` — `{"eid": "8908…", "info1": {…}, "info2": {…},
|
||
"addresses": {"default_dp_address": …, "root_ds_address": …},
|
||
"rat": [{…}], "errors": {"<part>": "<reason>"}}` (a part the card does not
|
||
support is reported in `errors` instead of failing the whole request).
|
||
The EUICCInfo TLVs are requested raw and decoded per SGP.22 v2.6 §5.7.8
|
||
(pySim's classes are incomplete), `rat` comes from the ES10b GetRat command
|
||
(§5.7.13) and undecoded TLVs are preserved in a `raw_tlvs` map:
|
||
|
||
```json
|
||
{"eid": "89086030202200000026000024920451",
|
||
"info1": {"svn": "2.2.2",
|
||
"euicc_ci_pki_list_for_verification": ["8137…FB"],
|
||
"euicc_ci_pki_list_for_signing": ["8137…FB"]},
|
||
"info2": {"profile_version": "2.3.1", "svn": "2.2.2",
|
||
"euicc_firmware_ver": "4.2.0",
|
||
"ext_card_resource": {"installed_application": 0,
|
||
"free_non_volatile_memory": 439084,
|
||
"free_volatile_memory": 9798},
|
||
"uicc_capability": ["usimSupport", "isimSupport", "…"],
|
||
"ts102241_version": "9.2.0",
|
||
"globalplatform_version": "2.3.0",
|
||
"rsp_capability": ["additionalProfile", "testProfileSupport"],
|
||
"euicc_category": "other",
|
||
"forbidden_profile_policy_rules": ["ppr1"],
|
||
"pp_version": "1.0.0",
|
||
"ss_acreditation_number": "ED-ZI-UP-0826"},
|
||
"addresses": {"default_dp_address": null,
|
||
"root_ds_address": "testrootsmds.gsma.com"},
|
||
"rat": [{"ppr_ids": ["ppr1", "ppr2"],
|
||
"allowed_operators": [{"plmn": "EEEEEE", "gid1": null, "gid2": null}],
|
||
"ppr_flags": ["consentRequired"]}],
|
||
"errors": {}}
|
||
```
|
||
|
||
`uicc_capability`, `rsp_capability`, `forbidden_profile_policy_rules` and
|
||
`ppr_flags` are ASN.1 BIT STRINGs decoded to the names of the set bits (the
|
||
first content octet is the unused-bit count, bits are MSB-first);
|
||
`euicc_category` accepts both the implicit (`0x8B`) and explicit (`0xAB`)
|
||
tag encodings; `pp_version` (`0x04`) and `ss_acreditation_number` (`0x0C`)
|
||
are the bare, untagged SGP.22 types; `tre_properties`,
|
||
`tre_product_reference`, `additional_euicc_profile_package_versions` and
|
||
`certification_data_object` are decoded when a card sends them.
|
||
- `GET /api/esim/profiles` — `{"profiles": [{"iccid": "8970…",
|
||
"isdp_aid": "A000…", "state": "enabled"|"disabled", "nickname": …,
|
||
"provider": …, "name": …, "class": "test"|"provisioning"|"operational",
|
||
"owner": "250-99", "icon_type": "png"|"jpg", "icon": "89504E47…",
|
||
"icon_size": 1234}], "error": null}`. `icon` is the SGP.22 icon data
|
||
(tag `0x94`) as hex and `icon_size` its byte count, so a client can show
|
||
the profile image (`data:image/<type>;base64,…`); both are `null` when the
|
||
card sent no icon.
|
||
- `GET /api/esim/notifications` — `{"notifications": [{"seq_number": 3,
|
||
"operations": ["enable"], "address": "smdp.example.org",
|
||
"iccid": "8970…"}], "error": null}`. `operations` are the decoded
|
||
`ProfileMgmtOperation` flags (`install`, `enable`, `disable`, `delete`, in
|
||
that order) — one or more per notification.
|
||
- `POST /api/esim/profile` — `{"action": "enable"|"disable", "iccid"?: …,
|
||
"isdp_aid"?: …, "refresh"?: true}` (one identifier required). With the
|
||
refresh flag set the ISD-R returns OK *before* the REFRESH (SGP.22 v2.6
|
||
§5.7.16/§5.7.17 step 6) and the switch completes upon the TERMINAL RESPONSE
|
||
or the following RESET (step 8): a `91xx` answer is that OK, so the server
|
||
answers the REFRESH proactive command (logged in `/api/proactive-log`),
|
||
**never retries the STORE DATA** (the mid-switch card answers `6985`) and
|
||
re-initializes the card like an equip (physical reset, re-read
|
||
ICCID/network state, new `card_session`). It then re-reads the profile
|
||
list and reports the verified result:
|
||
|
||
```json
|
||
{"ok": true, "result": "ok", "refresh_seen": true, "reinitialized": true,
|
||
"verified": true, "state_after": "disabled", "iccid": "8970…",
|
||
"card_session": 7}
|
||
```
|
||
|
||
`verified` is `false` when the target profile is not in the requested state
|
||
after the re-init (`null` when the state could not be read). Failures carry
|
||
the ES10c result code (`iccidOrAidNotFound`, `profileNotInDisabledState`,
|
||
`profileNotInEnabledState`, `disallowedByPolicy`, `wrongProfileReenabling`,
|
||
`catBusy`, `undefinedError`) and a short `message`; an unexpected status
|
||
word is returned as `{"ok": false, "result": "undefinedError", "sw": "6985",
|
||
"message": "SW 6985"}`.
|
||
|
||
### `POST /api/help`
|
||
|
||
Get structured help for a shell command.
|
||
|
||
```json
|
||
{"cmd": "apdu"}
|
||
```
|
||
|
||
Returns:
|
||
```json
|
||
{"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
|
||
(one ENVELOPE per SMS). With `apdu` the secured packet is built
|
||
server-side (no single-SMS limit); a packet that does not fit one SMS is
|
||
sent as a **concatenated** download per TS 31.115 §4.3: the packet is
|
||
split into SMS user-data parts (first SM 132 octets, following ones 134 —
|
||
the first one additionally carries the concatenation and CPI IEs) and the
|
||
segments are sent in order. A packet that would need more than 5 segments
|
||
is refused (the card's concatenation buffer is the limit). With `sp` a
|
||
pre-built packet is delivered the same way.
|
||
|
||
**SMS framing — `includeCpi`.** By default (`true`) the delivered SMS TPDUs
|
||
carry the OTA **CPI information element** (IEI `70`, null IED — TS 31.115
|
||
§4.2/§4.3), the marker of a command packet; it shrinks the SMS user data — a
|
||
single SM takes up to **137** packet octets instead of 140, and the first
|
||
concatenated segment **132** instead of 134. The RAM/ISD operations send
|
||
with `includeCpi: true` — a concatenated command carries the CPI IE in its
|
||
first SM (TS 31.115 §4.3), and those are the packets that span several SMs.
|
||
With `false` no CPI IE is added (a single SM then carries no UDH at all and
|
||
the full 140-octet budget is available) — for a send whose receiver does not
|
||
expect the marker.
|
||
|
||
**SPI1 and the TAR's MSL.** With a `preset_id` the packet's SPI1 is derived
|
||
from the MSL of its `tar` (TS 102 226 §8.2.1.3.2.4) unless the request passes
|
||
`spi1` — an explicit value wins (a hand send may deliberately ask for more
|
||
security than the minimum), and a value below the TAR's MSL is reported as
|
||
`"warning"` in the response (numeric compare). A TAR the preset does not
|
||
carry, without an explicit `spi1`, is refused with `400` and a hint.
|
||
|
||
The card may answer with PoR status `actual_response_sms_submit` (`0x0B`):
|
||
the real response (a big GET STATUS listing, for example) then arrives as one
|
||
or more proactive SEND SHORT MESSAGE commands. The server captures those
|
||
SMS-SUBMIT TPDUs, reassembles the concatenated segments and returns the
|
||
decoded response in `por` (as if it had arrived in the ENVELOPE), so callers
|
||
see a normal `por.response_status == "por_ok"` with the remote status word
|
||
and response data. Responses wrapped in the TS 102 226 5.2.2 Response
|
||
Scripting template (`AB`/`AF`: executed-count TLV `80` + R-APDU TLV `23`) are
|
||
decoded the same way (`response_type: "scripting"`), with the R-APDU's own
|
||
status word and data.
|
||
|
||
With `ram_format` the server handles the two RAM command formats of
|
||
TS 102 226 §5.2.1: `"auto"` (the default when the key is present) sends a
|
||
read-only `GET STATUS [ISD]` probe first — compact, then wrapped as `AA`
|
||
(Command TLV `22`) — and sends the command in the format the card answered;
|
||
`"compact"` / `"expanded"` pin it. The response reports the detected
|
||
`ram_format` and `final_cntr` (the counter to use next — accepted probe
|
||
packets consume counters). Without the key the command data is sent
|
||
byte-exact: applet-directed payloads (HTTP OTA triggers, §9 push, expanded
|
||
scripts) must not be wrapped.
|
||
|
||
`final_cntr` is reported for the plain `sp` path too (a pre-built packet):
|
||
the counter that was sent, advanced by one when the card accepted the packet.
|
||
|
||
**`cntr` is optional (v3.20.0)** — the preset store is the source of truth:
|
||
|
||
- with `apdu` (the server builds the packet) a missing `cntr` is seeded from
|
||
the named preset's keyset (the one the packet's KIc/KID resolve to); the
|
||
effective value is echoed back as `cntr`, so a caller that omits it learns
|
||
what was used. The seed uses the packet's keyset whatever `SPI1.b5b4`
|
||
says — with no counter check the CNTR field is spec-ignored, nothing is
|
||
advanced or persisted, and the `OTA SEND: counter not tracked` line names
|
||
it;
|
||
- an explicit `cntr` always wins — the packet uses it verbatim (a hand send,
|
||
the counter probe or a pre-built `sp` may deliberately pick one);
|
||
- the store only moves **forward**: an explicit value (or its next counter)
|
||
below the stored one is used for the packet but never lowers the store; a
|
||
higher accepted value is persisted;
|
||
- the `sp` (pre-built packet) path never seeds — the packet carries its own
|
||
counter — so pass `cntr` with it to get `final_cntr` / persistence. A
|
||
request with neither simply reports no `final_cntr` — it never fails the
|
||
send.
|
||
|
||
With `preset_id` the server persists
|
||
the counter into the keyset the packet used (see *Card
|
||
presets*), so a lost response or a closed tab cannot lose the increment; the
|
||
request is refused when KIc/KID carry different keyset numbers (TS 102 225 A.2)
|
||
or a non-zero keyset number the preset does not define.
|
||
|
||
**Counter tracking follows SPI1.b5b4** (TS 102 225 §5.1.1): with `00` the
|
||
counter field is "present, ignored, never updated", so a packet whose SPI1 has
|
||
no counter check (the keyless `SPI1 00` form) reports **no** `final_cntr` and
|
||
persists nothing — a keyless send must leave every preset counter untouched.
|
||
The spec's no-security KIc/KID values `00/00` are accepted when the SPI says
|
||
no ciphering / no RC-CC-DS (A.2), so `{apdu, spi1: "00", kic: "00", kid: "00"}`
|
||
builds a keyless packet (the card accepted exactly that form; a fake `15/15`
|
||
was refused with `6200`). When a PoR was requested and the card did not
|
||
answer, the response carries `por_missing: true`.
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"sp": "00201516011515b00000...",
|
||
"spi1": "16",
|
||
"spi2": "01",
|
||
"kic": "15",
|
||
"kid": "15",
|
||
"tar": "b00000",
|
||
"cntr": "0000000001",
|
||
"kicKey": "D6FCC023...",
|
||
"kidKey": "1B07E7E0...",
|
||
"preset_id": "7ee0367f3a444b28988fea5fbc50de9f",
|
||
"ram_format": "auto",
|
||
"includeCpi": true
|
||
}
|
||
```
|
||
|
||
**Response (delivery PoR):**
|
||
```json
|
||
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
|
||
"bytes": 36, "segments": 1,
|
||
"ram_format": "compact", "final_cntr": "0000000002",
|
||
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
|
||
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
|
||
"last_response_data": ""}}}
|
||
```
|
||
|
||
`bytes` is the secured packet size and `segments` the number of SMS
|
||
segments sent (1 = single SMS, >1 = concatenated download; the failure
|
||
response carries them too, plus an `error`).
|
||
|
||
**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 response form follows the command's TAR (v3.21.0): a remote-management
|
||
TAR (the preset's role entries and the standard ISD/RFM allocations of
|
||
TS 101 220 Annex D) is decoded as an RM response; any other TAR's secured
|
||
data is the receiving application's own (TS 102 226 4) and is reported with
|
||
`por.response_type` `raw` - the whole secured data in
|
||
`decoded.last_response_data` and no status word (`secured_data` carries the
|
||
response bytes in every case). The same `raw` classification covers a
|
||
secured data that cannot be the compact structure (its command count exceeds
|
||
the command script that was sent, or it is too short for Table 5.1), while a
|
||
genuinely malformed packet or a failed RC/CC/DS check still yields no `por`.
|
||
|
||
The SPI2 `por_in_submit` bit (0x20) selects submit-mode PoR.
|
||
|
||
### `POST /api/cap-info`
|
||
|
||
Validate a Java Card `.cap` archive and estimate its memory requirements.
|
||
Read-only: it runs the same structural parse as the install paths (so a
|
||
corrupt or wrong-format file fails here first) plus the bundled CAP analyzer
|
||
(`pysim_simple_server/capmem.py` — component parsers and a method-bytecode
|
||
allocation scan). It never touches the card, the SCP81 listener or the
|
||
scripts; the install endpoints stay self-sufficient and the estimate is not
|
||
used for installation.
|
||
|
||
```json
|
||
{"cap_hex": "504B0304..."}
|
||
```
|
||
|
||
**Response (ok):**
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"load_file_aid": "AA1902BC226001", "module_aid": "AA1902BC226001",
|
||
"load_file_bytes": 1234,
|
||
"memory": {
|
||
"package_aid": "AA1902BC226001",
|
||
"applet_count": 1, "applets": ["AA1902BC226001"],
|
||
"imports": [{"aid": "A0000000620101", "minor": 0, "major": 1, "refs": 6},
|
||
{"aid": "A0000000090005FFFFFFFF8912000000", "minor": 11, "major": 1, "refs": 7}],
|
||
"flags": {"raw": 4, "int": false, "export": false, "applet": true},
|
||
"package_name": null,
|
||
"components": [{"name": "Header", "size": 20}, {"name": "Method", "size": 433}],
|
||
"class_count": 3, "method_count": 12,
|
||
"code": {"method_component": 850, "load_file": 1234},
|
||
"nvram": {"static_image": 12, "array_init": 4, "install_objects": 100,
|
||
"header_overhead": 24, "ref_storage": 8, "total": 148, "runtime": 0,
|
||
"requirement": 1382},
|
||
"ram": {"transient_arrays": 16, "runtime_transient": 0, "peak_frame": 8, "total": 24},
|
||
"suggested": {"c6": 850, "c7": 272, "c8": 148},
|
||
"warnings": []
|
||
}
|
||
}
|
||
```
|
||
|
||
`imports` are the libraries the CAP is linked against (JC VM spec §6.6,
|
||
the Import component), each with the export-file version recorded in the CAP
|
||
and the number of distinct constant-pool references to it (§6.7; a linked but
|
||
unreferenced package is 0). A card resolves an import only when the resident
|
||
package has the **same major** version and a **minor ≥** the recorded one
|
||
(§4.5.2), hence the `name >= version` display. `flags` decodes the Header
|
||
package flags (Table 6-4: `0x01` uses `int`, `0x02` exports an API, `0x04`
|
||
applet package); `package_name` is only present in JC 2.2+ headers (CAP 2.1
|
||
files have none); `components` lists the archive entries in load-file order
|
||
(each size includes the component's tag and size header, so the sizes sum to
|
||
`load_file_bytes`).
|
||
|
||
`code.load_file` is the concatenated load file (all components) - the
|
||
package image the card stores, our proxy for the GlobalPlatform Card Spec
|
||
v2.3.1 Table 11-48 "non-volatile code" minimum memory requirement; the
|
||
tool's bytecode-only figure stays in `code.method_component`. The suggested
|
||
`C6` follows the load file, and `nvram.requirement` = `code.load_file` +
|
||
`nvram.total` (the C6+C8-style total per §11.5.2.3.7: with no code/data
|
||
split in the card's memory the required minimum is the sum of both). The
|
||
estimate excludes card-specific memory management, allocation rounding and
|
||
the GP registry entry.
|
||
|
||
**Errors** (HTTP 200 with `ok: false`, like `/api/scp81/gen-install`):
|
||
`{"ok": false, "error": "cap parse failed: File is not a zip file"}` for a
|
||
corrupt/wrong archive, or `cap analysis failed: …` when a component passes
|
||
the structural parse but not the analyzer. The numbers are an estimate:
|
||
the model assumes 2-byte references, a 6-byte object header and NVM cell
|
||
rounding, and does not include applet-created runtime objects/arrays.
|
||
|
||
### `POST /api/test/run`
|
||
|
||
Runs a **test script**: an ordered list of actions and proactive-command
|
||
expectations, executed server-side on the equipped card. While a run is
|
||
active the card is owned by the script - other card endpoints answer
|
||
`409 {"error": "test script running ..."}` and background STATUS polling is
|
||
suspended; only `/api/test/*`, `/api/status`, `/api/poll-status`,
|
||
`/api/version` and static files stay available.
|
||
|
||
```json
|
||
{"script": {"name": "STK menu browsing", "steps": [
|
||
{"type": "action", "kind": "menu-select", "params": {"item_id": 128},
|
||
"check": {"sw": "91??"}},
|
||
{"type": "expect", "command": "SELECT ITEM",
|
||
"checks": [{"kind": "item", "id": 1, "text": "test"}],
|
||
"respond": {"result": "ok", "item_id": 1}},
|
||
{"type": "expect", "command": "DISPLAY TEXT",
|
||
"checks": [{"kind": "text", "value": "hello"}],
|
||
"respond": {"result": "ok"}}
|
||
]},
|
||
"preset": {"name": "lab card",
|
||
"keysets": [{"kic": "15", "kid": "15", "kicKey": "...", "kidKey": "...",
|
||
"cntr": "0000000A"}],
|
||
"tars": [{"role": "isd", "tar": "000000", "msl": "16"},
|
||
{"role": "uiccRfm", "tar": "B00000", "msl": "16"},
|
||
{"role": "usimRfm", "tar": "B00001", "msl": "16"}]}}
|
||
```
|
||
|
||
The PWA sends the current script inline (a run always uses what the editor
|
||
shows) plus `script_id` when the script is stored; sending `script_id` alone
|
||
resolves the script from the store. The PWA sends `preset_id` (the stored
|
||
preset's id) instead of `preset`; the
|
||
server resolves it from the preset store, so a run always uses the stored
|
||
keysets, TARs and counters. The inline `preset` form stays for external
|
||
callers.
|
||
|
||
**Action steps** (`type: "action"`): `kind` is `envelope` (`event`, `data`,
|
||
optional `src` - the device-identities source override),
|
||
`event` (`event` name or hex + `fields` for the four server-built events
|
||
0x03/0x0B/0x12/0x1D, or the PWA form's built `data` hex for any other event -
|
||
validated as hex and against the one-ENVELOPE budget at load; optional `src`,
|
||
the device-identities override), `menu-select` (`item_id` 1-255, or `text` +
|
||
`mode` `exact`/`contains` (case-insensitive by default; `case_sensitive: true`
|
||
forces an exact-case match) - the text is resolved against the card's cached
|
||
SET UP MENU, which is refreshed whenever the card sends the menu), `file-write`
|
||
(`path`, `data`, `mode` `auto`/`binary`/`record`, `record`), `file-read` (same,
|
||
verifies `check.data`),
|
||
`apdu` (raw transport, no auto-handler), `scp80` (`apdu` or `sp`, selected by
|
||
the optional `source` field when both are present; optional `kvn` (keyset
|
||
number 1-15), `tar`/`spi1`/`spi2` overrides and `format` (`compact` default /
|
||
`expanded` / `expanded-ae` - the TS 102 226 5.2.1 Command Scripting template
|
||
around the C-APDU) - KIc/KID and the counter always
|
||
come from the resolved preset, which must match the equipped card and be
|
||
complete), `status`
|
||
(`attempts`, `interval_ms` - when `attempts > 1` the default SW check is the
|
||
mask `91??`, i.e. poll until the card announces a command) or `proactive-drain`
|
||
(the proactive drain: poll STATUS and consume every announced command -
|
||
`respond`/`first` TERMINAL RESPONSE results, `attempts`/`interval_ms` bound,
|
||
optional `require` `{command, qualifier?}` asserting at least one drained
|
||
command; the final SW is the step's `check.sw`, so a still-pending card fails
|
||
and hands the command to the next step - an empty drain passes unless
|
||
`require` was given). It is exempt from the runner's
|
||
unexpected-pending-command guard, so it also consumes a command a previous
|
||
step left pending (`91XX`).
|
||
|
||
`check` is `{"sw": ..., "data": ...}` (exact or `{"mode": "mask", "value":
|
||
"91??"}`, `?` = per-nibble wildcard) plus `"por"` for SCP80 -
|
||
`"none"`/`"ok"`/`"any"` or an object `{"status"?, "sw"?, "data"?}` asserting
|
||
the decoded PoR of this exchange (inline or SEND SHORT MESSAGE; at least one
|
||
field). `on_fail` is `error` (terminates the script) or `warning` (continues)
|
||
and applies to every step failure - including an expectation that finds no
|
||
pending command (v3.21.0; `warning` is the tolerant consume-if-pending form)
|
||
and a card/transport error raised during the step (v3.22.7 - the error is
|
||
reported as a failed `Error` check at the step's level).
|
||
|
||
**Expectation steps** (`type: "expect"`) require a command pending from the
|
||
previous step (`91XX`); they never poll - a `9000` response means no command
|
||
and fails the step at its `on_fail` level (TS 102 221 7.4.2.1 / TS 102 223
|
||
6.3; a `status` action with `attempts > 1` polls when the card delivers on
|
||
poll). `command` is a name or type code; `checks`
|
||
may be `text` (contains/exact), `alpha` (the command's Alpha identifier
|
||
`05`/`85`, contains/exact - distinct from the `8D` text string), `item`
|
||
(`id`/`text` for SELECT ITEM / SET UP MENU), `raw` (mask), `por` (a SEND
|
||
SHORT MESSAGE carrying the PoR of the
|
||
preceding `scp80` step - PoR-in-submit, SPI2 bit `20`: `status` (the response
|
||
status name, `ok` = `por_ok`), `sw` (the R-APDU status word, exact/mask) and
|
||
`data` (the R-APDU response data, exact/mask)), `sms` (the SEND SHORT MESSAGE
|
||
TPDU: `da` digits with whitespace skipped and a leading `+` ignored,
|
||
`pid`/`dcs`/`ud` hex exact/mask, `udl` decimal; SMS-SUBMIT
|
||
only) or `files` (the File List of a
|
||
REFRESH, TS 102 223 8.18; `files` is a list of path hex strings, matched
|
||
order-insensitively); `respond` is the TERMINAL RESPONSE in the TS 102 223
|
||
6.8.0 object order (`result` - a script alias or a two-digit hex value from
|
||
the full TS 102 223 8.12.0 list; `additional_info` - the general result's
|
||
additional-information hex byte, sent as `83 02 <result> <additional_info>`,
|
||
default `00`; `text`+`dcs` for GET INKEY/GET
|
||
INPUT, `dcs` `00` packed / `04` 8-bit / `08` UCS2; `item_id`; extra `raw`
|
||
TLVs appended after the standard objects). The aliases (`ok`=00,
|
||
`partial`=01, `missing`=02, `modified`=07, `cancel`=10, `back`=11,
|
||
`timeout`=12, `refused`=22, `not_understood`=32, plus the compatibility-only
|
||
`no_response`=12) follow the spec; v3.23.5 corrected `refused`,
|
||
`not_understood`, `modified` and `no_response` (they used to carry legacy
|
||
values). The terminal's own objects ride
|
||
along unless `raw` overrides them: a successful PROVIDE LOCAL INFORMATION
|
||
answer carries the TR Config PLI dictionary entry (or the host clock for the
|
||
date/time qualifier) and a POLL INTERVAL answer the current-interval echo.
|
||
|
||
The response is the initial state (`running: true`), the final counter
|
||
(`scp80_counter`) and the step list; poll `/api/test/status`. The **server
|
||
persists** every counter an SCP80 step consumed into the preset store as the
|
||
run proceeds; the snapshot's `scp80_counter`/`scp80_counters` carry the
|
||
latest values (the PWA only syncs its local copy - nothing is written back).
|
||
|
||
A run that ends (an error, a stop, an unexpected pending command) with a
|
||
command still announced **drains it with `ok` (0x00)** - fetching while the
|
||
TERMINAL RESPONSE answers `91XX` (bounded); a cancel is never sent
|
||
automatically (it tells the card the user aborted the session and can push
|
||
assertive applications onto an error path). The cleanup is reported as a
|
||
`cleanup` step; a script that wants the refusal path answers explicitly (an
|
||
expectation's `respond`, or the `proactive-drain` action's `first`).
|
||
|
||
### `GET /api/test/scripts` and the script store
|
||
|
||
Test scripts live in a server-side store (`~/.pysim-simple-server/test_scripts.json`,
|
||
`--test-scripts PATH`) - the runner is server-side, so the store is the source
|
||
of truth and every browser manages the same set. The store validates with the
|
||
same engine the runner uses, so a stored script can never fail at run start for
|
||
a validation reason.
|
||
|
||
```json
|
||
{"path": "/home/user/.pysim-simple-server/test_scripts.json", "count": 1, "version": 2,
|
||
"scripts": [{"id": "…", "name": "applet RFM update", "suite_id": "…",
|
||
"require_adm": false, "steps": [...],
|
||
"created": 1690000000.0, "updated": 1690000000.0}]}
|
||
```
|
||
|
||
**Schema v2 (v3.22.0): every script belongs to exactly one test suite**
|
||
(`suite_id`; the suite is the root object) and may declare `require_adm`
|
||
(the run verifies the ADM before starting). A v1 entry loads with
|
||
`suite_id: null` and the server attaches it to an auto-created
|
||
"Imported scripts" suite on startup.
|
||
|
||
`POST /api/test/scripts` creates one (`{name, steps, suite_id, require_adm?}`
|
||
or `{script: {...}}`; a valid `suite_id` is required - the script joins the
|
||
suite's member list) and `POST /api/test/scripts/update` replaces
|
||
name/steps/require_adm (`{id, script}`). A `suite_id` that differs from the
|
||
current one is a **move** (both suites are updated in the request).
|
||
`POST /api/test/scripts/delete` (`{id}`) detaches the script from its suite
|
||
first - the suite's member list is the only reference, so no dangling id is
|
||
left. `POST /api/test/scripts/move` (`{id, suite_id}`) moves a script to
|
||
another suite (the target appends it as a member, keeping the `on_fail`
|
||
policy) and `POST /api/test/scripts/copy` (`{id, suite_id, name?}`) clones it
|
||
into the target. `POST /api/test/scripts/import` takes
|
||
`{scripts: [...], mode: "merge"|"replace", suite_id?}` (the old localStorage
|
||
export shape; invalid entries are reported in `errors` instead of failing the
|
||
whole import; `suite_id` attaches the imported scripts to that suite). A
|
||
`replace` is **refused while any suite still lists scripts** — the wipe would
|
||
leave dangling references; delete those scripts first, or import with
|
||
`merge`.
|
||
|
||
### `GET /api/test/suites` and the suite store
|
||
|
||
Test suites live next to the scripts (`~/.pysim-simple-server/test_suites.json`,
|
||
`--test-suites PATH`): a suite is the ordered list of its scripts - the
|
||
optional `setup` first, the `member` scripts, the optional `teardown` last -
|
||
plus the per-member `on_fail` policy, the suite-level `require_adm` and the
|
||
optional **`start_reset`** (reset + fully re-initialize the card before the
|
||
run - see `/api/test/run`).
|
||
|
||
```json
|
||
{"path": "/home/user/.pysim-simple-server/test_suites.json", "count": 1, "version": 2,
|
||
"suites": [{"id": "…", "name": "alfa regression", "require_adm": true,
|
||
"start_reset": false,
|
||
"scripts": [{"script_id": "…", "role": "setup", "on_fail": "stop"},
|
||
{"script_id": "…", "role": "member", "on_fail": "stop"},
|
||
{"script_id": "…", "role": "teardown", "on_fail": "stop"}],
|
||
"created": 1690000000.0, "updated": 1690000000.0}]}
|
||
```
|
||
|
||
**Schema v2 (v3.23.0)** adds `start_reset` (default `false`); a v1 file loads
|
||
with it `false`.
|
||
|
||
The store normalises the order (setup first, teardown last, at most one of
|
||
each; a script appears once per suite) and validates `on_fail` as
|
||
`stop` (default) or `continue` - "which failures cascade" is a visible
|
||
per-suite decision.
|
||
|
||
`POST /api/test/suites` creates one (`{name, require_adm?, start_reset?,
|
||
scripts?}` or `{suite: {...}}`), `POST /api/test/suites/update` replaces
|
||
name/require_adm/start_reset/scripts (`{id, suite}`; the **member set** changes through
|
||
the script endpoints, not here), `POST /api/test/suites/delete` (`{id}`)
|
||
removes it - refused while the suite still holds scripts - and
|
||
`POST /api/test/suites/import` takes
|
||
`{suites: [...], scripts: [...], mode: "merge"|"replace"}`: the scripts are
|
||
imported first **always with `merge`** (a script replace would wipe the other
|
||
suites' scripts; the mode applies to the suite store), ids preserved, a
|
||
collision gets a fresh uuid and the suites' references are remapped; then the
|
||
suites are imported (`replace` wipes the suite store first); an unresolvable
|
||
reference is reported in `errors` and the suite is skipped. After the import the
|
||
ownership invariant is reconciled: a script the imported suites list adopts
|
||
its listing suite, a script listed by several suites keeps its explicit owner
|
||
and the other listings are pruned, and a script no suite lists any more — the
|
||
`replace` mode wipes the suites that owned them — is **adopted into "Imported
|
||
scripts"** instead of becoming invisible (the same reconciliation runs at
|
||
startup for a v3.21.0 store).
|
||
|
||
The import always **merges**: an id already in the store gets a fresh uuid
|
||
(scripts) / a fresh suite id, so importing a bundle **into the store it came
|
||
from duplicates the suite and its scripts** — the round-trip is not
|
||
idempotent. Restore a bundle into a fresh store, or wipe the target first
|
||
(`mode: "replace"` for the suites).
|
||
|
||
### `POST /api/test/run` (single script or suite)
|
||
|
||
`{script, script_id?, preset_id?, require_adm?}` runs one script (the PWA
|
||
sends the edited copy; the server validates and runs it). `{suite_id,
|
||
preset_id?}` runs a suite: its scripts in order, one card session, no resume.
|
||
With `start_reset` the card is **physically reset and fully re-initialized**
|
||
first (ICCID, network state, TERMINAL PROFILE, the equip drain); the run's
|
||
preset must still match the card's ICCID afterwards (a mismatch refuses the
|
||
start) and a required ADM is verified against the **new** card session. Then
|
||
the ADM prerequisite (suite- or member-level `require_adm`: the
|
||
matched preset's key is verified **once**; a failure refuses the start, is
|
||
not retried automatically, and requires a manual verify), setup,
|
||
members (per-member `on_fail`), teardown. The initialization consumes the
|
||
card's initial proactive commands (they stay in the proactive log), so a
|
||
script cannot catch the first command after the reset. **The teardown runs
|
||
on every stop** as long as the card is in the reader; a card reset (session
|
||
change) stops the suite, skips the remaining members and is flagged in the
|
||
report.
|
||
|
||
The suite run's snapshot adds `suite`: the member records (`status`, `steps`,
|
||
`note`, `log` - the semantic run-log excerpt), the `summary` (ok/warning/
|
||
error/skipped counts, `wall_ms`, `stopped`, `session_changed`, the SCP80
|
||
`counters_before`/`counters_after` per keyset), the `start_reset` flag and
|
||
the `reset` result (ICCID + ms) and the run's `adm` state.
|
||
The report is exported from the PWA as JSON or Markdown; the run state lives
|
||
in the server until `POST /api/test/clear` (no history).
|
||
|
||
### `GET /api/test/status`
|
||
|
||
The current (or last) run:
|
||
|
||
```json
|
||
{"running": true, "name": "STK menu browsing", "status": null,
|
||
"index": 1, "total": 3, "scp80_counter": null,
|
||
"steps": [{"index": 0, "type": "action", "label": "ENVELOPE(Menu Selection)",
|
||
"status": "ok", "sw": "9103", "sent": "80C2000009...",
|
||
"checks": [{"label": "SW", "ok": true, "expected": "91??",
|
||
"actual": "9103", "level": "error"}], "ms": 4}]}
|
||
```
|
||
|
||
`status` becomes `ok`/`warning`/`error`/`stopped` when the run finishes;
|
||
expected/actual pairs are reported per check.
|
||
|
||
### `POST /api/test/stop`
|
||
|
||
Requests a stop (`{"stop": true}` is set on the run); the runner finishes the
|
||
current step, drains any pending proactive command with an **ok** (0x00)
|
||
TERMINAL RESPONSE (the same bounded cleanup as a run that ends - a cancel is
|
||
never sent automatically, v3.21.0) and reports the run as `stopped`.
|
||
|
||
### `POST /api/test/clear`
|
||
|
||
Clears a finished run report (409 while a run is active).
|
||
|
||
### `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; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). A missing `cntr` is seeded from the preset's keyset (v3.20.0); an explicit value wins but never lowers the stored counter (only forward moves are persisted). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The **NV footprint** is measured around the chain: a best-effort `GET DATA FF21` (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required. `delete_package: true` prepends a **cascade GP DELETE** (`P2=0x80`) on the package AID — a leftover package blocks `INSTALL [for load]`, so a re-install can clear it first; the delete step tolerates 'not found' (`6A88`/`6A82`, a fresh card) and any other delete failure stops the chain with its reason (the same policy as the batch and `/api/ram-install-app`).
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"cap_hex": "DECAFFED...",
|
||
"sd_aid": "A000000003000000",
|
||
"install_params": "C900",
|
||
"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 of the install-parameter field — the mandatory `C9` TLV (application-specific parameters; `C900` when empty), the System Specific Parameters `EF`, extra raw TLVs. When empty the server **composes** it from `volatile_quota`/`nv_quota` and `stk_params` (see below) |
|
||
| `stk_params` | no | The STK part appended to the install parameters: the **EF-wrapped** CA (`EF <len> CA <len> …`, TS 102 226 §8.2.1.3.2.1 — the SIM access domain leads the CA value as `01 <ad>` / `00`) or the UICC template (`EA <len> 80 … [81 …]`). A bare `CA` is refused (it belongs inside the EF) |
|
||
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) coded as `C8` / `C7` in the install EF; only composed when `install_params` is empty. `0` is a valid quota (the vendor reference form carried `C7 02 0000 C8 02 0000`); omit the field to leave the TLV out |
|
||
| `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, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
||
| `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it |
|
||
| `includeCpi` | no | As in `/api/send-ota`: `true` (default) marks the delivered SMS with the OTA CPI IE; `false` drops it (the full 140-octet single-SM budget then applies) |
|
||
| `delete_package` | no | If true, prepend a cascade DELETE on the package AID (`P2=0x80` — the load file with its modules and installed applications). The delete step tolerates 'not found' (a fresh card proceeds); any other delete failure stops the chain |
|
||
|
||
**Install-parameter composition (TS 102 226 §8.2.1.3.2.1; GPD_SPE_013 v1.1
|
||
Table 6-5).** The memory quotas (`C7`/`C8`) and an EF-form STK part (the CA)
|
||
must share **one** System Specific Parameters `EF`; an EA-form STK part is a
|
||
sibling of `EF` (the reference TCA loader sends `C9 00 EF 00 EA …`). A
|
||
second `EF` makes the card read the quotas and ignore the CA (live
|
||
2026-10-05: `por_ok` but no toolkit registration and no menu), so the server
|
||
refuses the combination with a 400 and names the fix:
|
||
|
||
- caller-composed `install_params` that already carry an `EF` **plus** an
|
||
EF-form `stk_params` → refused;
|
||
- `nv_quota`/`volatile_quota` next to non-empty `install_params` → refused;
|
||
- with an empty `install_params` the server composes
|
||
`C900 EF{C7?,C8?,CA}` (or `C900 EF{quotas} EA…` for the EA form).
|
||
|
||
The PWA composes the same single EF itself, so its RAM installs never hit the
|
||
refusal.
|
||
|
||
**Response (success):**
|
||
```json
|
||
{"success": true, "failed_step": null,
|
||
"steps": [{"name": "INSTALL [for load]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "por_cntr": "000000011B", "sw": "9000", "bytes": 58, "segments": 1},
|
||
{"name": "LOAD (1/9)", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 274, "segments": 3},
|
||
{"name": "INSTALL [for install]", "por_status": "por_ok", "por_sw": "9000", "por_type": "compact", "sw": "9000", "bytes": 66, "segments": 1}],
|
||
"final_cntr": "0000000004",
|
||
"load_file_aid": "A000000003000000",
|
||
"module_aid": "A000000003000000",
|
||
"application_aid": "A000000003000000",
|
||
"load_block_size": 240,
|
||
"load_block_size_requested": null,
|
||
"load_block_size_auto": true,
|
||
"nv_before": 50646,
|
||
"nv_after": 23400,
|
||
"nv_delta": 27246}
|
||
```
|
||
|
||
`load_block_size` is the effective size used for the LOAD blocks (240 by
|
||
default), `load_block_size_requested` echoes an explicit `load_block_size`
|
||
(null = the default was used) and `load_block_size_auto` marks that default.
|
||
Each step reports the secured packet size `bytes` and the number of SMS
|
||
`segments` it took. `sw` is the transport (ENVELOPE/GET RESPONSE) status
|
||
word; the RAM results are in `por_status` (the PoR verdict: `por_ok`,
|
||
`rc_cc_ds_failed`, `cntr_low`, ... or `no_por` when the card sent none) and
|
||
`por_sw` (the remote command's own status word from the compact/expanded
|
||
response, with `por_type`/`por_cntr`/`por_data`/`por_raw` for context).
|
||
|
||
`nv_before`/`nv_after` are the free non-volatile memory (bytes) read via
|
||
`GET DATA FF21` before and after the chain, `nv_delta` their difference (what
|
||
the operation consumed; negative = memory was released, e.g. a discarded
|
||
partial load). The three fields are omitted when the card has no FF21
|
||
readout; on a failure the values still show what the failed attempt consumed.
|
||
The two FF21 reads consume counters like any accepted packet, so
|
||
`final_cntr` already includes them.
|
||
|
||
A step fails when the PoR is not `por_ok`, when `por_sw` is outside the
|
||
success set (`9000`, `61xx` more data, `62xx`/`63xx` warnings, `CAFE` GP
|
||
"more data"), or when response data arrived but could not be decoded. The
|
||
failing step carries `por_error` (e.g. `remote SW 6700`) and the response
|
||
sets `success: false`, `failed_step` and a detailed `error` such as
|
||
`LOAD (1/9): remote SW 6700`.
|
||
|
||
**Response (failure):**
|
||
```json
|
||
{"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/ram-batch/start`, `GET /api/ram-batch/status`, `POST /api/ram-batch/stop`, `POST /api/ram-batch/clear`
|
||
|
||
The **batch operations** (v3.28.0; the `reinstall-app`/`delete` kinds
|
||
v3.29.0): one operation applied to card after card, for bulk provisioning or
|
||
repair. The loop runs **in the server** (a worker thread), so a page reload or
|
||
a closed tab does not interrupt it; the state lives in memory and a server
|
||
restart ends the run.
|
||
|
||
- `POST /api/ram-batch/start` — `kind` selects the operation; the plan is card
|
||
independent and is built once, so a bad `.cap`, AID or parameter combination
|
||
is a `400` at Start, not a per-card failure. The body carries the kind's
|
||
fields **without** the per-card ones (`preset_id`, `tar`, `spi1`, `cntr`,
|
||
`kic`/`kid`/`kicKey`/`kidKey`):
|
||
- `kind: "install-cap"` (default) — the `/api/ram-install` payload
|
||
(`cap_hex`, `sd_aid`, `install_params`, …) plus `delete_package` (a
|
||
cascade DELETE on the package AID before the chain, see `/api/ram-install`);
|
||
- `kind: "reinstall-app"` — the `/api/ram-install-app` payload (`loadfile_aid`,
|
||
`module_aid`, `instance_aid`, `privileges`, `install_params`,
|
||
`stk_params`, `make_selectable`, `nv_quota`/`volatile_quota`): DELETE the
|
||
applet instance (tolerating 'not found'), then INSTALL [for install] from
|
||
the loaded package — no `.cap`, no LOAD;
|
||
- `kind: "delete"` — `aid` (5-16 bytes of hex) and `target`: `"app"`
|
||
deletes the object only (an applet instance, `P2=00`), `"package"` the
|
||
object and its related objects (the load file with its modules and
|
||
installed applications, `P2=80`).
|
||
|
||
**No card is needed** — the worker waits for one. `400` for a bad request
|
||
or an unknown `kind`, `409` when another batch or a test run is active. The
|
||
response is the status payload below plus `ok: true`.
|
||
- `GET /api/ram-batch/status` — **card-free** (the panel polls it while an
|
||
install holds the card lock):
|
||
|
||
```json
|
||
{"active": true, "phase": "waiting_card", "kind": "reinstall-app",
|
||
"attempts": [{"ts": 1760000000.0, "iccid": "8970119000004600098",
|
||
"preset": "Batch 0002", "result": "installed",
|
||
"duration_ms": 4120, "final_cntr": "0000000005",
|
||
"steps": [{"name": "DELETE (application)", "ok": true, "sw": "6101", "not_found": false},
|
||
{"name": "INSTALL [for install]", "ok": true, "sw": "6101", "not_found": false}],
|
||
"steps_total": 2}],
|
||
"attempts_total": 3,
|
||
"current": null, "notice": "Card 8970119000004600098 reinstalled.",
|
||
"counts": {"installed": 2, "failed": 1, "skipped": 0},
|
||
"started": 1760000000.0, "stopped": null, "progress": null}
|
||
```
|
||
|
||
`phase` is `idle` (no run), `waiting_card`, `installing`, `stopping`,
|
||
`stopped` or `finished`; `kind` names the operation (the panel's labels
|
||
follow it: installed / deleted / reinstalled). A failed attempt carries
|
||
`failed_step` (e.g. `LOAD (3/7)`), `por_sw` (the card's own status word),
|
||
`por_status`, `por_error` and `sw` — or, when the chain died before a verdict
|
||
(a lost card, a transport error), just `error`; a skipped one carries
|
||
`reason` (a request/eligibility error: the operation never started). An
|
||
attempt also carries the last three steps (`steps`: `[{name, ok, sw,
|
||
not_found}]` plus `steps_total`), so the panel shows the per-step verdicts
|
||
(`DELETE (application) 6101 · INSTALL [for install] 6101`) for the short
|
||
delete/reinstall chains; a delete step that answered 'not found' has
|
||
`not_found: true` and `ok: true`. `current` names the card being processed
|
||
(`{iccid, preset, started}`) and `progress` mirrors the RAM progress payload
|
||
while a chain runs. The report keeps the last 500 attempts
|
||
(`attempts_total` counts them all).
|
||
- `POST /api/ram-batch/stop` — sets the stop flag: a running install finishes
|
||
first (an interrupted LOAD chain can leave a half-loaded package), then the
|
||
run ends. It touches no card state, so it answers while the card lock is
|
||
held (a `_CARD_FREE_POST` exception); a repeated stop is idempotent, `409`
|
||
when no run exists.
|
||
- `POST /api/ram-batch/clear` — drops the finished run's report (`409` while a
|
||
batch is active).
|
||
|
||
**Per-card resolution.** Every equipped card's **preset is matched by
|
||
ICCID**; the preset's ISD role TAR supplies the keyset (v3.26.0), the keys and
|
||
the counter seed (v3.20.0) — the same rules as `/api/ram-install`. A card
|
||
with no readable ICCID, no matching preset or no usable ISD keyset is recorded
|
||
as `skipped` with its reason and the batch keeps waiting. A card is attempted
|
||
**once per run** (any outcome): re-equipping it is recorded as
|
||
`already processed in this batch (<outcome>)`, never installed again —
|
||
restart the batch to retry it. Failures never stop the run and are never
|
||
retried automatically. A **delete step** (the reinstall's instance delete,
|
||
the delete kind, the `delete_package` pre-step) tolerates 'not found'
|
||
(`6A88`/`6A82` — the object is already gone, the goal is met); any other
|
||
delete failure stops that card's chain and the attempt reports the delete's
|
||
reason.
|
||
|
||
`/api/status` carries a small summary in `ram_batch` — the detailed state
|
||
**minus the attempts list** (`{active, phase, current, notice,
|
||
attempts_total, counts, started, stopped}`), so the panel's headline (the
|
||
phase, the current card, the counts) rides the existing 2 s status poll; the
|
||
attempts report is fetched from `/api/ram-batch/status` only when
|
||
`attempts_total` (or the active flag) changed.
|
||
|
||
### `POST /api/ram-install-app`
|
||
|
||
Run **INSTALL [for install]** (or **INSTALL [for make selectable]**) for an *already loaded* package — no `.cap` and no LOAD, so the install parameters can be iterated without re-loading. Since v3.29.0 the install **deletes the applet instance first** (`mode: "install"`; the operation does not delete the package, so the instance is removed for a clean re-install), tolerating 'not found' (`6A88`/`6A82` — a fresh card just installs); any other delete failure stops the chain with its reason. `mode: "make_selectable"` stays a single APDU. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure).
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"mode": "install",
|
||
"loadfile_aid": "F0414C46416101",
|
||
"module_aid": "F0414C4641610101",
|
||
"instance_aid": "",
|
||
"privileges": "00",
|
||
"install_params": "C900",
|
||
"stk_params": "",
|
||
"make_selectable": true,
|
||
"spi1": "0E", "spi2": "01",
|
||
"kic": "15", "kid": "15",
|
||
"tar": "000000",
|
||
"cntr": "0000000001",
|
||
"kicKey": "D6FCC023...",
|
||
"kidKey": "1B07E7E0..."
|
||
}
|
||
```
|
||
|
||
| Field | Req | Description |
|
||
|---|---|---|
|
||
| `mode` | no | `install` (default) or `make_selectable` (GP Table 11-44; needs `instance_aid`) |
|
||
| `loadfile_aid` | cond | Package AID (required for `mode: "install"`) |
|
||
| `module_aid` | cond | Executable module / applet class AID (required for `mode: "install"`) |
|
||
| `instance_aid` | no | Application AID; empty → the module AID |
|
||
| `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` |
|
||
| `install_params` / `stk_params` | no | As in `/api/ram-install`: the `C9` field (with any extra TLVs) and the STK part; an EF-form STK part must not follow a caller-composed `EF` (the quotas and the CA share one `EF`, v3.20.0) |
|
||
| `nv_quota` / `volatile_quota` | no | As in `/api/ram-install`: composed into the install `EF` only when `install_params` is empty |
|
||
| `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` |
|
||
| `includeCpi` | no | As in `/api/ram-install`: `true` (default) marks the delivered SMS with the OTA CPI IE |
|
||
|
||
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`. A missing `cntr` is seeded from the preset's keyset (v3.20.0; an explicit value wins but never lowers the stored counter).
|
||
|
||
### `POST /api/cap-compat`
|
||
|
||
Run the **CAP compatibility test**: the format check, `INSTALL [for load]` and the LOAD blocks only up to the block that completes the **Import** component (the point where the JCRE verifies the import list, JC VM spec 4.5.2), then stop — no last-block flag and no `INSTALL [for install]`, so nothing is committed and nothing has to be deleted afterwards. The verdict (`imports_ok`) covers the **LOAD/import gate only**; the link gate is what a real install checks at `INSTALL [for install]`.
|
||
|
||
**Request body:** as `/api/ram-install` (`cap_hex`, the SCP80 fields, optional `load_block_size`, `ram_format` and `includeCpi`) — a missing `cntr` is seeded from the preset's keyset (v3.20.0) — plus:
|
||
|
||
| Field | Req | Description |
|
||
|---|---|---|
|
||
| `probe_imports` | no | `{"all": "x.y"}` or `{aid_hex: "x.y"}`: rewrite the Import component's requested versions (diagnostic) |
|
||
| `probe_additions` | no | `[{"aid": hex, "version": "x.y"}]`: append synthetic imports (card-capability queries; an AID already present becomes a version override) |
|
||
|
||
**Response:** `{"success": bool, "imports_ok": bool, "steps": [...], "boundary_block": N, "total_blocks": M, "load_file_aid": "...", "module_aid": "...", "ram_format": "...", "probe_imports": [...], "probe_unmatched": [...], "final_cntr": "...", "load_block_size": N, "error": "...", "failed_step": N}`.
|
||
|
||
`probe_imports` lists the entries changed or appended (`{"aid": "...", "from": "1.3", "to": "0.0"}`; `from: null` = an appended synthetic entry). `probe_unmatched` lists override AIDs that matched no Import entry — the CAP does not import them, so the card is never asked about them — as `[{"aid": "...", "version": "0.0"}]` with the version the probe requested. The PWA renders both as a per-AID list in the result popup, together with the failing block's CAP section and the remote status word's meaning.
|
||
|
||
### `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. A missing `cntr` is seeded from the named
|
||
preset's keyset (v3.20.0) — the same rule the PWA's builder uses, so a
|
||
reference for a packet built from an untouched counter field matches.
|
||
|
||
```json
|
||
{"spi1": "16", "spi2": "01", "kic": "15", "kid": "15", "tar": "b00000",
|
||
"cntr": "0000000001", "apdu": "00a40000023f00",
|
||
"kicKey": "D6FCC023...", "kidKey": "1B07E7E0..."}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{"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:**
|
||
```json
|
||
{"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).
|
||
|
||
```json
|
||
{"item_id": 128}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{"type": "display_text", "text": "Hello", "sw": "9122"}
|
||
```
|
||
or
|
||
```json
|
||
{"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.
|
||
|
||
```json
|
||
{"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.
|
||
```json
|
||
{"active": true, "pending": true, "pending_type": "select_item"}
|
||
```
|
||
|
||
### `POST /api/read`
|
||
|
||
Read file content. Auto-detects transparent vs record files.
|
||
|
||
```json
|
||
{"name": "EF.ICCID", "fid": "2FE2", "parent_path": ["MF"], "mode": "raw"}
|
||
```
|
||
|
||
Returns transparent data:
|
||
```json
|
||
{"success": true, "sw": "9000", "file_type": "transparent", "data": "...",
|
||
"apdu_times": [{"type": "select", "ms": 12}, {"type": "read_binary", "ms": 9}]}
|
||
```
|
||
|
||
Returns records:
|
||
```json
|
||
{"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.
|
||
|
||
```json
|
||
{"name": "EF.ICCID", "fid": "2FE2", "data": "A0A1A2...", "parent_path": ["MF"]}
|
||
```
|
||
|
||
A full path may be used instead of name/fid/parent, like `/api/select` and
|
||
`/api/read`:
|
||
|
||
```json
|
||
{"path": "ADF.USIM/6F7E", "data": "4FE69DE7..."}
|
||
```
|
||
|
||
For record files:
|
||
```json
|
||
{"name": "EF.ADN", "fid": "6F3A", "data": "A0A1...", "record_nr": 1, "parent_path": ["MF", "7F10"]}
|
||
```
|
||
|
||
Returns:
|
||
```json
|
||
{"success": true, "sw": "9000"}
|
||
```
|
||
|
||
### `POST /api/select`
|
||
|
||
Select a file by name or FID, with optional parent selection.
|
||
|
||
```json
|
||
{"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:
|
||
```json
|
||
{"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.
|
||
|
||
```json
|
||
{"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:
|
||
```json
|
||
{"exists": true, "name": "MF", "fid": "3F00", "file_type": "df", "children": [{"name": "EF.ICCID", "fid": "2fe2", "isDir": false}]}
|
||
```
|
||
|
||
**PBR-derived children**: when the requested node is the DF that holds the
|
||
card's **EF_PBR** (`4F30`, TS 31.102 §4.4.2.1 — `MF/7F10/5F3A`,
|
||
`ADF.USIM/5F3A` or `ADF.USIM-NON-IMSI/5F3A`), the server parses the cached
|
||
PBR records (see `GET /api/pbr`) and appends the phonebook files the PBR
|
||
references as children — the files are not in pySim's model, so they would
|
||
otherwise be invisible:
|
||
|
||
```json
|
||
{"name": "EF.ADN", "fid": "4F3A", "isDir": false, "pbr": true, "sfi": "02"}
|
||
```
|
||
|
||
`fid` is uppercase; `pbr: true` marks a PBR-derived node and `sfi` carries
|
||
the file's short file identifier when the PBR declares one (`null`
|
||
otherwise). A file pySim's model already lists is not added twice, and the
|
||
matching is by the requested node's full normalised path (uppercase, `3F00`
|
||
→ `MF`); a bare DF FID that cannot be told apart (the three PBR copies all
|
||
sit in a DF with FID `5F3A`) injects nothing. The PWA renders these nodes
|
||
like any other tree node — no frontend change.
|
||
|
||
Because the PBR files are not in the model, `POST /api/tree`, `/api/select`,
|
||
`/api/read` and `/api/write` **auto-allow the probe** for exactly the FIDs
|
||
the parsed PBR references (the PWA only sets `allow_probe` for its own custom
|
||
files), so the injected nodes can be selected and read without any client
|
||
flag. The probe stays scoped to those FIDs and the temporary model file is
|
||
detached again after the request. An explicit `allow_probe: false` does not
|
||
disable it — the server knows the file exists, so a caller cannot make the
|
||
tree's own nodes unselectable (the flag remains the PWA's custom-file switch).
|
||
|
||
### `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.
|
||
|
||
```json
|
||
{"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:
|
||
|
||
```json
|
||
{"sw": "9000", "data": "..."}
|
||
```
|
||
|
||
For **Poll Interval Negotiation** (event `0x1C`, TS 102 223 §7.5.22) the
|
||
proposal rides in `event_data` as the Duration TLV `04 02 <unit> <interval>`
|
||
(unit `00` minutes / `01` seconds / `02` tenths of seconds) and the UICC's
|
||
response is decoded into `negotiation`:
|
||
|
||
```json
|
||
{"sw": "9000", "data": "1101020402011E",
|
||
"negotiation": {"result": 2, "result_name": "modified",
|
||
"unit": 1, "interval": 30, "seconds": 30}}
|
||
```
|
||
|
||
`result` is `00` accepted / `01` rejected / `02` modified (§8.97); no response
|
||
data means *accepted*. A *modified* answer sets the background poll interval,
|
||
and a *rejected* answer may carry the closest acceptable duration.
|
||
|
||
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 (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`.
|
||
|
||
### `POST /api/net-sim`
|
||
|
||
Runs one network-condition scenario from `projects/UICC_NAA.md` section 13
|
||
against the equipped card and returns the step log:
|
||
|
||
```json
|
||
{"scenario": "service_lost", "mcc": "262", "mnc": "01", "lac": "6CD7",
|
||
"send_event": true, "dummy_locations": true, "keep_kasme": true}
|
||
```
|
||
|
||
Scenarios: `cold_boot`, `attach_eps`, `attach_2g`, `service_lost`,
|
||
`limited_service`, `roaming_denied`, `churn`, `sms_received`, `cb_reconfig`,
|
||
`authenticate`. Optional parameters: `mcc`/`mnc` (or `plmn`), `lac`,
|
||
`cell_id`, `tac`, `rac`, `tmsi`, `ptmsi`, `ptmsi_sig`, `guti`, `ksi`,
|
||
`kasme`, `ul`, `dl`, `algo`, `kc`, `rand`, `autn`, `churn_count`,
|
||
`churn_delay_ms`, and the toggles `send_event`, `dummy_locations`,
|
||
`invalidate_epsnsc`, `keep_kasme`, `write_kc`, `sms_location`, `cb_clear`
|
||
(empty identity values are randomized). The event step is skipped when the
|
||
card did not subscribe to Location status; only UPDATE BINARY/RECORD,
|
||
ENVELOPE and AUTHENTICATE are sent — EF.FPLMN is appended only by
|
||
`roaming_denied` (shift-list semantics of TS 31.102 §4.2.16, duplicates
|
||
skipped) and an attach to a listed PLMN clears its entry first (successful
|
||
manual selection, TS 23.122), while the 5GS location files are never written.
|
||
The response is `{success, error, steps:[{action, file, path, data, sw, ok}],
|
||
net_state}` where `net_state` is the updated monitor state (see
|
||
`GET /api/net-state`).
|
||
|
||
### `GET /api/net-state`
|
||
|
||
Returns the cached **network-state monitor** — the panel the PWA's Phone tab
|
||
shows next to the simulation buttons. The state is created when a card is
|
||
equipped (only when EF.ICCID was readable) and is updated in place from the
|
||
bytes the network simulation wrote, so this endpoint performs no card I/O:
|
||
|
||
```json
|
||
{"available": true, "state": {
|
||
"files": {"imsi": {"name": "EF.IMSI", "fid": "6F07",
|
||
"path": "ADF.USIM/6F07", "present": true,
|
||
"source": "init", "updated": 1758396000.0,
|
||
"kind": "transparent", "data": "0829051032547698"},
|
||
"epsnsc": {"kind": "record",
|
||
"records": [{"num": 1, "data": "..."}]}},
|
||
"service": {"state": "normal", "source": "service_lost",
|
||
"time": 1758396123.0},
|
||
"network": {"service": {"state": "normal"},
|
||
"location": {"plmn": "26201", "mcc": "262", "mnc": "01",
|
||
"area": "6CD7", "lac": "6CD7",
|
||
"country": "Germany", "operator": "Telekom",
|
||
"roaming": "home", "rejected": false,
|
||
"source": "write"},
|
||
"home": "26201"},
|
||
"read_at": 1758396000.0}}
|
||
```
|
||
|
||
`available: false` with `state: null` means no monitor state exists yet (no
|
||
card equipped or EF.ICCID not readable). `service.state` is the simulated
|
||
service state (`normal` / `limited` / `none`, `null` until a scenario or a
|
||
Location status event sets it); `network.location.roaming` is `home` /
|
||
`equivalent` / `guest` against EF.HPLMNwAcT and EF.EHPLMN, and `rejected` is
|
||
set when the location files or EF.FPLMN show a permanent rejection.
|
||
Monitored file keys: `imsi`, `ehplmn`, `spdi`, `hplmnwact`, `loci`, `psloci`,
|
||
`epsloci`, `epsnsc`, `cbmi`, `cbmir`, `smsstatus`, `fplmn`; each entry's
|
||
`source` is `init` (equip read), `read`, `refresh` or `write` (patched from
|
||
the simulator's own writes). The PWA decodes the entries client-side.
|
||
|
||
### `POST /api/net-state-refresh`
|
||
|
||
Re-reads the monitored files from the card, merges them into the cached state
|
||
(`source: refresh`) and returns the same shape as `GET /api/net-state`. The
|
||
optional body filters the read:
|
||
|
||
```json
|
||
{"files": ["imsi", "loci", "epsnsc"]}
|
||
```
|
||
|
||
Without a body all monitored files are re-read. Answers `503` when the reader
|
||
is not initialized.
|
||
|
||
### `GET /api/arr`
|
||
|
||
Returns the card's cached **EF.ARR copies** — the access-rule files the FCP
|
||
security attribute DO `'8B'` references (ISO 7816-4 Table 25). The copies
|
||
are read once per card session when a card is equipped (`MF/2F06`,
|
||
`MF/7F10/6F06`, `MF/7F20/6F06` and `ADF.USIM/6F06`, best effort) and served from memory, so this endpoint
|
||
performs no card I/O; a disconnect clears the cache, and an `EF.ARR` write
|
||
through `/api/write` refreshes it.
|
||
|
||
```json
|
||
{"ok": true, "session": 12, "arrs": [
|
||
{"fid": "2F06", "path": "MF/2F06", "record_len": 10, "num_records": 23,
|
||
"records": ["FFFF...", "..."]}]}
|
||
```
|
||
|
||
`ok` is `false` (with an empty `arrs`) when the card has no readable ARR.
|
||
The PWA's file manager resolves the `'8B'` reference of the selected file's
|
||
FCI against this cache and shows the referenced record's access rules as
|
||
indented lines; snapshots resolve the same references from their own
|
||
captured `EF.ARR` copy.
|
||
|
||
### `GET /api/pbr`
|
||
|
||
Returns the card's cached **EF.PBR records** — the phonebook reference file
|
||
(`4F30`, TS 31.102 §4.4.2.1), whose BER-TLV structure describes the
|
||
phonebook files (the `'A8'`/`'A9'`/`'AA'` containers and the primitive
|
||
phonebook kinds). The copies are read once per card session when a card is
|
||
equipped (`MF/7F10/5F3A/4F30`, `ADF.USIM/5F3A/4F30`,
|
||
`ADF.USIM-NON-IMSI/5F3A/4F30`, best effort, at most `PBR_RECORD_LIMIT` = 16
|
||
records each) and served from memory, so this endpoint performs no card I/O;
|
||
a disconnect clears the cache.
|
||
|
||
```json
|
||
{"ok": true, "session": 12, "pbrs": [
|
||
{"fid": "4F30", "path": "MF/7F10/5F3A/4F30", "record_len": 32,
|
||
"num_records": 1, "records": ["A80AC0034F3A02C5034F0904AA0A..."]}]}
|
||
```
|
||
|
||
`ok` is `false` (with an empty `pbrs`) when the card has no readable PBR.
|
||
The records are served raw: the PWA owns the decoder for the decoded file
|
||
views (`efDecPbr`, TS 31.102 §4.4.2.1 — the containers, the primitive tags,
|
||
the master EF and the EF_IAP reference). The spec leaves the `'AA'`
|
||
container's "path to the file" form unspecified; a container that does not
|
||
parse as primitive tags is kept raw (the decoder notes it) and its files are
|
||
neither resolved nor injected into the tree — only same-directory FIDs are. The server keeps a small parser of
|
||
the same structure only to populate the file tree — `/api/tree` appends the
|
||
referenced phonebook files as children of the DF that holds the PBR and
|
||
auto-allows the probe for exactly those FIDs (see the `/api/tree` section).
|
||
The two parsers share the decode vectors in `frontend/tests/pbr_vectors.json`
|
||
(drift-guarded from both suites).
|
||
|
||
### `GET /api/proactive-log`
|
||
|
||
Returns the last 50 proactive commands fetched during CAT sessions, newest
|
||
first:
|
||
|
||
```json
|
||
[{"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:
|
||
|
||
```json
|
||
{"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
|
||
(plus the profile used):
|
||
|
||
```json
|
||
{"ok": true, "profile": "FFFF...", "menu": true, "events": [4, 5]}
|
||
```
|
||
|
||
### `GET /api/terminal-profile`
|
||
|
||
The TERMINAL PROFILE currently in effect and the CLI default (for reference;
|
||
runtime changes are in-memory only):
|
||
|
||
```json
|
||
{"profile": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003",
|
||
"bytes": 33,
|
||
"cli_default": "FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003"}
|
||
```
|
||
|
||
### `POST /api/terminal-profile`
|
||
|
||
Sets the TERMINAL PROFILE at runtime (in-memory) and re-sends it to the card,
|
||
resetting the STK session state exactly like `/api/rescue`. Body with a new
|
||
profile, or `{}` to re-send the current one:
|
||
|
||
```json
|
||
{"profile": "FFFFFFFF7F1F007FFF00001F230811060700"}
|
||
```
|
||
|
||
Hex, even number of digits, 1–255 bytes. Response is the same shape as
|
||
`/api/rescue` (with `ok: true`); invalid hex is a 400, no reader a 503.
|
||
|
||
### `GET /api/poll-status`
|
||
|
||
Background STATUS polling state. Polling is enabled by default (a CAT
|
||
terminal polls during idle, TS 102 221 §14.6.2; `--poll-interval 0` disables
|
||
it at startup) and `enabled` is the operator's switch. `card_disabled` is true
|
||
after the card sent POLLING OFF (TS 102 223 §6.4.14): proactive polling stays
|
||
suspended until a POLL INTERVAL arrives, independent of `enabled` - the PWA
|
||
shows the effective state (OFF while suspended).
|
||
|
||
```json
|
||
{"enabled": true, "interval": 30, "card_disabled": false}
|
||
```
|
||
|
||
### `POST /api/poll-toggle`
|
||
|
||
Turns background STATUS polling on or off.
|
||
|
||
```json
|
||
{"enabled": true}
|
||
```
|
||
|
||
Returns the new state (`{"enabled": ..., "interval": ..., "card_disabled": ...}`);
|
||
while the card has disabled polling, enabling adds a `warning` explaining
|
||
that the timer stays suspended until the card sends a new POLL INTERVAL.
|
||
|
||
### `GET /api/pli-qualifiers`
|
||
|
||
Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.
|
||
|
||
```json
|
||
[{"code": "00", "name": "Location Information"}, {"code": "0A", "name": "Battery Charge Level"}]
|
||
```
|
||
|
||
### `GET /api/pli-dict`
|
||
|
||
Returns the effective PLI data dictionary as a qualifier-code map: the
|
||
operator's override, else the built-in default (the qualifiers whose objects
|
||
the pinned ETSI specs define - location as the test network 001/01, the test
|
||
IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1).
|
||
The date/time qualifier stays empty - the host clock answers it while no
|
||
override is set.
|
||
|
||
```json
|
||
{"00": "930500F1100000", "0A": "630104", "03": ""}
|
||
```
|
||
|
||
### `POST /api/pli-dict`
|
||
|
||
Updates dictionary entries. Body is a map of qualifier code to hex value.
|
||
A non-empty value becomes an override; an **empty value clears the
|
||
override**, so the built-in default (if any) applies again. Returns the
|
||
updated effective dictionary plus, when any field was not saved, the lists
|
||
that name them: `invalid` (a malformed key or value — not hex, an odd number
|
||
of digits, a non-string value) and `unknown` (a well-formed code outside the
|
||
qualifier list); both keys are omitted when everything was applied (v3.25.6).
|
||
|
||
```json
|
||
{"00": "130500F1100000", "0A": "630102", "invalid": ["0G"], "unknown": ["18"]}
|
||
```
|
||
|
||
### `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:
|
||
|
||
```json
|
||
{"action": "start", "mode": "dump", "host": "127.0.0.1", "port": 8443}
|
||
```
|
||
|
||
Redirect mode (`mode: "redirect"`) starts **no local listener**: every BIP
|
||
channel the card opens is connected to the configured target (`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: "redirect"` with the target while it runs. (This is the behavior that
|
||
was called `passthru` before 2.2.13 — the name is now taken by the mode
|
||
below.)
|
||
|
||
```json
|
||
{"action": "start", "mode": "redirect", "host": "203.0.113.10", "port": 10174}
|
||
```
|
||
|
||
Pass-through mode (`mode: "passthru"`) starts **no listener and has no
|
||
target**: every BIP channel dials the destination the card requests in OPEN
|
||
CHANNEL — the `Other address` (`3E`/`BE`) plus the `Transport level`
|
||
(`3C`/`BC`) port, TCP client remote (`02`) only. The specs define no default
|
||
port, so an incomplete or non-TCP request fails the channel with result `3A`
|
||
and an `open-fail` log reason; `host`/`port` in the request are ignored. The
|
||
status API reports `{"mode": "passthru"}` and the per-channel targets appear
|
||
in `bip.channels`.
|
||
|
||
```json
|
||
{"action": "start", "mode": "passthru"}
|
||
```
|
||
|
||
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.
|
||
|
||
```json
|
||
{"action": "start", "mode": "tls", "host": "127.0.0.1", "port": 8443,
|
||
"psk_map": [{"identity": "89012345678901234567",
|
||
"psk_hex": "00112233445566778899aabbccddeeff"}],
|
||
"script": ["80CAFF2100", "80F28002024F0000"], "script_kind": "Explore ISD"}
|
||
```
|
||
|
||
`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).
|
||
|
||
### `POST /api/bip/control`
|
||
|
||
Generic BIP terminal control for the Simulator's **BIP** pill - the terminal
|
||
side of the Bearer Independent Protocol for cards that use TCP without HTTP
|
||
OTA. The session is shared with the SCP81 listener: only one BIP session runs
|
||
at a time, starting either control stops the other, and the response reports
|
||
what was stopped as `replaced: {"owner": "scp81"|"bip", "mode": ...}` (the
|
||
PWA shows a notice).
|
||
|
||
```json
|
||
{"action": "start", "mode": "sink", "host": "127.0.0.1", "port": 0}
|
||
```
|
||
|
||
Modes:
|
||
|
||
- `sink` (default) - starts a local TCP listener that accepts the card's BIP
|
||
channels and only logs what arrives (`conn`, `sink-rx`, `conn-close`); it
|
||
never sends anything back. `port` may be `0`/omitted for an ephemeral port;
|
||
the bound address is reported in the status. Use it for cards that open a
|
||
TCP connection just to upload data.
|
||
- `passthru` - no listener and no target: each channel dials the destination
|
||
the card requests in OPEN CHANNEL (TCP client only, no default port; an
|
||
incomplete or non-TCP request fails with result `3A`).
|
||
- `redirect` - every channel connects to the required `host`/`port`; the
|
||
address the card requests is only logged.
|
||
|
||
`link_events` (default `true`) controls the terminal-side Channel status
|
||
events (TS 102 223 7.5.11). An invalid request never disturbs a running
|
||
session. Stop with `{"action": "stop"}`; the response carries the same body
|
||
as the status endpoints.
|
||
|
||
### `GET /api/bip/status`
|
||
|
||
Same shape as `GET /api/scp81/status`: `{"owner": "bip"|"scp81"|null,
|
||
"bip": {...}, "listener": {...}}`. A running sink reports
|
||
`{"mode": "sink", "host": ..., "port": ..., "connections": N}`.
|
||
|
||
### `GET /api/bip/log` / `POST /api/bip/log-clear`
|
||
|
||
The shared BIP event log (`?after=<seq>`) - identical to `/api/scp81/log`.
|
||
|
||
### `GET /api/scp81/status`
|
||
|
||
```json
|
||
{"bip": {"enabled": true, "mode": "redirect", "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,
|
||
"version_seen": "TLSv1.2", "cipher_seen": "PSK-AES128-CBC-SHA256"}}
|
||
```
|
||
|
||
`owner` is `scp81` or `bip` (whichever control started the session), `null`
|
||
when idle; it is returned by both this endpoint and `/api/bip/status`.
|
||
|
||
Listener modes: `tls` (local PSK TLS server), `dump` (capture-only TCP
|
||
listener), `redirect` (no local listener; the BIP channels go straight to the
|
||
configured `host:port`, e.g. an external HTTP OTA platform — reported as
|
||
`{"mode": "redirect", "host": ..., "port": ..., "target": "host:port"}`) and
|
||
`passthru` (no listener and no target; each channel dials the destination the
|
||
card requests in OPEN CHANNEL — reported as `{"mode": "passthru"}`, with the
|
||
actual peer in `bip.channels[].target`).
|
||
|
||
`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):
|
||
|
||
```json
|
||
{"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.
|
||
|
||
```json
|
||
{"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00",
|
||
"install_params": "", "stk_params": "", "nv_quota": 0, "volatile_quota": 0,
|
||
"make_selectable": true}
|
||
```
|
||
|
||
`sd_aid` empty = the ISD. `install_params`, `stk_params` and the
|
||
`nv_quota`/`volatile_quota` primitives follow the composition rule of
|
||
`/api/ram-install` (the quotas and an EF-form STK part share one System
|
||
Specific Parameters `EF`); an invalid combination answers
|
||
`{"ok": false, "error": "..."}`. Responds with `{"ok": true, "apdus": [...],
|
||
"load_file_aid": ..., "module_aid": ...}`.
|
||
|
||
### `GET /api/scp81/script`
|
||
|
||
Returns the configured command script and the execution state:
|
||
|
||
```json
|
||
{"script": ["80CAFF2100", "80F28002024F0000"], "next": 2, "total": 2,
|
||
"done": [0, 1], "kind": "Explore ISD",
|
||
"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`.
|
||
|
||
The connection stays open between POSTs for the whole dialog (the card is the
|
||
HTTP client and may reuse it or dial a new one at will — GP Am. B 4.3.1 leaves
|
||
connection management to the SD); only the 204 ends the session, and the server
|
||
then shuts the TLS session down cleanly (`close_notify` while the response is
|
||
still buffered, then FIN). There is no option to close after every response.
|
||
|
||
`compact_headers` (default `false`) omits the optional space after each header
|
||
colon — legal per RFC 7230 3.2 (OWS), though a single SP is preferred, and it
|
||
saves one byte per header (useful to keep a response within one card-sized TLS
|
||
record); `conn_header` (default `'none'` = omit the header; `'keep-alive'` adds
|
||
it) declares the connection fate, `tls_version` (default `'auto'` — accept TLS
|
||
1.0-1.2 and let OpenSSL pick the highest the card offers; `'1.0'`/`'1.1'`/`'1.2'`
|
||
pin a version for debugging) selects the protocol window, `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`, all modes) 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.
|
||
|
||
The response headers are minimal: `X-Admin-Protocol`, the optional
|
||
`X-Admin-Next-URI`/`X-Admin-Targeted-Application`, `Content-Type` on 200s and
|
||
`Transfer-Encoding`/`Content-Length` per `chunked`. The Date/Server/X-Powered-By
|
||
mimicry (`apache_headers`) was removed in 2.2.14 — the reference server's extra
|
||
headers were an unnecessary copy (the real blocker was the BIP TERMINAL
|
||
RESPONSE BER length) and the card accepts the minimal set.
|
||
|
||
`GET /api/scp81/status` echoes the negotiated handshake as `version_seen` /
|
||
`cipher_seen` (plus `identity_seen`); a handshake that fails for TLS/cipher
|
||
reasons (no shared cipher, unsupported protocol version, card alert) is logged
|
||
as `tls-handshake-failed` with the OpenSSL reason, separately from
|
||
post-handshake record errors (`tls-error`).
|
||
|
||
The PWA's Listener **Options** block (collapsed by default; a `custom` marker
|
||
appears when anything differs from the reference defaults) exposes `chunked`,
|
||
`chunk_size`, `conn_header`, `compact_headers`, `next_uri` (checkbox + template),
|
||
`script_template`, `cr_tag`, `targeted_app` (checkbox + `//aid/...` field) and
|
||
`link_events` (applied at Start, persisted in `localStorage`); `tls_version`,
|
||
`cipher`, `keylog` and `answer_delay` stay API-only.
|