fix: capture the submit PoR on every card + per-operation RAM format check (v3.6.24)

Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow
showed why only one card works:

- The PoR capture was gated on SPI2 bit 0x20.  The traced cards deliver the
  PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries
  (SPI2=0x01) the fetched TPDU was thrown away - the Explore died with
  "no data" and the RAM install steps silently degraded to an unverified
  "no_por".  The capture is now unconditional (inline ENVELOPE responses and
  proactive submits both work), and the STATUS-poll fallback is enabled for
  every card.
- `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit
  counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step -
  the server-side half of the v3.6.23 counter report.  It now advances the
  full 40 bits.
- RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded
  `AA` > `22` form) is detected per operation and never stored:
  /api/ram-install and /api/ram-install-app always probe, /api/send-ota
  probes when the caller passes `ram_format`.  The probe is a read-only
  GET STATUS [ISD] as the first step (compact, then wrapped) and every
  command of the operation uses the detected format.  `_wrap_expanded_apdu`
  is pinned to the reference bytes `AA0A220880F24000024F0000`; responses
  carry `ram_format` and `final_cntr` (accepted probes consume counters, so
  the views use the server's next counter).  Applet-directed payloads stay
  byte-exact; only RAM views (and RAM/GP chain packs) opt in.

504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
This commit is contained in:
2026-09-28 22:38:17 +03:00
parent 991be7353b
commit cf931da2f9
6 changed files with 234 additions and 30 deletions
+17 -3
View File
@@ -306,6 +306,16 @@ 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.
**Request body:**
```json
{
@@ -317,7 +327,8 @@ status word and data.
"tar": "b00000",
"cntr": "0000000001",
"kicKey": "D6FCC023...",
"kidKey": "1B07E7E0..."
"kidKey": "1B07E7E0...",
"ram_format": "auto"
}
```
@@ -325,6 +336,7 @@ status word and data.
```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": ""}}}
@@ -489,7 +501,7 @@ 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). The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
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). 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 `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
**Request body:**
```json
@@ -519,6 +531,7 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) for `gen_install_parameters()` |
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 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 |
**Response (success):**
```json
@@ -594,8 +607,9 @@ Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**)
| `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 PWA composes `C9`+`EF`+raw and appends the STK part) |
| `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` |
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
### `POST /api/sp-verify`