esim: drive the profile switch ourselves; guard the FCP metadata

Disabling a profile failed with 6985 and left the card stuck until an
equip.  pySim's send_apdu_checksw auto-handler keeps flushing proactive
commands after the REFRESH TERMINAL RESPONSE; the card is then mid-switch
and answers 6985 to the next FETCH, which propagated as a 500 and skipped
the re-initialization.  Per SGP.22 v2.6 5.7.16/5.7.17 a 91XX answer is
the ISD-R's 'result OK before REFRESH' (step 6) and the switch completes
on the TERMINAL RESPONSE or the following RESET (step 8) - lpac treats
91XX the same way and never retries.

- esim.py: build_switch_apdu/parse_switch_response/switch_profile split
  out of set_profile_state; the switch is one raw STORE DATA via
  scc._tp.send_apdu, a 91XX runs our own FETCH/TR chain (status_poll=False)
  and is reported as ok, the STORE DATA is never retried and a chain
  failure still counts the accepted switch.
- server.py: /api/esim/profile answers the REFRESH with our chain, then
  re-initializes the card and re-reads the profile list, returning
  verified/state_after; _handle_proactive_chain grew status_poll.
- /api/status and /api/select: FCP metadata via _fcp_value - an ADF or a
  failed select (card with the active profile disabled) has no
  file_descriptor and used to crash the request handler; _get_file_type
  no longer raises either.
- esim._restore logs a failed selection restore instead of swallowing it.
- PWA: esimSwitchStatus shows the verified state / not-confirmed warning.
- tests: the switch flow (9000 / 91XX / error SW / chain failure), the
  FCP guards and the status helper; docs and sw simple-v230.
This commit is contained in:
2026-09-22 00:14:02 +03:00
parent 91c642a646
commit 86808ce55d
11 changed files with 338 additions and 87 deletions
+22 -9
View File
@@ -230,16 +230,29 @@ lock; `GET /api/status` reports `euicc` and `eid` for the PWA.
"operations": ["enable"], "address": "smdp.example.org",
"iccid": "8970…"}], "error": null}`.
- `POST /api/esim/profile` — `{"action": "enable"|"disable", "iccid"?: …,
"isdp_aid"?: …, "refresh"?: true}` (one identifier required). The card
usually answers with a REFRESH proactive command first (logged in
`/api/proactive-log`); after a successful switch — or whenever a REFRESH
was seen — the server re-initializes the card like an equip (reset,
re-read ICCID/network state, new `card_session`) and returns
`{"ok": true, "result": "ok", "refresh_seen": true, "reinitialized": true,
"iccid": …, "card_session": N}`. Failures carry the ES10c result code
(`iccidOrAidNotFound`, `profileNotInDisabledState`,
"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`.
`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`