The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:
- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
version the card was asked for and the CAP's own version when the probe
lowered it, appended synthetic entries and unmatched lines - each with the
resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
section the failing block covers (load-file byte range and component names,
'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
or that the import list already passed, the remote status word with its
name, and what the probe state implies (every import at 0.0 -> a missing
package or another load-file check is the cause; otherwise lower the
versions and bisect).
ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md. The same failure block for the real install is the next step.
sw cache simple-v325; tests: 550 Python / 703 Node.
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.
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.
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.
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.
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.
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.
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.
BIP was only reachable through the SCP81 listener; cards that use TCP for
other purposes (an applet's OPEN CHANNEL, a push trigger without HTTP OTA)
now get a generic control surface, independent of SCP81.
Server:
- /api/bip/control starts a plain BIP session in three modes: sink
(default; a local TcpDumpServer that accepts the card's channels and only
logs conn/sink-rx/conn-close, never answering; port 0 = ephemeral, the
bound port is reported), passthru (dial the OPEN CHANNEL destination, TCP
client only) and redirect (fixed host:port).
- /api/bip/status returns {owner, bip, listener}; /api/bip/log and
/api/bip/log-clear share the BIP event log with /api/scp81/log.
- The session state is shared with the SCP81 listener and only one session
runs at a time: _bip_session_stop() stops whichever control started it
and the control responses report it as `replaced` (both directions,
SCP81 <-> BIP). State renamed _SCP81_MODE/_TARGET/_LISTENER/_LINK_EVENTS
-> _BIP_* plus _BIP_OWNER.
- TcpDumpServer logs conn-close and counts accepted connections; stop()
joins the accept thread so the port is really free on restart.
- The new control endpoints are blocked during test-script runs.
PWA:
- New Simulator pill BIP (after TR Config): mode select with notes,
Host/Port rules, Start/Stop with a "replaced" notice, status line (mode,
bound address, owner, channels), a Channels box and the shared BIP log
(reuses the SCP81 log renderer, now showing `peer`).
- The SCP81 status line marks a session owned by the BIP pill.
Help EN/RU 8.10, docs/api.md, AGENTS; CAT_TP/UDP recorded as not
implemented.
Tests: tests/test_bip.py (9) and frontend/tests/bip.test.js (6).
600 frontend / 482 Python green; version 3.6.0; sw simple-v273.
The Import component (JC VM spec 6.6) is now exposed by /api/cap-info and
rendered in the CAP analysis box:
- imports: the libraries the CAP is linked against with the export-file
versions and the number of distinct constant-pool references (6.7),
displayed as "name >= version" (a card resolves an import only with the
same major and a minor >= the recorded one, 4.5.2). Standard names come
from the AID table; each gets a family label (Oracle JavaCard / ETSI SIM
2G / ETSI UICC / 3GPP USIM-ISIM / GlobalPlatform) and, for
javacard.framework, a Java Card SDK release hint derived from the local
Oracle SDK kit corpus (jc211..jc305u4 exports; unknown versions stay
unhinted). Vendor/applet AIDs stay bare.
- Header package flags (Table 6-4: int / exports / applet package) and the
optional JC 2.2 package_name (absent in all CAP 2.1 files).
- components: every archive entry in load-file order with its size and
share of the load file (including the Directory/Export entries capmem
does not parse); the sizes sum to load_file_bytes.
- The PWA box leads with "Requires: ..." when imports exist, keeps the
compiled-against line (Java Card hint + CAP format), the package/applet
identity, the import details (family, refs, AID) and the component
breakdown in Details; a memory-only response still renders.
Tests: Python +2 (imports/flags/name/components; header flags + package
name) with the synthetic CAP builder extended; frontend +2 renderer cases
(unknown AIDs, no-import responses) plus jcAidNorm/jcAidFamily tests; the
jcAidNorm extraction added to the ram/scp81 harnesses.
Help EN/RU, docs/api.md, AGENTS.
591 frontend / 473 Python green; version 3.5.16; sw simple-v269.
GlobalPlatform models this explicitly (GP Card Spec v2.3.1 Table 11-48,
load parameters): C6 = non-volatile code, C7 = volatile data, C8 =
non-volatile data, and 11.5.2.3.7 - when the card makes no code/data
distinction the required minimum is C6 + C8. The analysis now reports it:
- capmem.memory_json() accepts the load file size (all CAP components -
the package image the card stores) and returns code.load_file,
nvram.requirement = load_file + persistent data, and sets the suggested
C6 to the load file (the bytecode-only Method.cap figure stays in
code.method_component); _cap_info_body passes the size it already
computed.
- The CAP estimate box (both the RAM installer and the SCP81
Install-from-.cap form) leads with "NVRAM requirement ≈ code image +
data" plus the RAM estimate, keeps the bytecode/other-component split and
the full data breakdown in Details, and carries the GP citation with the
caveats (card memory management, allocation rounding and the registry
entry are not included; the static field image appears in both parts).
- An older server response without code.load_file still renders (the
bytecode size is used as the fallback).
Tests: Python +1 (load-file semantics) with extended cap-info assertions,
frontend +1 (renderer + fallback). Help EN/RU, docs/api.md, AGENTS.
587 frontend / 472 Python green; version 3.5.15; sw simple-v268.
PWA:
- The SCP80 "Source" switch sticks: the choice is stored in
`params.source`, switching keeps both values (the server honors the
explicit source when both are present, and the form validates only the
selected one).
- The SW check field is empty by default (placeholder "default: 9000
(91?? when polling)"), so the server defaults apply - previously the
pre-filled 9000 defeated the polling STATUS default and a card that
announced a command made the step fail with "expected 9000".
- Item text checks offer contains/exact only (the mask mode was rejected
by the server validation).
- The SCP80 counter is written back to the preset even when the run is
observed after a page reload or was started elsewhere (resolved by
ICCID or preset name from the run snapshot).
Server:
- Step-entry mutations happen under `_TEST_LOCK`
(`_test_entry_update`/`_test_finish_entry`): the status endpoint
serializes the state with json.dumps, so entries must not change while
it iterates them.
- The pending-command drains (unexpected command, error, stop) hold
`_CARD_LOCK` like every other card conversation.
- `_int` accepts plain decimals with leading zeros and 0x hex.
- The scripted TERMINAL RESPONSE text string uses the CR-set tag `8D`
(consistent with the other TR TLVs; both are legal).
- A script-driven menu selection mirrors `server.menu_active`.
Tests: frontend +4 (source switch/round-trip, status SW default, render
checks, item modes), Python +3 (integer parsing, menu_active, source
selection). Help/docs unaffected beyond api.md's `source` note.
583 frontend / 470 Python green.
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.
- Background STATUS polling is enabled by default, per the spec's idle
polling rule (TS 102 221 14.6.2): `_POLL_ENABLED = True`, __main__ runs
`_poll_enable()` regardless of card presence (--poll-interval 0 still
disables) and `_do_status_poll` keeps ticking while enabled even without
a session, so a cardless start resumes as soon as a card appears.
- A new card session clears a POLLING OFF from the previous card in
`_apply_equipped_card` before re-enabling polling.
- The Phone-tab toggle now shows the *effective* state: OFF in amber
("card disabled polling (POLLING OFF)") while the card suspended
proactive polling, back to ON on the next POLL INTERVAL. The 5 s
background poll now passes `card_disabled` through (it was dropped, so
an autonomously received POLLING OFF never reached the UI).
- Tests: poll_ui updated (button OFF while suspended, warning path),
test_poll +2 (cardless ticking, module default via a subprocess check).
- Help EN/RU, docs/api.md, AGENTS; version 3.5.10; sw cache simple-v263.
573 frontend / 441 Python green.
- EVENT_NAMES (server + PWA) corrected against TS 102 223 v18.3.0 8.25:
0x14 is "Access technology change (multiple)" (not Change of UICC
Access), 0x19 Profile container, 0x1A Void, 0x1B Secured profile
container, 0x1C Poll interval negotiation, 0x20-0x22 reserved. Values
the CAT spec leaves "Reserved for 3GPP" now carry the concrete TS 31.111
event name + clause (0x11 (I-)WLAN access status, 0x12 Network
rejection, 0x15 CSG cell selection, 0x17 IMS registration, 0x18 Incoming
IMS data, 0x1D Data connection status change, 0x1E CAG cell selection,
0x1F Slices status change).
- Poll Interval Negotiation (0x1C) in the Phone tab: the form proposes a
Duration (unit + interval) and the UICC's answer (TS 102 223 8.97
accepted / rejected / modified + optional Duration) is decoded and
shown; a "modified" duration becomes the background poll interval. The
form appears when the card subscribed to the event, like every other.
- Polling emulation is card-driven: a POLL INTERVAL adopts its Duration
(minutes/seconds/tenths -> 1..255 s, logged) for the background poll,
is echoed in the TERMINAL RESPONSE (6.8.4) and clears a POLLING OFF
suspension; POLLING OFF (6.4.14) suspends proactive polling until a new
POLL INTERVAL - the manual Send STATUS button and presence detection are
unaffected. /api/poll-status and /api/poll-toggle report card_disabled
(+ a warning when enabling while suspended); the PWA shows it in amber.
- Tests: tests/test_poll.py +7, test_proactive_names.py +3, frontend
event_forms +2 and poll_ui (4). Help EN/RU, docs/api.md, AGENTS.
573 frontend / 439 Python green; version 3.5.9; sw cache simple-v262.
Selecting a .cap in the RAM installer or the SCP81 "Install from .cap"
template now runs a read-only analysis (POST /api/cap-info) before any
APDU is built: the archive is validated structurally (a corrupt or
wrong-format file fails here) and the bundled capmem analyzer estimates
the code size and the persistent (NVRAM) / volatile (RAM) requirements,
with the tool's suggested C6/C7/C8 quotas shown as information. The
form's action button (Execute / Generate) stays disabled until the
analysis succeeds - pressing it is the user's confirmation to continue.
- pysim_simple_server/capmem.py: bundled analyzer (component parsers +
JCVM opcode table + method-bytecode allocation scan), adapted to take
the CAP archive as bytes and return report/memory dicts; output
verified byte-identical to the workspace tool on 21 real CAPs.
- _cap_info_body + POST /api/cap-info (read-only; the install endpoints
stay unchanged and self-sufficient).
- PWA: shared capAnalyzeFile/capMemHtml/capGateOk helpers, estimate box
under both CAP inputs (reusing the idle #ram-cap-info div, new
#scripts-cap-info), data-cap-gate gating in pysimApplyAvailability,
stale-response guard, Retry, EN/RU strings.
- Tests: tests/test_cap_memory.py (synthetic CAPs: new/newarray/
makeTransientByteArray/static fields/unknown-opcode warnings/corrupt
input), frontend capmem.test.js (renderer, gate, analyze flow).
- help EN/RU, docs/api.md, AGENTS; version 3.5.8; sw cache simple-v261.
567 frontend / 429 Python green.
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.
'Explore' and 'Delete' were ambiguous next to the other SCP81 actions.
- template selector: Explore -> Explore ISD, Delete -> Delete AID (EN/RU);
- a new Explore script is named 'Explore ISD'; the Delete template keeps
its count-based name (e.g. 'Delete 2 AIDs');
- new scriptKindLabel() maps the stored kinds (unchanged:
explore/delete/install/empty, so existing localStorage scripts keep
working) to those labels and is now used for the scripts table kind
column, which used to print the raw lowercase kind untranslated;
- docs: help EN/RU, README/RUS, docs/api.md examples, AGENTS;
- sw.js simple-v238.
ListNotification showed 'pmo' as the operation for every notification:
pySim's ProfileMgmtOperation is a Struct whose first (ignored) byte is
the padding-bits octet, so a TLV parsed from the card nests the flags
under 'pmo' while an object built from decoded flags (the unit-test path)
does not. The mapping iterated the outer dict and reported the nested
dict as a truthy key.
- esim._profile_operations() accepts both shapes and returns the set
flags in spec/bit order (install, enable, disable, delete).
- tests: a notification parsed from the card-shaped raw TLV (padding
octet included) plus the helper's both-shapes/multi-flag cases.
- docs: /api/esim/notifications and the repo AGENTS note.
pySim already requests the Icon tag (0x94) together with the other
ProfileInfo tags; the mapping dropped it, so the PWA only had the icon
type.
- esim.profiles(): each profile now carries `icon` (the image bytes as
hex) and `icon_size` (byte count), null when the card sent no image.
- PWA: esimIconDataUrl() builds a data: URL (png/jpg -> image/png|jpeg)
and the profile card shows the image unscaled to the left of the
metadata rows; the Icon row keeps the type and adds the data size
(e.g. 'png · 1234 B').
- tests: profiles icon/icon_size mapping (+ the absent case) and the
frontend data URL / row / render checks; docs and sw simple-v231.
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.
The chip endpoint returned pySim's flattened EuiccInfo dict, whose classes
are incomplete: the capability fields are raw GreedyBytes, extCardResource
is raw bytes and several SGP.22 TLVs are missing from the class, so cards
showed 'unknown_ber_tlv_ie_99' and raw hex instead of decoded values.
- request the EUICCInfo1/2 and configured-address TLVs raw and decode them
in esim.py per SGP.22 v2.6 5.7.8, cross-checked against lpac's
es10c_ex.c: extended card resource, UICC/RSP capability bit lists (first
octet = unused bits, MSB-first), CI PKI lists, category (both the
implicit 0x8B and explicit 0xAB tag encodings), forbidden profile policy
rules (0x99), ppVersion (0x04), sasAcreditationNumber (0x0C) and the
optional certification data object / TRE fields; undecoded TLVs stay in
raw_tlvs instead of being dropped.
- add the ES10b GetRat rules authorisation table (PPR ids, allowed
operators, consent flag) to the chip response.
- PWA: label every new field, map nested labels per path component (the
old code only matched whole keys), group the view into EUICCInfo1 /
EUICCInfo2 / Addresses / RAT sections, render arrays of objects with
index labels and translate the labels (RU).
- tests: decoders against a real card's values (077F3E1F80, 0490, 0640,
81010082040006B32C83022646, the RAT fixture) and the frontend label
mapping; sw.js simple-v229.
- docs/api.md: document GET /api/net-state and POST /api/net-state-refresh
(state shape, monitored keys, refresh filter, 503); add eid/euicc to
/api/status; correct the /api/net-sim FPLMN wording (roaming_denied
appends, attach clears; 5GS files untouched) and its response
(net_state); move the stray /api/event-send example back into its
section; refresh the version examples to 3.x.
- README/RUS: Cards — ADM field, the three TARs, the SCP80/SCP81
fieldsets and the header markers; File Browser — Read raw/Read decoded
pills, Edit raw, named FCI block, Verify ADM; Profiler — Clone, three
list tabs; Custom files — root/parent/FID/alias form, canonical paths,
cascade delete, legacy-path resolution; Phone simulator — three pills,
eSIM, Network state monitor, Home network button, corrected FPLMN
wording, proactive-log row content; RU CLI table gaps (--sms-oa/
--sms-sm-sc, --terminal-profile, --mcc-mnc-list) and the missing
event-form bullets.
- help EN/RU: three profiler list tabs, 3.x compatibility example.
- Version 3.0.1 (docs release), sw.js simple-v228.
New eSIM pill (Phone simulator) for SGP.22/32 cards, built on pySim's
ES10 static API — no lpac, no new dependencies, no pysim patches, no
SM-DP+ interaction:
- Chip: EID, EUICCInfo1/2, configured addresses (ES10a/b).
- Profiles: GetProfilesInfo with metadata (state, nickname, provider,
ICCID, ISD-P AID, class, owner, icon).
- Notifications: read-only ListNotification viewer.
- Switch: Enable/DisableProfile with RefreshFlag=1; the card's REFRESH
(fetched/answered/logged by the transport's proactive handler) or an ok
result triggers _esim_reinit() — reset + equip + _apply_equipped_card —
so the ICCID, network state and cached views are re-read.
- Server: pysim_simple_server/esim.py, GET /api/esim/chip|profiles|
notifications, POST /api/esim/profile (all under _CARD_LOCK; 400
not_an_euicc), euicc/eid in /api/status.
- Tests: tests/test_esim.py (fake scc, monkeypatched store_data_tlv),
frontend/tests/esim.test.js; help EN/RU, docs/api.md, AGENTS.
The preset ADM key was stored but never used: the header badge showed
whether a key exists and whether the card was verified, yet the only way
to verify was the pySim command line.
- POST /api/verify-adm builds the TS 102 221 VERIFY itself (CHV number
from the card model, short keys padded to 8 bytes with 'f') so the raw
SW is reported: 63Cx -> attempts_left, 6983/9804 -> blocked, 6982 ->
security error. The key is never stored and is redacted from request
logs.
- PWA: the header ADM badge is clickable when the matched preset has a
key; a failed file-manager read/write (6982/9804) shows a Verify ADM
button next to the error. Every retry after a failure asks for
confirmation and shows the remaining attempts (stronger text on the
last attempt); a blocked ADM disables both entry points until the card
session changes. No automatic retries.
- tests: tests/test_adm_verify.py (fake scc, APDU/SW mapping, redaction)
and frontend/tests/adm_verify.test.js (retry prompt, SW classifier,
wiring) + card_state indicator expectations
- docs/api.md, help EN/RU, AGENTS; version trio 2.7.8; sw cache v211
Live-testing against a UICC without the EPS files (no USIM service 85)
and without the Kc files showed the runner aborting when EPSLOCI/Kc were
absent. write_binary/write_record now take an 'optional' flag: a missing
candidate file is logged as a skip step instead of failing the scenario
(store_epsnsc, real/dummy locations, churn and Kc writes). Also allow
POST /api/write to select by 'path' like /api/select and /api/read (the
netsim live test needed it to restore the captured file values).
Server (pysim_simple_server/netsim.py + POST /api/net-sim):
- pure builders for the observed phone write vocabulary (UICC_NAA.md §13):
EPSNSC (A0 TLV with KSI/KASME/counts/algo, padded to the card's FCP record
size; invalidate-wipe and invalidate-keep-key), LOCI/PSLOCI/EPSLOCI real
and dummy forms (PLMN kept, LAC FFFE, status 01/02), Kc/KcGPRS (9-byte
USIM and 33-byte GSM forms, 07 invalidates), EF.SMSstatus counter bump
(read-modify-write), CBMI/CBMIR, the Location status event (9B 01 <st>,
with the optional 13 07 location info for normal service) and the
AUTHENTICATE APDU (TS 31.102 7.1.2.1) with DB/DC response parsing
- scenario runner executes the recipes under _CARD_LOCK, picks the first
existing candidate path (ADF.USIM vs DF.GSM/DF.TELECOM), reads FCP/current
records where the format is card-specific, skips the event unless the card
subscribed to Location status, honours the scenario toggles and returns a
per-step log; only D6/DC, ENVELOPE and AUTHENTICATE are ever sent
- GET /api/mcc-mnc serves the optional worldwide operator list
(--mcc-mnc-list, default <workspace>/samples/mcc-mnc-list.json): ?q=
search and ?random=1&exclude= for roaming; the list stays out of the repo
PWA:
- Phone pill gains a 'Network simulation' fieldset: one button per scenario
(cold boot, EPS/2G attach, service lost, limited service, roaming denied,
churn, SMS received, CB reconfig, AUTHENTICATE), a collapsed Parameters
block (operator search + random roaming, LAC/Cell ID/TAC/RAC, optional
identities, toggles, churn count/delay) and a step log with SWs
- i18n EN/RU, help/README/api.md/AGENTS.md updated; version 2.5.0,
SW cache simple-v193
Tests: tests/test_netsim.py (19), frontend/tests/netsim.test.js (3) and
html guards - 423 frontend / 280 Python, all passing
- version check in pysimConnect() now compares the server major against
SIMPLE_VERSION (2.3.1) instead of the hardcoded 1.x assumption; the header
version is rendered from the same constant
- removed the 'SIM OTA with a Human Face' slogan (span, i18n key, translate
hook) to free header space
- broaden the PWA description in manifest.json/package.json and the help
overview (EN/RU): APDU workbench, OTA lab, CAT/STK simulator, card profiler
- version compatibility tables in README/RUS, help EN/RU and docs/api.md now
describe the major-version rule
- README/RUS: Cards promoted to a top-level section; EN duplicate
Theme/Localisation sections removed
- docs: /api/cardinfo documented; SCP81 'expose framing options' next-work
item dropped (done); local AGENTS.md not-implemented note corrected
- SW cache simple-v190
- Server: _decode_iccid (nibble-swapped E.118 digits, trailing-F pad) and
_read_iccid (best-effort MF/2FE2 read through the parent-scoped select
helper; the previous selection is restored, the read never raises). The
equip path records the digit string before the TERMINAL PROFILE, i.e.
before any CAT session is active; a startup with a card does the same.
The value is cleared on card removal and exposed as /api/status 'iccid'
(only while connected).
- PWA: when a connected status update reports a *new* ICCID, the matching
card preset is selected in both SCP80 views - Secured Packet (sp-card-sel
+ form fill) and RAM (ram-card-sel + _ramCardIdx). Matching normalizes
digits and accepts the raw EF hex form, leading zeros ignored; a manual
choice for the same card is kept until the next equip, and a card removal
re-arms the auto-selection. The status line shows the ICCID.
- Tests: tests/test_iccid.py (decode variants, model + probe read, equip
recording, disconnect clearing) and frontend/tests/cards_iccid.test.js
(normalize, find, select, no-override, card swap); the card_state test
harness stubs the new hook and covers the guard reset.
- Docs: api.md /api/status fields, help EN+RU (SCP80 intro + Cards tab),
READMEs, AGENTS. SW cache otaman-v183.
- Keep-alive is no longer a setting: the TLS connection stays open between
POSTs for the whole dialog (the card, as HTTP client, may still dial a new
one at any time - GP Am. B 4.3.1) and only the 204 ends it, with a clean
close_notify while the response is still buffered, then FIN. Dropped
keep_alive/on_before_close/_scp81_wait_drained and the Connection-header
'close' value (the API, status and UI no longer carry a keep-alive knob).
- The Listener Options block is a collapsed <details> with a 'custom' badge
when anything differs from the reference defaults; the Reset button moved
into the body so it cannot toggle the panel.
- X-Admin-Targeted-Application is opt-in (checkbox + //aid/... field, field
disabled while off); X-Admin-Next-URI has a checkbox + hint explaining the
one-shot rule of GP Am. B 4.4.2; both dependent fields grey out when
unchecked.
- Labels/i18n: 'Chunked body (Transfer-Encoding: chunked)' stays English,
'Show link events (...)', 'Теги comprehension-required'; the compact-header
wording now spells out that it omits the optional space after ':' (legal
per RFC 7230 3.2 OWS; saves one byte per header).
- Tests: a 200 keeps the socket for the next POST; the 204 closes with a
mutual close_notify exchange; options helper/badge unit tests; removed the
close-per-response and drain-wait tests. SW cache otaman-v181.
Listener Options block (applied at Start, persisted in localStorage, Reset to
defaults): HTTP framing - chunked body, chunk size (0 = one TLS record),
keep-alive, Connection header, compact headers, Next-URI (unchecked = omit);
script framing - indefinite/definite Command Scripting template, CR tag,
targeted app; link events (now parsed on the common start path, so every mode
honors them). scp81OptionsFromForm() is unit-tested.
TLS is automatic: 'auto' (min 1.0, max 1.2 + :@SECLEVEL=0) is the new
default, all six PSK suites are offered and OpenSSL negotiates the highest;
the negotiated version/cipher is logged (tls-handshake) and reported as
version_seen/cipher_seen in /api/scp81/status, and a handshake failing for a
TLS/cipher reason logs tls-handshake-failed (post-handshake record errors
stay tls-error). tls_version/cipher/keylog/answer_delay stay as API-only pins.
Dropped the Apache-style header mimicry completely: no Date/Server/
X-Powered-By, no Content-Length-before-Content-Type ordering, no Content-Type
on 204 - the minimal response set is X-Admin-Protocol (+ X-Admin-Next-URI /
Targeted-Application), Content-Type on 200s, and Transfer-Encoding or
Content-Length per the chunked flag. Docs, help (EN/RU), READMEs and the
AGENTS notes updated; SW cache otaman-v180.
The former 'passthru' mode is now 'redirect': it pins the configured target
and every BIP channel is connected there (the card's requested address is
only logged; host/port required). The name 'passthru' now means the new mode:
no listener and no target - each channel dials the destination the card
requests in OPEN CHANNEL (Other address + Transport level port, TCP client,
remote, 0x02 only). The specs define no default port (TS 102 223 8.59), so an
incomplete or non-TCP request fails the channel with result 3A and an
open-fail log reason.
BipTerminal gains a mode (enable(host, port, mode=...), open(..., proto=...)),
reports it in status(), and the control API/status expose redirect (target)
and passthru (per-channel targets). The PWA mode selector shows four modes
with per-mode notes and disables Host/Port in passthru; the status line shows
each channel's actual target. Docs, help (EN/RU), READMEs and the AGENTS notes
updated; SW cache otaman-v179.
Breaking API change: mode:'passthru' has the new semantics - use
mode:'redirect' for the previous behavior (no alias).
Audit against GP v2.2 Am.B 4.7 / TS 102 226 / GP Card Spec 11.11 found the
retry waiting delay encoded as plain hex instead of the TP-SCTS semi-octet
order required by TS 102 223 8.38 -> TS 23.040 9.1.2.3 (1 min must be 10,
20 s must be 02); the builder now encodes semi-octets, clamps 0-59 / 0-99 and
pads the 2-byte counter. Odd-length hex is padded instead of producing
fractional BER lengths; empty 83/84/89 sub-TLVs are omitted (lengths are 1-n
per Tables 4-3/4-5/4-8..10) and an empty trigger becomes 81 00; a store
payload above a short APDU is chained as P1.b8=0 STORE DATA blocks (P2 =
block number). Connection presets fixed: device identities 82, alpha 05,
command details 81, bearer 35/03; the A5 store tag and the 'B0,00=unlimited'
counter hint are marked unverified (not in the pinned spec). Docs:
UICC_SPECS.md 9.6.5 example annotated 2 s, findings 10-minute timer corrected
to 1 minute; help updated. SW cache otaman-v177.
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.
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.
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.
explore with the fixed response parser ran 18 commands / 10 continuation
pages (real 63 10/9000 statuses) and shows the installed applet:
AA1902BC22580101 (life=07 selectable, elf=AA1902BC225801) in the
applications listing, AA1902BC225801 (life=01) in the ELF registry and with
its module in the ELF+modules listing.
The R-APDU TLV length was read as a raw byte, so a listing page above 127
bytes (AF 80 23 81 FC <252 bytes> 00 00) was cut to its first 127 bytes with
a bogus status word (the data's last two bytes: CAFE/0001/9F70 instead of
the real 63 10 "more data"). The bogus SW also stopped the SW CAFE/6310
pagination, which is why later registry entries - e.g. the installed package
AA1902BC225801 - never showed up. Uses httpota.ber_len_read now.
Tests: 250-byte long-form page and short-form regression (204 python);
findings updated; service worker v154
- continuation repeats the SAME GET STATUS command with P2.b1 set (the
pagination state lives in the card); changing the 4F criterion is a match
filter, not a position - P2=03 with the last AID is rejected with 6A80 and
P2=02 with it returns that single match (the earlier duplicate)
- handle the standard "more data available" warning SW 63 10 (Table 11-38)
in addition to the live card's proprietary CA FE
- explore script: P1=40 is applications+SDs, P1=20 the ELF registry, P1=10
ELF+modules (Table 11-33) - the ELF-only registry was never queried, which
hid the installed package; labels and the results decoder show C4 (ELF AID)
and CC (SD AID) too
- UICC_SPECS.md: GET STATUS P1/P2 tables made explicit with the pagination
rule, plus BER length coding notes for the scripting templates and the
TS 102 223 channel data TLV (the two >127-byte traps)
201 python + 346 frontend; service worker v153
P2=02 means "get first or all occurrence(s)" (Table 11-34), so every
continuation re-returned the first listing (the criterion's single match),
pagination stopped after one extra page and the installed package
AA1902BC225801 never showed up in the ELF registry. P2=03 = "get next
occurrence(s)" is the correct value for the SW CAFE continuation.
Tests updated with the new continuation bytes; findings documented;
service worker v152
INSTALL [for load] -> LOAD x6 -> INSTALL [for install] (SW 9000) against the
live card. Records the parameter-less 6A80 finding and the working
INSTALL [for install] command built from the Remote APDU -> RAM form and
queued via "Queue in SCP81".
- POST /api/scp81/queue takes an explicit APDU list (or single APDU) and
queues it as the SCP81 script; entries that already are Command Scripting
templates (AA.../AE80..., the expanded format) are sent verbatim instead of
being wrapped again
- Remote APDU -> RAM chain: "To expanded" builds each command in the
TS 102 226 expanded form (AA definite / AE80 indefinite selector) and
"Queue in SCP81" queues the built commands for the next card POST, so the
full-featured RAM/INSTALL [for install] form (AIDs, privileges, TK/STK
parameters) can drive the HTTP OTA install
- RU strings; api.md; service worker v151
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.
The C-APDU TLV length was written as a raw byte: a 245-byte LOAD command
produced 'AE 80 22 F5 ...', which BER reads as a long-form marker, so the
card mis-parsed every script command over 127 bytes. Small commands worked,
which made a RAM install look alive: the card answered the LOAD steps with a
degenerate 'AF 80' body (no R-APDU), and the final INSTALL [for install]
failed with 6A88 because the package never loaded.
Both the indefinite ('22' TLV) and definite ('AA' outer) lengths are now
BER-encoded (same rule as the BIP channel data TLV fix). Tests: 245-byte
LOAD body, short form, definite variant (200 python, 346 frontend);
findings doc updated; service worker v148.
- 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
Long GET STATUS listings answer SW CAFE with 127-byte pages. The script
responder now extracts the last complete AID from the page and inserts a
next-occurrence GET STATUS (80F2 <P1> 02 <Lc> 4F <len> <AID> 00) as the
next command, until the listing ends. Pages are logged (script-page), a
repeated page logs script-page-stalled and stops, inserted continuations are
dropped at session start.
Live-verified: ELF registry and applications collected completely in two
pages each (7/7 commands, all script-status ok).
- tests: page parsing (truncated tails, live FC-prefixed junk), continuation
bytes, auto-insert, stall guard, per-session purge (192 python + 337 frontend)
- UI: page number and script-status log rendering; service worker v143
- 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