Low-counter reporting, counter sync probe, Bad-format decode, expanded forms

Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.

- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
  suggested_cntr and the UI's "(card counter ...)" text are gone - the value
  is not observable.  The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
  with a read-only command until the card accepts one, then accepted+1 is
  persisted (monotonic, source counter-probe).  Rejected attempts change
  nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
  below the 40-bit maximum where the counter blocks) and after max_attempts
  (default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
  7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
  carries bad_format/bad_format_name and _ram_step_result fails the step with
  "bad format 02 (wrong length)" instead of falling through to the compact
  decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
  (AE 80 ... 00 00) scripting template; the RAM command formats are compact/
  expanded/expanded-ae and _ram_detect_format probes compact, then both
  expanded codings (the expanded probes use Le='00' and no chained GET
  RESPONSE, TS 102 226 5.2.1.1).  The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
  0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
  ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
  and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
  template forms, the Bad-format TLV (definite and plain tag styles), the
  live ISD response through _decode_por, the step-result failure, and the PWA
  guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
This commit is contained in:
2026-10-02 00:15:40 +03:00
parent ad2a253f2a
commit 391d4a322d
10 changed files with 683 additions and 160 deletions
+35
View File
@@ -37,6 +37,7 @@ a 3.x PWA).
| `/api/presets/delete` | POST | Delete a preset (`{id}`) |
| `/api/presets/import` | POST | Import presets (`{presets: [...], mode: merge\|replace}`) |
| `/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) |
@@ -246,6 +247,40 @@ import also accepts presets exported by the older localStorage-based builds.
`"replace"`) → `{"ok": true, "added": N, "skipped": M, "errors": […]}`;
invalid entries and duplicates are skipped and reported, never fatal.
### `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 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,
"apdu": "00A40000023F00"}
```
All fields except `preset_id` are optional: `kvn` picks a keyset (the first
otherwise), `tar`/`spi1`/`spi2` default to the preset's values, `cntr` to the
keyset's counter, `ceiling` to `FFFFFFFF` (must stay below `FFFFFFFFFF`),
`max_attempts` to 40 (clamped to 1–64), and `apdu` to a read-only `SELECT MF`
(the probe command should never modify anything). A SPI1 without a counter
check is refused (400).
**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