91996a941ba0e85134e0e4b3c5ed7b1aef5e887a
55 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d20aac4aeb |
v3.25.7: the ENVELOPE response-bytes fetch
The live report: an SPI2 01 secured packet reported no PoR. For such a packet TS 31.115 4.2/4.4 (b6=0) has the Response Packet retrieved by the ME from the ENVELOPE exchange (SEND SHORT MESSAGE is the b6=1 case), but _send_envelope only asked for response bytes after a 61xx answer - the card signals them with 62xx/63xx instead (the "more data" quirk the remote-command SWs already acknowledge, _ram_remote_sw_ok), so the whole PoR stayed on the card and the late-PoR STATUS poll found nothing. _envelope_get_response (shared with _send_event_download, which had the same 61xx-only code): a 61xx names the GET RESPONSE length; a 62xx/63xx without data gets Le 256 (00c0000000 - a bare 61 no longer emits the odd-length '%02x' % 0x100 APDU). The result is adopted when bytes were delivered (or for a 61xx); otherwise the warning stays, so the v3.6.58 SEND-SM PoR path (the STATUS poll) keeps working. Tests: the 61xx length, the 63xx+PoR fetch (Le 256), the bare-61 encoding, the warning-kept path (the poll still runs) and the event-download sharing. 887 frontend + 880 Python; sw cache simple-v431. |
||
|
|
547ea61708 |
v3.25.2: the TR tag-form alignment with TS 102 223 6.8.0/6.8.1
The TR's response objects are all Min = N in the spec's own table, and 6.8.0 says such objects "should set the CR flag to comprehension not required" - so the builders now emit the base tag forms: - device identities 82 -> 02 (every builder) - PLI data 93/94/AD/AE/BF/E2/E5 -> 13/14/2D/2E/3F/62/65 (26/63 already base); the PWA inline codecs follow - POLL INTERVAL Duration 84 -> 04 - GET INKEY/INPUT Text string 8D -> 0D - SELECT ITEM Item identifier 90 -> 10 - Timer identifier/value A4/A5 -> 24/25 (_bip_tr already used the reference traces' 01/02/03.) The command details now echo the command's tag form (01/81) besides its qualifier - 6.8.1 requires the object to be identical to the command's "including the comprehension required flag": the parser returns cd_tag and _build_tr, _handle_timer_command, _handle_bip_command, the menu pendings and testscript.build_tr carry it. The Result keeps 03/83 (Min = Y). _tr_data_only strips both tag styles and the POLL Duration decode accepts 04. Tests: the base forms per builder + the echo + the PWA drift guards; 883 frontend + 866 Python; sw cache simple-v426. |
||
|
|
af00932aca |
v3.25.1: the run-ownership fix for the proactive auto-handling
A card that re-queues a command immediately after each accepted TR (live 2026-10-11: the EP BAP card re-issued the same PROVIDE LOCAL INFORMATION 1100 times) turned the background poll's FETCH/TR chain into an unbounded loop that held _CARD_LOCK forever - every card operation, a test run's start included, blocked behind it. The chain now yields: - _handle_proactive_chain aborts before the next FETCH while a test run owns the card (_TEST_RUNNING) - the pending command stays for the script - and takes max_rounds; the background poll passes _POLL_CHAIN_MAX = 10, so a chatty card cannot hold the lock. - _do_status_poll re-arms on its run skip (a heartbeat that never fetches); _timer_expired and the BIP Data-available event wait during a run; _bip_flush_channel_events stays quiet too. - _sim_resume_poll() resumes polling at every run end - the two worker bodies, their exception paths and the spawn failure. Also the TR command-details qualifier echo (TS 102 223 6.8.1: the object shall be identical to the command's): _build_tr, testscript.build_tr and the interactive _menu_send_response echoed a hardcoded 00 - harmless for the loop's qual-00 PLI, wrong for any other qualifier. The menu pendings now carry cmd_qual (parsed in the on_fetch handler). Tests: the chain bound/abort (a fake card re-queueing a command), the poll/timer guards, the resume wiring, the qualifier echo in both builders. 883 frontend + 865 Python; sw cache simple-v425. |
||
|
|
c9ab15c392 |
v3.23.7: the proactive log survives a TERMINAL PROFILE re-send
Pressing the Phone tab's Send (or Rescue) re-sends the profile and starts a new CAT session. The log was cleared with the rest of the session state, losing the only record of the card's proactive commands - a common tweak-the-profile-and-resend loop wiped the history every time. _resend_terminal_profile now calls _mark_proactive_session( 'terminal-profile') instead: the entries stay, _PROACTIVE_SESSION_START restarts (so the new session's elapsed values begin at 0) and a boundary marker entry is appended. The PWA renders the marker as a '- TERMINAL PROFILE re-sent - new CAT session -' divider (EN/RU). Equip / disconnect keep the full clear (a card change); the pending-command cancel, menu timeout and event list reset are unchanged. Tests: the marker keeps the entries and restarts the timer, the reset still clears, the re-send path pinned by source (Python) and the renderer's marker branch (Node). 857 frontend + 842 Python; sw cache simple-v420. |
||
|
|
e0dfc1032e |
v3.23.5: the TERMINAL RESPONSE result list and its spec corrections
The step editor's result select (the expect's respond, the proactive-drain's respond/first) offered five symbolic values with no hex. It now offers the full TS 102 223 v18.3.0 8.12.0 list (00-3F), labelled '<hex> - <alias>' (or '<hex> - <spec name>'), plus a manual hex entry, and an optional additional-information byte for the general result (83 02 <result> <info>, default 00; the 8.12.2 terminal-problem values are suggested through a datalist). The script aliases were corrected to the spec: refused 03->22, not_understood 04->32, modified 06->07, no_response 22->12 - a stored script using one of them now answers with the corrected value; no_response stays accepted for compatibility but is no longer offered by the editor. The server's TR decode tables (_RESULT_NAMES_BASIC/_GENERAL, a GSM 11.14 mix displayed in the proactive log) are replaced by the spec table plus the 8.12.2 additional-information names; the dcs field parses as hex (0A used to be refused and 10 read as decimal 10); the drain summary's 22 label is fixed. The result labels are not translated (spec text); the unused RU keys were removed. Shared fixture frontend/tests/tr_results.json, drift-guarded by both suites. 855 frontend + 838 Python; sw cache simple-v418. |
||
|
|
9ef1a59f2c |
v3.23.1: the PLI dictionary defaults and the PLI codec tag fixes
An unconfigured PLI dictionary answered result-ok with no data object,
which confuses applications that expect data. The dictionary now has a
base layer of sensible defaults for the qualifiers the pinned ETSI specs
define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23):
- location as the test network 001/01 + LAC 0000 (8.19),
- the classic test IMEI 490154203237518 as F-padded BCD (8.20) and its
IMEISV (8.74),
- language 'en' (8.45), Timing Advance (2E/AE), E-UTRAN (8.61; UTRAN +
E-UTRAN for the multiple-technology qualifier 0E),
- automatic search mode (8.75), a full battery (8.76).
An override set through /api/pli-dict wins, and clearing a field restores
its default (the PWA's Save re-renders the effective values; the GET returns
the override or the default for the full qualifier set). The date/time
qualifier stays empty so the host clock keeps answering it; the
3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices,
CAG, ...) stay empty - the ETSI specs define no contents for them, and a
fabricated object would mislead the application more than no answer.
Verifying the values against the pinned tag table exposed pre-existing bugs
in the PWA's PLI inline codecs, fixed here:
- Timing Advance used C6 (a 3GPP2 tag) -> 2E/AE;
- IMEISV used 94 (the IMEI tag) -> 62/E2;
- Network Search Mode used AD (the Language tag) -> 65/E5, and its labels
were swapped ('00' Manual / '01' Automatic per 8.75);
- the battery field was a percentage where 8.76 is an enum ('00' very low
... '04' full, 'FE'/'FF');
- encImeiPli padded the odd 15th digit with 0 instead of F (TS 124 008).
Docs: help EN/RU, api.md, the guide. Tests: the defaults + the resolution
order (Python), the HTTP endpoint's defaults/override/clearing, the codec
tags/labels and a server-defaults drift guard (Node). 846 frontend +
829 Python; sw cache simple-v414.
|
||
|
|
e12dfc7871 |
v3.22.0 (review fixes): the quota zero, the suite repair paths, the docs
Second review round on the unpushed v3.20.0-v3.22.0 batch: - install parameters: a zero memory quota (nv_quota: 0 - the vendor reference form's C7 02 0000 C8 02 0000) is composed instead of being dropped as "unset" (only None/blank leaves the TLV out); a truncated EF length in stk_params is refused instead of read as an empty EF. - test suites: a suite whose member script is gone is repairable - the update endpoint accepts pruning a dangling reference (a removal of a live script stays refused; the PWA's row Delete prunes it), and reconcile_scripts_to_suites() lists an orphan even when its suite_id write is refused by the validator, so a stored script that no longer validates stays visible in the editor and fails at run start with the message instead of disappearing. - engine/PWA: the proactive action refuses a check.data (its result data is the drained-command list - the check was silently ignored); the vestigial _testScriptDraft is gone; berLenStr emits the 2/3-byte BER long form (a >255-byte C9 composes correctly). - docs: api.md (the stop cleanup answers ok, never an automatic cancel; the server persists the counters - the PWA does not write them back; the suite bundle import always merges the scripts; the quota-0 note) and test-scripts.md (the event action covers all modelled events; the direct-create path needs a suite_id; the format history); help EN/RU counter wording. - tests: the zero quota / truncated EF, the prune path, the invalid-script listing, the proactive data-check refusal, berLenStr and the >255-byte C9. Tests: 839 frontend + 804 Python; sw cache simple-v403. |
||
|
|
b2cb42caf5 |
v3.22.0 (review fixes): the ownership reconciliation and the import guards
The suite/script ownership invariant could break silently: a suites import with mode "replace" wiped the suites while their scripts stayed behind (suite_id pointing at gone suites - invisible in the UI), a scripts import with mode "replace" left every suite's member list dangling, and a script whose suite_id disagreed with its listing suite stayed inconsistent. - server: reconcile_scripts_to_suites() repairs the invariant - a script a suite lists adopts that suite (the member list is authoritative), a script no suite lists joins "Imported scripts"; migrate_scripts_to_suites() delegates to it (the startup migration is the same pass) and the suites import runs it after loading, so a "replace" adopts the wiped suites' scripts instead of losing them. - the scripts import refuses mode "replace" while any suite still lists scripts (400 naming the dangling references; merge is the normal path). - nits from the review: a legacy numeric `event` is no longer parsed as hex in the step summary/error check (0x0A displayed as 0x10), and an integral float memory quota (a JSON client's 200.0) is accepted. - tests: the reconciliation (missing suite, listed-but-mismatched, idempotent), the scripts-replace guard (refused/allowed), the suites-replace adoption, the float quota, the numeric-event summary. Tests: 838 frontend + 800 Python; sw cache simple-v402. |
||
|
|
e6ced47323 |
v3.21.0: the test-runner tolerance batch + the proactive cleanup action
Five reported issues, plus the action that makes an explicit drain possible: - on_fail is honoured by every step failure: an expectation with no pending command (or a FETCH that returns nothing) fails as a Command/Fetch check at the step's level instead of raising past it; the default still stops the run, on_fail "warning" continues (the tolerant consume-if-pending form). - the run cleanup answers ok (0x00) and keeps draining while the TR answers 91XX (bounded); a cancel is only sent when a script asks for it - it tells the card the user aborted the session and pushes assertive applets onto an error path. - new `proactive` action: STATUS rounds consume every announced command (respond/first TERMINAL RESPONSE results, attempts/interval_ms bound) until the card answers 9000; an optional require asserts at least one drained command (an empty drain fails it), the final SW is the step's SW check and a still-pending command is handed to the next step. PWA editor + summary + validation, EN/RU help, docs, action-kind drift test. - the PoR response form follows the command's TAR: _tar_is_rm (preset role entries + the TS 101 220 Annex D ISD/RFM allocations) decides; a non-RM TAR's reply is always raw (TS 102 226 4) - no fabricated status word even when the bytes happen to parse (01 00 0B 91 ... used to read as SW 000B). - a secured data too short for the compact structure (a 2-byte applet reply) is recovered by _por_header_parse (the decode minus the compact parse; a malformed packet or a failed RC/CC/DS check still yields no por) and reported raw, so short applet replies are assertable via por.data. Docs: test-scripts.md, api.md, help EN/RU, repo AGENTS. sw cache simple-v396. Tests: 825 frontend + 755 Python green. |
||
|
|
82c5733906 |
v3.20.0 (fix): the install-parameter composition - one System Specific Parameters EF
Two reported SCP80/RAM issues, both confirmed against the specs:
- docs/api.md called `stk_params` a "Hex CA TLV", but TS 102 226 8.2.1.3.2.1
puts the CA *inside* the System Specific Parameters EF ('EF'), and the PWA
builds `EF <len> CA <len> ...` (the SIM access domain leads the CA value as
`01 <ad>` / `00`). A client following the doc sent a CA at the wrong level.
- the RAM composition appended the STK part after a caller-composed
C9+EF(quotas) prefix, producing TWO EF objects. The card read the first
(the quotas) and ignored the CA: the install was accepted (por_ok) but the
toolkit parameters were never registered, so no STK menu appeared (live
2026-10-05). Per GPD_SPE_013 v1.1 Table 6-5 (pySim's
gen_install_parameters) the quotas (C7/C8) and the CA share ONE EF; an
EA-form STK part is a sibling of EF (the reference TCA loader's
`C9 00 EF 00 EA ...`).
- server: `_compose_install_params()` (used by `_cap_install_apdu` ->
`_cap_apdu_sequence`, hence /api/ram-install, /api/ram-install-app,
/api/scp81/gen-install and the PWA's install script template) enforces the
rule: a caller-composed EF plus an EF-form `stk_params`, a bare CA, a
malformed EF and quotas next to a non-empty `install_params` are refused
(400 / `{"ok": false}` with the fix named); with an empty `install_params`
the quotas and an EF-form CA are composed into one EF
(`C900 EF{C7?,C8?,CA}`), an EA part stays a sibling.
- the documented `nv_quota`/`volatile_quota` fields are now real (they were
named in the docs but implemented nowhere): integers composing C8/C7.
- PWA: `composeRamInstallParams()` composes the same single EF itself (a
CA-form STK field takes the quotas into its EF, the merged EF travels in
`stk_params`), the RAM install bodies send it, the placeholder shows it
with a note when the quotas moved into the STK EF, and a refused request
(no steps) reports the error instead of "FAILED at step: undefined".
- docs/api.md: the corrected `stk_params` description with both accepted
shapes, the composition rule paragraph with the refusal cases, the fixed
example (`C90000` -> `C900`), and the /api/ram-install-app and
/api/scp81/gen-install cross-references.
- tests: composition unit tests (merge/refusals/EA-sibling) and the
ram-install-app 400 paths (Python); `ramQuotaTlvs`/`composeRamInstallParams`/
`ramInstallParamsCompose` pins incl. the CA merge (Node).
Tests: 824 frontend + 740 Python; sw cache simple-v395.
|
||
|
|
85cc4b9535 |
v3.20.0: the server-owned SCP80 counter (an untouched field = the preset's next value)
The counter an operation consumed was re-entered by the PWA on every send (the form field was filled from the preset and posted back), so a stale tab could send a value the card rejects (cntr_low), and typing in the field silently wrote the preset. Now the store is the single source of truth and the form field is a per-send override (Option B). - _preset_counter_seed: an explicit `cntr` in the request wins (a hand send, the counter probe or a pre-built packet may deliberately pick one); otherwise the preset store's value for the packet's keyset. Wired into /api/send-ota (the APDU path; the pre-built `sp` path keeps the caller's bookkeeping value - the packet carries its own counter), /api/ram-install, /api/ram-install-app, /api/cap-compat, /api/tar-probe and /api/sp-verify (the reference must use the counter the JS packet was built with). - the send-ota response echoes the effective `cntr` (a caller that omitted one learns what was used) and the OTA SEND log line prints it. - persistence is unchanged and monotonic (presets.set_counter refuses backwards writes), pinned by tests: an explicit lower counter is used for the packet but never lowers the store, a higher accepted one is saved. - PWA: the Secured packet counter field starts empty with the preset's next value as its placeholder (auto: <cntr>); typing affects that send only and is never written to the preset (spCntrSyncPreset is gone), spParams / getRamSpParams omit an empty counter, the RAM/send flows sync only the local preset copy (forward-only) instead of filling the field, the +/- buttons start from the effective value, and the preview/verify packet is built from the effective counter (genSp). Placeholder translated EN/RU. - the runner and /api/counter-probe keep their existing seeding (the probe already starts at the failed value and walks above it). - docs: api.md's optional-`cntr` contract + seeding notes on every endpoint, help EN/RU 3.1. Tests: 822 frontend + 734 Python; sw cache simple-v393. |
||
|
|
14c43f37b3 |
v3.19.1: the compact-PoR decode guard (applet TAR responses reported as data)
pySim's CompactRemoteResp was applied to every response packet, so a third-party applet's own TAR response parsed as count=128/SW=0110/data='' (live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status word and a lost response. _decode_por now takes the plaintext command script's length (cmd_len): the compact structure's command count cannot exceed it (TS 102 226 5.1.2 Table 5.1 - each command needs at least one byte), so a larger count is classified `response_type: 'raw'`, the secured data becomes `decoded.last_response_data` (a por.data check matches it) and no SW is decoded; `secured_data` is exposed in every case. Callers pass cmd_len where they know the plaintext: the test runner's scp80 step (None for a pre-built packet; carried in ctx['last_scp80'] for the submit-PoR path), /api/send-ota (both paths), the RAM step sender and the counter probe; a caller without it keeps the previous parse. Docs: test-scripts.md and api.md describe the raw fallback. Tests: 821 frontend + 729 Python; sw cache simple-v390. |
||
|
|
d1ea4fe450 |
v3.18.4: run-log review follow-ups (one ENVELOPE line per event step)
The review of v3.18.3 found the event steps logging twice: the shared sender writes its generic `ENVELOPE(Event Download): type=... data=...` / `ENVELOPE SW: ...` pair, and the runner's step-annotated line repeated the same data. `_send_event_download` gained `log=True`; the runner's envelope and event branches pass `log=False`, so an event step logs exactly one line - the annotated one, which already carries the step number, the event name, the effective source and the response SW. All other callers keep their output unchanged (netsim, the interactive event form, the BIP/timer paths). The review's test-pinning gaps are closed: - the shared sender's per-segment answer line (`OTA SEND: ENVELOPE i/N -> <SW>`) is asserted in the multi-segment test; - the scp80 step's report `sent` C-APDU/SP is asserted in the run-log test; - the single-ENVELOPE-line behaviour (and the source override) is asserted in the event-action test. Tests: 816 frontend + 718 Python; sw cache simple-v386. |
||
|
|
3c5882d16d |
v3.14.2: the counter probe's verdict rule
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber. Without a prior cntr_low nothing is saved, and a
missing PoR stays an error. actual_response_sms_submit/ussd count as
accepted.
Docs: docs/api.md, repo AGENTS. sw cache simple-v377.
Tests: 672 Python + 798 frontend green.
|
||
|
|
475c13aeb8 |
v3.14.1: the counter probe demands its PoR
_counter_probe_params validates SPI2, forces b1 (PoR required; a caller's
00 becomes 01) and defaults to the RAM listing transport 21; the no-PoR
error names the effective SPI2, and ramSyncCounter posts
ramListingSpi2('40') for the RAM view (the failed operation's transport).
Live 2026-10-02 (NP BAP): the probe had sent the plain SCP80 form's 00
(no PoR) - the card answered the ENVELOPE 9000 and sent nothing, so the
probe stopped after one packet with "card verdict: no PoR".
Docs: docs/api.md, repo AGENTS. sw cache simple-v376.
Tests: 670 Python + 798 frontend green.
|
||
|
|
c18b6cd0d4 |
v3.14.0: event form object-set fixes + the slices single-envelope mandate
An audit of every event form against the pinned specs (31.111 7.5.x/9.3, 102 223 8.87, TS 101 220's master tag table) found three missing objects, three malformed/wrong ones, and settled the slice-data scope. - 0x00 MT call: the IMS URI object is cut at 100 bytes and now carries the URI truncated object (8.135, F3 00) when the cut happened. - 0x1F Slices: the form gains the Partial NSSAI object (8.154, F9, rel 18); a single envelope carrying it is marked as the Last Envelope (8.79, F0 00) automatically (7.5.27.2). - 0x06 Card reader status and 0x07 Language: the builders now emit their object tags (A0 01 ..., AD ...) - they emitted bare values before. - 0x18 Incoming IMS data: the IARI tag is F6 (76/F6), not F7 (which is the IMPU list tag - the two-column 9.3 table had been misread). - Single-envelope mandate: the event data is capped at 244 bytes (inner 252; the D6 long-form length and the APDU Lc stay in one byte). The server refuses more with a 400 and the PWA shows the size error before sending. Chained (multi-envelope) delivery, the More Data object and envelope sequences are not implemented - documented in the help and the repo AGENTS. - Tests: event form vectors (URI truncation, the restored tags, partial / Last Envelope, the size helper) and the server guard; the release reference and its PWA/code mirrors gain the three objects and the corrected IARI key. - sw cache simple-v375. |
||
|
|
355bcadb59 |
v3.10.0 review fixes: repair /api/ram-install-app, escape quotes in esc
- /api/ram-install-app referenced undefined spi1/tar (the resolution block
was only added to /api/ram-install and /api/cap-compat), so every request
answered 500. The same preset/MSL resolution is now in place, with
regression tests for both modes (install / make selectable) and for the
no-MSL refusal before any card I/O.
- esc() now escapes " and ' too: the TAR editor renders free-text
descriptions (and other values) into attribute contexts, so a crafted
preset import could inject attributes. Three tests asserting the old
unescaped output updated; a dedicated esc test added.
- _spi1_for_tar names the no-TAR case in its hint (a pre-built packet
without tar/spi1 used to report "TAR ?").
- preset free TAR rows no longer store an empty "role" key (the documented
shape); a non-list `tars` in an update is refused (400) instead of being
silently ignored.
Live: the no-MSL request answers 400 with the hint; a make_selectable with
a bogus AID runs the format probe and the INSTALL (derived SPI1 16 from the
preset MSL), the card answers remote SW 6A88 and the counter advances - no
500. Suites: 658 Python + 775 frontend.
No version bump: completes the unreleased v3.10.0 (
|
||
|
|
6360dc5bae |
v3.10.0: preset TAR table with per-TAR MSL; derive the SPI1 from the TAR
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar fields: a preset now carries `tars` - the three mandatory role entries (isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an optional description, each with its MSL (Minimum SPI1, TS 102 226 8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all three MSLs); duplicate TARs and empty TAR/MSL values are refused; the store schema version is 3. Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the RAM ops, the test runner (per-step overrides still win), the counter probe and each TAR-probe row; the plain send keeps an explicit SPI1 with a `warning` when it is below the MSL, and a TAR outside the preset without an explicit SPI1 is refused with a hint. The TAR probe resolves per checked TAR before sending and reports the effective spi1 per row. The secured packet form's SPI2 default is 00 (the RAM/probe operations pin their own). PWA: the Cards form edits the TAR table (role rows with MSL inputs plus Add/Remove free rows with a description), the list shows a TARs (MSL) column, the SCP80 TAR field suggests the preset's entries (datalist) and a preset apply seeds the SPI1 from the operation TAR's MSL with an amber warning below it; the preset-wide SPI1/SPI2 fields and the header marker's SPI requirement are gone (completeness = a keyset + the three role entries with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache simple-v358. Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter advanced); a send without spi1 derived 16 (por_ok, counter advanced); a send with spi1 0A returned the warning and the card refused it (6200, no counter consumed); an unknown TAR without spi1 was refused with the hint. |
||
|
|
391d4a322d |
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. |
||
|
|
ad2a253f2a |
PLI date/time responses default to the host clock
Live finding (Network rejection events): the card asks the terminal for the current date and time (PROVIDE LOCAL INFORMATION, qualifier 03) and our TR answered "performed successfully" with no data. TS 102 223 6.4.15 says the terminal shall return the current date and time, and the 6.8.0 TR structure requires the Date-Time and Time zone object (8.39) for that qualifier. - _pli_data_hex(qual): the TR Config dictionary entry wins; when it is empty the date/time qualifier is answered from the host clock via _encode_scts (the SMS timestamp builder, TP-SCTS coding). - _build_tr and _DefaultProactiveHandler put the command-specific data objects after the Result TLV per the 6.8.0 order (the POLL INTERVAL Duration echo too; real terminal responses pin the order: 8103010300 02028281 030100 0402011E). - _decode_tr decodes qualifier 03 with the same 8.39 coding (swapped semi-octet BCD digits, time-zone sign bit 0x08, 'FF' = unknown); it read plain decimal bytes and a 0x80 sign before. - The PWA PLI dictionary codec for the date/time entry writes the same coding (it wrote plain hex bytes and a 0x80 sign), and the TR Config panel notes the empty-entry default. sw cache simple-v351. Tests: builder vectors (east/west/unknown zone) + dictionary override + the empty non-date/time qualifier + the object order (PLI and POLL INTERVAL) + the handler path + the TR decode + a frontend codec round-trip test. |
||
|
|
a26a2b6648 |
feat: the remaining six event types get their parameter forms
MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN access status and CSG cell selection no longer fall back to the generic "send a bare notification" prompt: - MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number + TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv), subaddress, IMS URI, media type; source network. - Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction identifier, near/far-end source select, media type, and a cause - absent, the zero-length radio-link-timeout form or custom bytes (the hex field shows only for "custom"). - Browser termination (TS 102 223 7.5.9.2): user / error cause. - (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage / connection on-going. - CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism, optional access technology, CSG ID, HNB name and PLMN ID; source network. The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and _send_event_download()/`/api/event-send` take an `event_src` override (an invalid value falls back to the terminal). Tests: the BCD/Address builders, each event's build vectors, the "no not_yet left" guard and the server source override. Help EN/RU, both AGENTS files; sw cache simple-v348. |
||
|
|
cafec549fa |
feat: the Data Connection Status Change event gets its parameter form (0x1D)
EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null - the ENVELOPE carried only the event list and device identities, none of the event's data objects. The form now builds the TS 31.111 7.5.25.2 object set (tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional location information (13, CR flag 0 as the spec requires for this command), optional access technology (BF), location status (9B), optional APN (C7) and PDP/PDN/PDU type (0B). Adjacent spec fixes found while verifying: - the Access Technology Change options (0x0B) did not match the pinned TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA ...); they now use the shared ACCESSTECH list, whose two labels were also corrected (TIA/EIA-553-A, TIA/EIA-136-270); - _send_event_download() wrote the D6 length as a single byte - a long APN would overflow it; 128..255 bytes now use the BER long form (81 <len>). Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list, the long-form D6. Help EN/RU and both AGENTS files; sw cache simple-v342. |
||
|
|
9f7876b5fa |
docs: help/README catch up with v3.8.0; keyset-number wording; RU "пресет"
Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
Keyset selector next to the preset selector (and that a successful PoR
advances the counter only when SPI1 carries a counter check); §4 describes
the keyset editor rows, the derived number and the per-keyset counters; the
probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
"TAR-сканер" section (standard list, verdicts, counter persistence); the
Cards section is rewritten for the server-side store - keysets, server-owned
per-keyset counters with the audit trail, JSON export/import as the migration
path, the keyset editor, the equipped-card mismatch banner - and the field
table, "Add a card" and "Edit / remove" follow.
Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
the messages say "keyset N ...". The TS 102 225 A.1/A.2 quotes and the GP
PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
"не является keyset пресета", ...) and uses "пресет" instead of
"предустановка" throughout the dictionary, the help and README_RUS.
sw cache simple-v340. Tests: 742 Node / 619 Python.
|
||
|
|
6eeb8dacb3 |
feat: TAR probe + the SPI1 counter-tracking rule; keyless packets build again (v3.8.0)
Three things learned from probing a live card, plus the capability they enabled:
1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
is "present, ignored, never updated", so a packet without a counter check
must leave every counter untouched. _counter_tracked() now guards
/api/send-ota (no final_cntr, no persistence, a log line), the RAM step
sender and the test runner; _preset_counter_persist() requires a non-zero
key version (the "single keyset" fallback could advance a keyset spuriously
for a keyless send). The PWA advances the form counter only when the server
reports final_cntr and says "counter not tracked" otherwise.
2. The spec's no-security form builds again: _ota_keyset() refused the valid
KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
SPI1.b2b1=00 for KID) because pySim wants an algorithm. An unknown nibble
is now refused only when the algorithm is actually used (ciphering / a
ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
the card accepted (a fake 15/15 was refused with 6200). A no-security PoR
is decodable again too, and a requested-but-missing PoR sets por_missing
on the plain path as well.
3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
(TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
with the preset's keyset and SPI 16/01. Each TAR is classified
(registered / no_answer / no_por / refused) from the ENVELOPE verdict and
the application's PoR, counters advance and persist normally, and the run is
refused when SPI1 has no counter check. Live on a card: 000000, B00000 and
B00001 registered (compact-format applications, answering 6D00 / 6B00),
the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
with the keyless form the same card accepted every TAR and answered no PoR.
Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337. Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
|
||
|
|
162930eb96 |
feat: low-counter guard for every SCP80/RAM operation (v3.6.58)
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.
Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
(fixed-width wrap like the counter field); carried by /api/send-ota (plus
an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
answer (62xx/63xx - the live card answers 6200 for a low counter) and
_send_secured_packet passes the captured PoR on, so the verdict is named
instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
later SPI2=0x21 query in the live log).
PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
a checked sendOta(), so the paginate/expanded loops stop, no further
packet is sent and no counter is written back (delete and Send to Card
check it too);
- the result states the card's counter and offers 'Go to preset'
(ramGoToPreset: opens the failed operation's preset in the Cards tab for
editing and focuses the counter field) - nothing is written
automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.
sw cache simple-v331; tests: 557 Python / 714 Node.
|
||
|
|
81a7aace15 |
fix: BER-safe proactive TLV walks + hardened FETCH loop (v3.6.57)
The live 500 in the Explore ('OTA send error: index out of range') was a
parser bug, reproduced with the live FETCH shape: _parse_proactive_header
walked the command TLVs reading the length as a single byte, so the card's
SEND SHORT MESSAGE with its 150-byte SMS TPDU (8B 81 96) was mis-walked
into the TPDU and eventually indexed past the end (val[2] on a short
slice) - the FETCH was left unanswered and the request answered 500.
- _ber_tlv_list/_proactive_body helpers: BER lengths (_ber_len_at) plus a
bounds check that stops at a truncated TLV instead of walking into the
value; _parse_proactive_header, _parse_display_text, _parse_select_item,
_parse_setup_menu_items, _tlv_map and _decode_poll_negotiation all use
them now (the same latent bug for any long-form TLV);
- _run_proactive_chain guards every step (header parse, proactive log,
on_fetch, BIP/timer handlers): an exception is logged with the offending
FETCH bytes and the TERMINAL RESPONSE is still sent - a decoder bug can
never leave a FETCH unanswered again;
- the expanded-registry report distinguishes a request error from a remote
SW: 'ELFs: request error (index out of range) - compact listing used'.
Regression tests: the live long-form FETCH shape parses (header + TLV map +
TPDU), truncated TLVs stop the walk, and a raising parser still gets a TR.
sw cache simple-v330; tests: 553 Python / 711 Node.
|
||
|
|
599d84fd66 |
feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle> <privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked against GlobalPlatformPro's populate_legacy (the 4th byte is privileges; 0x00 is required for load files). The parser read it as a 'version' one byte late, so ELF rows showed the module count / the next entry's length as Version: 00/10 - the compact listing has no version at all; it now comes from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table 11-37 Note 1) and merges (the bogus compact value no longer blocked it). New data exposed: - EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled) - unknown registry objects are preserved (rawTlvs) and shown as 'Other registry data: 8F=752B . 90=012D ...' instead of being dropped - 84 parses the GP coding (one Executable Module AID per TLV, repeated; the concatenated 4F<len><aid> form stays as fallback) and the ELF expanded queries add 84 to the tag list (with a tag list the card returns only the listed tags - the fallback path would otherwise lose the module lists and their Install buttons) - life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card, app, SD - detected via the Security Domain privilege - and ELF), with multi-byte 9F70 (02 0700 on live cards) decoding the first byte and surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were aligned with the same tables /api/ram-install measures the NV footprint: a silent GET DATA FF21 read before and after the chain (also on failure) returns nv_before/nv_after/ nv_delta; the reads never fail the operation (no FF21 -> no fields) and the PWA shows 'NV used: ~N B (free A -> B)' in the install result. sw cache simple-v324; tests: 548 Python / 698 Node. |
||
|
|
904eca1758 |
fix: accept comprehension-required tag variants everywhere (v3.6.40)
The TERMINAL PROFILE chain parsed the SET UP MENU title with 'tag == 0x05' only, so the live Alfa card's title (CR-set 0x85, 'Alfa Mobile') was dropped: /api/menu returned items without a title and the PWA showed 'No menu set by the card' - card-specific only because cards pick the tag variant (this one sends plain 0x8F items and a CR-set title in the same command). - _parse_setup_menu_command (pure, tested with the live FETCH bytes) replaces the inline parse; the PWA shows a title-less menu as 'STK: Menu' instead of 'No menu set by the card' (stkMenuLabel, pure). - _cmd_tlv is now the single lookup helper: exact tag, then tag ^ 0x80 (bit 8 is the CR flag, TS 101 220 7.1.1); the old '| 0x80'-only helper and the duplicate _cmd_tlv_any are gone. - Remaining single-style checks fixed: _parse_setup_menu_items (0x8F/0x0F), _parse_select_item title (0x85/0x05), _find_sms_tpdu (0x8B/0x0B - the plain variant lost the SEND SHORT MESSAGE PoR TPDU), the response scripting parsers (R-APDU 0x23/0xA3, executed count 0x80/0x00) and the FF21 resource tags (0x81-0x83/0x01-0x03). - tests/test_tag_variants.py flips the CR flag on every top-level tag of real command fixtures and asserts identical parse results for all parsers (verified to fail when a single-style lookup is reintroduced). sw cache simple-v313; tests: 524 Python / 675 Node. |
||
|
|
0027c405c1 |
feat: answer GET INKEY / GET INPUT interactively (v3.6.38)
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU, so a GET INPUT was auto-answered with a bare success TR and no Text string - the live RemMobileID PIN prompt got an empty answer. Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help), Response length 91 and Default Text 9D; the fetch handler pauses with them; _menu_send_response(..., text=) validates the answer (digits set, min/max, one char for GET INKEY) and builds the Text string TLV 8D in the requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7), Yes/No as 01/00, empty input as the null text string, help as result 13 and the variable-timeout Duration echoed (6.8.4). /api/menu-respond takes text; /api/stk-status carries pending_data so a card-initiated prompt opens the panel; _decode_cmd decodes both commands. PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained panel (password field for hidden entry, numeric keyboard, default text, Yes/No buttons, help, immediate-digit auto-submit); OK submits through stkMenuOk; the poll opens the panel for card-initiated prompts. sw cache simple-v311; tests: 517 Python / 674 Node. |
||
|
|
ddf5cbb8e7 |
fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)
_send_envelope defined its SEND SHORT MESSAGE capture handler inside the 91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a packet whose ENVELOPE answered 9000 and whose late PoR arrived on the STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM operation mid-packet with the card's command left pending (live 2026-09-29, 120-byte LOAD blocks). The handler is now defined for every path, and the late-PoR poll runs only after a packet's last segment (poll_status from _send_secured_packet) - a concatenated download makes no spurious mid-packet STATUS exchanges. The PWA annotates every LOAD step with the CAP components its block covers (ramStepComponents: 'completes Import' / 'inside Method' / 'Header...Import') and ramInstallFailHint names the component the failing block completed - the live import-verification rejection lands in the block completing Import (6438/6985 at both 240 B and 64 B blocks). sw cache simple-v310; tests: 509 Python / 672 Node. |
||
|
|
0b7b6b8157 |
feat: RAM operations run behind a blocking modal (v3.6.34)
A CAP install is a long chain of LOAD commands whose progress and results were only visible at the bottom of a large form - and an interrupted install can leave a half-loaded package. - Server: _RAM_PROGRESS (+ _ram_progress_begin/step/end) records the live step of the running RAM operation; /api/ram-install and /api/ram-install-app wrap their chains in it (total = steps + 1 for the format check) and /api/status carries it as ram_progress - lock-free, so the 2 s poll works while the request holds _CARD_LOCK. - PWA: #ram-op-modal (the STK panel pattern) blocks everything while a RAM operation runs: title, spinner, status line, progress bar, per-step result lines, Close disabled until it finishes; a beforeunload guard protects the window during the run. install-cap, install-app / make-selectable and Explore all use the shared helpers (ramOpModalBegin/Status/Finish); the install results stay in the form area as before once the modal is closed. The status poll feeds the modal via ramOpProgress (headline from the pure ramOpProgressText). - Tests: ramOpProgressText (JS) + the progress lifecycle (Python); the card-state and capmem suites stub the DOM-bound hooks. - Stylesheet rebuilt; version 3.6.34; sw simple-v307. |
||
|
|
cf931da2f9 |
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. |
||
|
|
31162efc8c |
feat: log the plaintext RAM APDUs of the install steps (v3.6.19)
`_ram_send_gp_apdu` (the shared step sender of /api/ram-install and /api/ram-install-app) now logs every step's plaintext APDU and packed packet, like /api/send-ota's unconditional lines: RAM C-APDU (INSTALL [for load]): 80E602000C07F0414C4641610100000000 RAM SECURED-PACKET (INSTALL [for load]): 00281516212525000000... No flag - the step name keeps the sequence readable, and --apdu-trace stays the card-level (ENVELOPE) trace. Tests: the logging pinned with stubbed packet builders. 498 Python / 650 frontend green; version 3.6.19; sw simple-v292. |
||
|
|
face2268ca |
feat: single-APDU INSTALL [for install] / [for make selectable] ops (v3.6.18)
Iterating the final install step no longer needs a delete + re-load of the .cap: - `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`, P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable] (`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already loaded package. The APDU builders are shared with `_cap_apdu_sequence` (`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift) and the per-step send/PoR/counter logic is the extracted shared `_ram_send_gp_apdu` (used by /api/ram-install too). - PWA: the RAM op selector gains "INSTALL [for install] (loaded package)" and "INSTALL [for make selectable]" with Load File / Module / Instance AID inputs (instance defaults to the module AID); the result renders through the shared `ramShowInstallResult` and `final_cntr` keeps the preset in sync. - Explore: an **Install** button on a package's module AIDs that have no instance yet, and a **Make selectable** button on INSTALLED (0x03) instances - both switch the op and pre-fill the AIDs. - tests: the install APDU pinned to the live no-STK packet and to the sequence tail, the make-selectable APDU, ramHasInstance, the Explore buttons and the op wiring; docs/api.md updated. 650 frontend / 498 Python green; version 3.6.18; sw simple-v291. |
||
|
|
29864617eb |
fix: CAP install APDUs match the reference terminal form (v3.6.8)
INSTALL [for load]/LOAD/INSTALL [for install] carried a trailing Le and an explicit ISD AID in the Security Domain field; the card executed the extra byte as a second (phantom) command, so the compact response reported count=2 with SW 6700 - which the v3.6.2 remote-SW check correctly treated as a failure, aborting at step 1. Decrypted from the live trace (KIc/KID 25/25, 3DES): ours was 80E6020014 07F0414C46416101 08A000000003000000 00 00 00 00 the reference tool sends 80E602000C 07<aid> 00 00 00 00 (empty SD, no Le) and its response is count=1 / 9000 while ours was count=2 / 6700. - `_cap_apdu_sequence`: the SD AID is only sent when a custom one was supplied (empty -> '00', the card defaults to the ISD; GP 11.5.2.3.1 Table 11-42) and all three RAM install APDUs are case 3 (no Le). - tests: the expected INSTALL bytes updated, a no-Le assertion for every APDU in the sequence, and a custom-SD-AID case. 615 frontend / 496 Python green; version 3.6.8; sw simple-v281. |
||
|
|
40f20d539e |
fix: decode the Response Scripting template and the compact listings (v3.6.5)
Explore still missed the F0414C46416101 package on a card whose responses wrap the R-APDU in the TS 102 226 5.2.2 Response Scripting template (`AB <len> 80 <count> 23 <len> <R-APDU>`): `_decode_por` parsed that as a compact response, so the frontend got `last_status_word` 81d0/7680 and data starting `80 01 01 23 ...` instead of the listing. - server: `_parse_response_scripting()` (AB definite / AF 80 ... 00 00 indefinite) extracts the executed-command count and the last R-APDU's SW and data; `_decode_por` exposes it as `response_type: scripting` in the same `decoded` shape as compact, so the RAM explore paging and the RAM install `por_sw` see the real 9000/6310. - frontend: the compact listing walk is deterministic on the AID length (`len AID life ver`; P1=10 adds `module_count (len module_AID)*`). `_parseRawAppEntry` no longer guesses `rawLen-1` for >8-byte AIDs (16-byte A113 applet AIDs were truncated), and `_parseRawElfEntry` no longer scans for `0x10` (a length/AID byte equal to 0x10 derailed the walk: a live page parsed to 1 entry with no F0414C46416101). - tests: exact trace fixtures - the ELF page lists F0414C46416101 (11 entries), the P1=10 page attaches its F0414C4641610101 module, the app page keeps the 16-byte A113 AIDs; Python covers the scripting template (definite/indefinite) against the live `AB 12` ISD and listing vectors. 610 frontend / 495 Python green; version 3.6.5; sw simple-v278. |
||
|
|
9e85f522f6 |
fix: recover the SMS-SUBMIT listings in RAM Explore (v3.6.4)
Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low. Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:
- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
`8B 81 97 ...` (BER long form) for the big pages, so it returned a
corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
read a bogus UDH and reported no concatenation, so the segments never
assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
The step was marked failed, the counter did not advance, the same counter
was retried and the card answered `cntr_low`, which also blocked P1=10
(modules) where the installed package appears.
- the captured segments now always store the assembled UD (`submit_ud_hex`);
`_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
`_decode_por`. Verified against the live capture: por_ok, remote SW 6310,
438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
the counter advances when the data follows via SMS-SUBMIT.
Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).
Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.
608 frontend / 492 Python green; version 3.6.4; sw simple-v277.
|
||
|
|
39c82f26f3 |
fix: SCP80/RAM re-read the preset before every operation; no counter bump on a rejected send (v3.6.3)
The SCP80/RAM forms hold a copy of the preset (counter, keys, TAR, SPI). Editing the preset on the Cards tab saved correctly, but the form kept the old copy: the operation sent the stale counter (the card answers cntr_low) and the post-send sync wrote the stale value back over the preset - the saved counter silently reverted. Reproduced in a real DOM: after select in SCP80: preset=0000000001 sp=0000000001 after Cards edit+save: preset=00000000AA sp=0000000001 after a sync: preset=0000000001 (edit lost) - `cardsApply()` split into `cardsApplyFields()` (field copy, no packet) and `cardsApply()` = fields + genSp; new `spRefreshFromPreset(selId)` re-reads the selected preset from `sp-card-sel` / `ram-card-sel` and re-applies it. - `pysimSendOta()` and `ramExecute()` call it before starting, so every SCP80/RAM operation uses the preset as it is now. - A rejected send no longer advances the counter: new `spPorAccepted(por)` gates the advance+write-back in `pysimSendOta`, the Explore pagination and its GET DATA step, and the server's RAM install (`_ram_next_cntr`: advance only for `por_ok`/`no_por` steps). A failed install still returns `final_cntr` (the accepted prefix) and the PWA persists it, so a retry never replays a counter the card already consumed. - tests: cards_counter.test.js (the stale-form regression, RAM selector, fields-without-genSp, spPorAccepted) and `_ram_next_cntr` cases; the cards_form/ram harnesses updated for the split. - docs/api.md counter semantics; AGENTS preset-source-of-truth rule. 607 frontend / 488 Python green; version 3.6.3; sw simple-v276. |
||
|
|
36d2f71bc7 |
fix: report the real PoR/remote-SW result of every RAM install step (v3.6.2)
The RAM installer read `por['decoded']['response_status']`, but the compact
response decoder's `decoded` only carries {number_of_commands,
last_status_word, last_response_data} - so the suffix was always empty and
every step showed the useless `por_error_`, even when the PoR was por_ok.
The PoR verdict is the top-level `response_status`.
With that fixed, the decoded details also show that the remote command's own
status word was the real verdict all along: a captured live install returned
por_ok with `last_status_word` 6700/6F00 (the card rejected every RAM APDU)
while the installer reported success.
- new `_ram_step_result()`/`_por_remote_sw()`/`_ram_remote_sw_ok()`: a step
fails on a non-por_ok PoR, on a remote SW outside the success set (9000,
61xx more data, 62xx/63xx warnings, CAFE GP "more data"), or on an
undecodable PoR (a 9000 transport SW with no PoR at all is `no_por`, not a
failure). A decoded 61xx is explicitly success, per GP/ISO.
- step records gain por_sw/por_type/por_cntr/por_data/por_raw and
`por_error`; the response carries `failed_step` and a detailed error such
as `LOAD (1/9): remote SW 6700`; the log line now prints
`status=por_ok remote_sw=6700`.
- PWA: new pure `ramStepLine()` renders e.g.
`❌ Шаг 2: LOAD (1/9) — PoR ok · remote SW 6700 (Wrong length in Lc) · 274 B / 3 SMS`
(SW meaning via the existing lookupSw decoder) and the transport SW only
when it is not 9000.
- tests: tests/test_ota_helpers.py RamPorStepTest (success set incl. 61xx,
the captured 6700 failure, no-PoR/undecodable, expanded responses);
ram.test.js ramStepLine cases.
- docs/api.md RAM install step fields + failure semantics; AGENTS updated.
603 frontend / 487 Python green; version 3.6.2; sw simple-v275.
|
||
|
|
3a4a098f1a |
feat: test script engine and server-side runner (phase 1)
A test script drives a deterministic dialogue with the card: action steps (ENVELOPE event / Menu Selection, raw APDU, SCP80 secured packet with a card-preset, file update/read, STATUS) with SW/data/PoR checks, and proactive-command expectations that fetch, check (command type, qualifier, text/item/raw) and answer with a scripted TERMINAL RESPONSE. - pysim_simple_server/testscript.py: pure engine (validation, exact/mask matchers with '?' nibble wildcards, item/text checks, TERMINAL RESPONSE building). - server.py: worker thread + state, /api/test/run|status|stop|clear, the card-endpoint guard (409 while running), background STATUS polling suspended, 'error terminates / warning continues', a pending command is drained with a cancel TR on stop/error, no-drain modes for the ENVELOPE and SCP80 senders (the pending command belongs to the next expectation). - Expectations never poll: a command must be pending (91XX) from the previous step, otherwise it is an error (TS 102 221 7.4.2.1 / TS 102 223 6.3); scripts add an explicit `status` action (attempts/interval) when the card delivers on poll. - SCP80 steps require a complete card preset, may override TAR/SPI1/SPI2 only, and the counter is advanced per send and reported (`scp80_counter`) for the PWA to write back. - tests/test_testscript.py (26 tests: engine, matchers, TR building, the STK menu dialogue, error/warning termination, status polling, unexpected-command drain, SCP80 preset/counter, file actions, guards). - docs/api.md endpoint reference. 467 Python / 573 frontend green. |
||
|
|
337e5df770 |
feat: decode the Items Next Action Indicator in proactive commands (v3.5.3)
SET UP MENU / SELECT ITEM items carry an optional Items Next Action Indicator (TS 102 223 8.24, tag '18'): one byte per item, in list order, coded with the Table 9.4 values marked "Used for Next Action Indicator". The decoder ignored it, so neither the proactive log nor the STK menu showed what the card would do when an item is selected. - server: NAI_TYPES whitelist (the ToC-only values - e.g. '26', '27', '47' - are reserved there and are ignored, as are '00' and unlisted values), _nai_name(), _attach_nai() (a short NAI list leaves the tail without an indicator, extra bytes are ignored); _parse_select_item() and _parse_setup_menu_items() plus the TERMINAL PROFILE SET UP MENU walk attach nai/nai_name to the items; _decode_cmd() renders '1. Menu -> SET UP MENU'. - PWA: stkMenuNaiSuffix() adds a small gray '<name>' suffix to STK menu items that carry an NAI; the proactive log picks the decode up via cmd_decoded. - tests: SET UP MENU / SELECT ITEM vectors with NAIs, a reserved value that is ignored, a short NAI list, and the stkMenuNaiSuffix unit test. - docs: UICC_SPECS 6.5 gained the '18' Items next action indicator row (8.24/9.4); help EN/RU mention the menu suffix; AGENTS files updated. 548 frontend / 413 Python green; version 3.5.3; sw cache simple-v255. |
||
|
|
73ff6a60bd |
fix: SCP80 RC/CPL parity, file-life-cycle labels, EF.ARR and PS template decoders (v3.5.1)
Three fixes found while filling the ETSI/3GPP registry gaps against the SIMalliance Stepping Stones R7: - SCP80 builder/verification parity (TS 31.115 Table 1 NOTE / 4.2 / 4.3): the CPL is now transmitted whenever the packet is ciphered or carries RC/CC/DS - it is part of their input - and whenever the packet needs SMS concatenation; a single unprotected SM keeps pySim's CHL-first form. Before, the JS dropped the CPL for every unciphered packet while the server reference re-added it, so "Verify vs pySim" reported a false MISMATCH for every unciphered RC/CC packet (SPI1 01/02/0A/12/1A...). All ten offered SPI1 values now match the server reference byte-for-byte. - RC (SPI1 b2b1 = 01) was offered but not built: the JS now computes CRC-32 (TS 102 225 5.1.3.2, pySim zlib.crc32 parity) over the same CPL frame as the CC; the packet no longer silently omits the 4-byte RC field. - Server: _build_secured_packet/_ota_reference add the CPL to a concatenated unprotected packet too (Table 1 NOTE / 4.3). - fcpLifeCycle: unlisted values with b8 clear are RFU, b8 set is proprietary (Table 11.7b); previously all unmatched values were labelled proprietary. - EF.ARR decoder now decodes the expanded format (AM_DO/SC_DO per ISO 7816-4 5.4.3.2 + TS 102 221 9.2.7): operation bit masks, INCREASE/RESIZE AM_DO 0x84, OR/AND/NOT templates, PIN key references with usage qualifiers. - FCP 'C6' PS template DO decoded (PS_DO bitmap + key references + usage qualifiers, TS 102 221 11.1.1.4.10/9.5.2) with a shared key-reference map. Tests: sp.test.js (CPL/RC vectors + crc32 known answer), ef_decode.test.js (expanded-format ARR vectors), profiler.test.js (LCSI RFU/proprietary, C6), test_ota_helpers.py (RC reference, unprotected single-SM vs concatenated). Help EN/RU and READMEs: CPL size 2 (SMS), RC/CC/DS 4-8, RC bullet, CPL rule. 547 frontend / 397 Python green; version 3.5.1; sw cache simple-v253. |
||
|
|
43f50d3b72 |
feat: SCP80 SMS concatenation + packet size / SMS count display (v3.2.0)
Packets longer than one SMS now go out as concatenated SMS-PP downloads per TS 31.115 4.3 and the UI shows how many SMS a packet needs. Server: - `_split_secured_packet` cuts the command packet at the exact SMS user-data capacities (first SM 132 octets: concat IE 5 + CPI IE 2; following ones 134; a single-SM packet may be 137 with the CPI IE) and `_build_sms_tpdu` tags every segment with the fixed concatenation reference 01; `_build_sms_tpdu` also enforces the 140-octet budget. - `_send_secured_packet` (shared by /api/send-ota and /api/ram-install) sends one ENVELOPE per segment in order, refuses more than MAX_ENVELOPE_SEGMENTS (5, the card's concatenation buffer) and reports `bytes`/`segments` in the response (RAM install per step as well). - `_build_secured_packet`/`_encode_cmd_unlimited`: our own TS 102 225 5.1.1 encoder on pySim's keyset/header constructors, byte-identical to pySim for packets <= 140 octets (tests) and not limited to one SMS (pySim refuses the longer ones, which is why they never went out). - RAM LOAD blocks are no longer clamped to one SMS: 1-240 bytes of payload with the default 240 (the GP maximum); `load_block_size_auto` replaces `load_block_size_clamped`. PWA: - `scp80SegmentInfo` / `spSizeInfoText` show "N bytes . M SMS (concatenated)" under the packet field, turn red past 5 SMS and report size/SMS in the send result and the RAM step log; LOAD block hints and placeholders updated; EN/RU. Tests/docs: Python +2 cases incl. segment order/capacities and byte identity with pySim; Node drift guard against the server constants and UI text tests; README/README_RUS, help EN/RU, docs/api.md, AGENTS. |
||
|
|
ac18c3e2cf |
rename OTAMan → SIMple (v2.3.0)
- package pysim_otaman_server → pysim_simple_server; distribution and console script pysim-otaman-server → pysim-simple-server - PWA branding SIMple: title, header, manifest, help EN/RU, GitHub links - localStorage keys otaman_* → simple_* (clean break, no migration) - export filenames simple-cards.json / simple-custom-files.json - service worker cache simple-v189 - README/docs/setup/start scripts updated; no 'otaman' left in the tree |
||
|
|
aa33d25466 |
terminal profile at runtime + canonical custom-file paths (v2.2.3)
TERMINAL PROFILE:
- GET /api/terminal-profile returns the profile in effect + the CLI default;
POST /api/terminal-profile validates ({profile}, hex, even, 1-255 bytes),
stores it in memory and re-sends it, resetting the STK session like
/api/rescue (the shared _resend_terminal_profile helper; rescue now
delegates to it). __main__ keeps server.cli_terminal_profile.
- Phone tab: a TERMINAL PROFILE block next to STATUS and Polling with the
current hex/byte count, Send (re-send) and Configure. The Configure
dialog has a device-model preset selector, a hex field and a per-bit
form generated from a 264-entry table for TS 102 223 V18.3.0 5.2 bytes
1-33 (pySim's table as scaffold, later bytes/3GPP bits added from the
spec; beyond the table generic RFU labels). Form <-> hex sync both
ways, hex authoritative, bits preserved. Apply posts the new value.
- Presets: Xiaomi Mi A1 (project default), Quectel GSM module example.
In-memory only, no persistence.
Custom files:
- Canonical paths rooted at MF / ADF.USIM / ADF.ISIM; entries are
{path, name, kind}. The editor now uses root + parent-DF selector +
4-hex FID + alias, requires the parent DF to be defined first, rejects
duplicates, rewrites descendants when a DF's path changes and cascades
deletes after a confirmation.
- Legacy forms are normalized on load/import (3F00/... -> MF/..., relative
a153/4954 resolved against the custom DFs); unresolvable entries are
dropped and reported in the list.
- Tree injection matches by exact parent path via the new pysimFsNodePath
(same-FID DFs under different parents no longer collide);
profilerCustomNameForPath uses the same normalization.
SW cache otaman-v167; help EN/RU + docs/api.md + AGENTS updated.
Tests: 236 Python + 371 frontend.
|
||
|
|
20fbfd99a5 |
scp81 passthru, proactive decoders, ADM badge, file manager layout (v2.2.1)
SCP81: - New listener mode "passthru": no local listener - the card's BIP channels connect straight to a configured external platform (host/port required), which terminates TLS and runs the dialog. The status API reports mode/target (_SCP81_MODE/_SCP81_TARGET) and clears them on stop. PWA mode select, hint, Start validation; TLS PSK stays the default. Proactive command decoding (Phone tab log): - SEND SHORT MESSAGE (0x13) is now decoded: alpha, address (TON/NPI + number), 3GPP-SMS TPDU (type, TP-MR, TP-DA, TP-PID 0x7F flagged as SIM data download, TP-DCS, TP-VP, TP-UDL), UDH concatenation IEs, and the TP-UD as text (GSM-7 with a septet unpacker, UCS2, 8-bit) or as a TS 31.115 secured packet for PID 0x7F; malformed TPDUs fall back to the raw hex line. - PROVIDE LOCAL INFORMATION qualifier names completed per TS 102 223 V18.3.0: ESN (07), MEID (0B), Supported RATs (1A); 05 relabelled "Reserved for GSM (Timing Advance)". Fixed in the server dict and both frontend tables. Header: - Compact ADM badge next to the card indicator: "ADM ✓" green when pySim's adm_verified is set, "ADM ✗" red otherwise, hidden without a card session or when the server is down; updated ahead of the card state-key early return so it never disturbs the connect/reset flow. File manager: - Sort pills (FID/Name), Probe all files button and progress line are pinned above the tree instead of scrolling with it; the tree box cap grows from 420px to 65vh. SW cache otaman-v165; help EN/RU updated; tests 234 Python + 361 frontend. |
||
|
|
8c90958718 |
scp81: PSK by identity, scripts page, exact snapshots, SCP80 LOAD fit (v2.2.0)
Cards / SCP81:
- Cards is a top-level tab; presets gain PSK identity + key, HTTP-OTA
column, Edit/Update and a live PSK-map push into a running listener.
- SCP81 has Listener/Scripts pills; scripts are named local APDU lists
(Empty / Explore / Install from .cap / Delete templates), sent to the
server explicitly at start. The listener takes mode/host/port/script
only; PSK inputs and the .cap row are gone.
- Multi-PSK TLS listener: identity -> key lookup from the card presets
(POST /api/scp81/psk-map updates a running listener), unknown
identities log tls-psk-unknown and fail; handshake logs carry psk_match.
- Script engine: execution tracking (next/done/pending/results), a
resumed dialog sends only the unexecuted tail (unreported APDU is
resent), a fresh dialog restarts, listing continuation pages are
tracked separately (pending.pages/complete). Restart script button.
- POST /api/scp81/gen-install replaces the SCP81 ram-install queueing
(generation only; the .cap is never stored).
Profiler / snapshots:
- Snapshot comparison is always exact (mask checkboxes removed; the
first-4-bytes mask remains a profile-creation option).
- "matching records" line shows count + #record numbers.
- New Clone action: copy named "Copy of <profile>", opened in the editor.
- Matched-record count/numbers fix ("1 из 8 (#8)").
SCP80:
- Configurable / auto-fitted LOAD block size: each LOAD APDU encodes into
one SMS (pySim rejects secured packets above 140 octets, so a 240-byte
block could never be sent). Response reports the effective size and
clamps; encode failures are reported per step with the pySim message.
SW cache otaman-v161; docs/api.md, scp81-findings and help EN/RU updated.
Tests: 226 Python + 356 frontend.
|
||
|
|
ff7a1ad5d3 |
scp81: fix RAM LOAD block splitting (overlapping 1-byte shifts) (v2.1.12)
The block slicer used the block number as a character offset (loadfile_tlv[i * 2:(i + 240) * 2]), so every LOAD block after the first was a 1-byte-shifted copy of the previous one - the cap header repeated every 239 bytes on the wire. A live install accepted three blocks, failed block 4 with SW 6400, then 6985, and INSTALL [for install] answered 6A88. The same slicing was inherited by the SCP81 helper from the SCP80 path, so multi-block caps could not install there either; both are fixed. Tests: blocks are consecutive and reassemble the C4 TLV byte-for-byte (200 python); findings updated; service worker v149. |
||
|
|
3463a68292 |
scp81: RAM install over HTTP OTA (v2.1.8)
- shared _cap_apdu_sequence helper (INSTALL [for load] -> 240-byte LOAD blocks -> INSTALL [for install]); the SCP80 /api/ram-install path now uses it too (one source of truth; byte-level tests pin the APDUs) - POST /api/scp81/ram-install: parses the .cap server-side and queues the APDU sequence as the SCP81 command script (one C-APDU per POST, runs on the card's next push); refused while a script is mid-run unless force - /api/scp81/script reports the script kind (explore/none/custom/ram-install) - tab: RAM install row (CAP file + SD AID + Queue button); RU strings - docs: api.md, findings, AGENTS; service worker v145 |
||
|
|
175ca934d8 |
scp81: PSK TLS server and command scripting (phases B/C) + BIP fix (v2.1.5)
- scp81.py: PSK TLS listener (stdlib ssl PSK callbacks) speaking the GP HTTP administration dialog; configurable framing (chunked/Content-Length, TLS record split, Apache-style/compact headers, Connection header, keep-alive, Next-URI template with %d, TLS version/cipher, answer delay, keylog for capture decryption) - server.py: script responder + Response Scripting parsing (AF/AB, 80/23 TLVs), memory decoder, SCP81 start options, terminal-side timer management, background-mode BIP events, permissive OPEN CHANNEL - BIP fix: the RECEIVE DATA channel-data TLV length is BER long form (36 81 <len>) above 127 bytes; a raw length byte is mis-parsed on the card, so the large TLS records never reached its stack (a live card fetched the script response and silently never processed it - endless resume). The card now executes scripts and returns R-APDUs: memory (13 applets, 50646 B NV free, 2402 B volatile), ISD, stored HTTP OTA parameters, ELF and application registries - frontend: SCP81 tab (listener, script selection, HTTP OTA log), phone event forms, i18n; service worker v141 - docs: api.md, scp81-findings.md (attempt matrix + root cause analysis); tools/scp81_decrypt.py decrypts listener captures via the keylog - tests: 187 python + 337 frontend |