v3.26.0: per-TAR default keysets (the preset/SCP80/RAM rework)

Every preset TAR entry carries `keyset` (int 1-15) - the keyset every
operation addressed to that TAR uses: mandatory for any non-zero MSL
(the counters are keyset-linked), optional for MSL 00 (allowed: some
cards reject the no-security 00/00 KIc/KID form).  The store fills it
automatically when the preset has a single keyset, the validation
refuses an entry naming an undefined keyset, and the schema is 6 (a v5
file loads leniently; editing it forces the fix).

The keyset selectors are gone: the SCP80 TAR field became a select of
the preset's entries (applying the TAR's keyset to the form and seeding
the SPI1 from its MSL); the RAM operations use the preset's ISD role
entry (the PWA sends only the preset id, the ISD TAR, SPI and the
counter override - the server resolves the keyset and the keys:
presets.tar_keyset, _preset_tar_keyset, _resolve_operation_keyset,
_op_keyset, used by send-ota's APDU path, ram-install, ram-install-app
and cap-compat); the counter probe and the test runner's scp80 steps
default to their TAR's keyset (an explicit kvn stays an override).  A
security TAR without a keyset refuses the operation ("set it in the
Cards tab") - never a silent keyless send - and the start buttons wait
for a usable operation-TAR keyset.

The TAR probe checks the preset's TAR list only (the standard
TAR_PROBE_TARS list and the custom-TAR localStorage are gone; a `tars`
list selects a subset, a TAR outside the preset is refused), each row
using its own MSL and default keyset.  The Cards editor's TAR table
gained a Keyset column (the only keyset preselected, an amber marker
when a security MSL lacks one).

Docs: help EN/RU, api.md's tar-probe contract, both AGENTS.  Tests: the
store (the field, the auto-fill, the schema, the string forms), the
server resolution (send-ota + the refusal, the probe's per-row keysets
and refusals), the PWA (the TAR select, the RAM ISD resolution, the
keyset-free requests) - several fixtures updated for the new schema.
891 frontend + 885 Python; sw cache simple-v434.
This commit is contained in:
2026-10-14 12:21:16 +03:00
parent 6e4494195c
commit cb40662e4e
18 changed files with 955 additions and 611 deletions
+17 -15
View File
@@ -341,28 +341,30 @@ first attempt instead of hammering the card.
### `POST /api/tar-probe`
Probe which OTA applications the card has registered: one **secured packet per
TAR** (the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1 unless
a `tars` list is given), each carrying a harmless C-APDU (`SELECT MF` by
default), with the preset's keyset. Each checked TAR uses **its own MSL** when
the preset carries the entry, otherwise the request's `spi1`; a TAR that
resolves to neither is refused with a hint **before any packet is sent**. The
resolved SPI1 must carry a counter check (b5b4 ≠ 00) and ask for a PoR.
Every accepted packet consumes a counter, which is persisted into the keyset
like any other operation, so the probe is refused when the SPI1 has no counter
check or the named preset does not define the keyset number. A missing
`cntr` is seeded from that keyset (v3.20.0; an explicit value wins but never
lowers the stored counter). The delivered SMS carries the OTA CPI IE by
TAR of the named preset** (v3.26.0 — the probe checks the preset's TAR list
only; a `tars` list may select a subset, and a TAR outside the preset is
refused with `400`), each carrying a harmless C-APDU (`SELECT MF` by default).
Each row uses **its own MSL** (the request's `spi1` is only a fallback for a
TAR the preset does not carry) and **its own default keyset** — the keyset
resolved from that TAR's entry, so the request does not carry KIc/KID/keys and
the probe's keyset selector is gone. A TAR whose MSL requires security
(non-zero) without a default keyset, or one naming an undefined keyset, is
refused with a hint **before any packet is sent**. The resolved SPI1 must
carry a counter check (b5b4 ≠ 00) and ask for a PoR. Every accepted packet
consumes a counter, which is persisted into that row's keyset like any other
operation (each keyset has its own counter, TS 102 225 A.1); an explicit
`cntr` seeds the first row only. The delivered SMS carries the OTA CPI IE by
default (`includeCpi`, as in `/api/send-ota`).
```json
{"preset_id": "…", "kic": "25", "kid": "25", "kicKey": "…", "kidKey": "…",
"cntr": "0000000187", "apdu": "00A40000023F00", "tars": ["000000", "B00000"]}
{"preset_id": "…", "spi1": "16", "apdu": "00A40000023F00",
"tars": ["000000", "B00000"]}
```
**Response:** `{"success": true, "registered": N, "total": M,
"final_cntr": "…", "kvn": 2, "spi1": "16", "spi2": "01",
"results": [{"tar": "000000", "label": "Issuer Security Domain (compact)",
"spi1": "16", "verdict": "registered", "sw": "9000",
"results": [{"tar": "000000", "label": "ISD", "spi1": "16", "kvn": 2,
"verdict": "registered", "sw": "9000",
"por_status": "por_ok",
"por_sw": "6D00", "por_data": ""}], "steps": [...]}`
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -282,7 +282,7 @@
<p class="mb-3">The <strong>SCP80</strong> top-level tab groups the SCP80-related views. It is switched by three pills: <strong>Secured Packet</strong>, <strong>RAM</strong> and <strong>TAR probe</strong>. Assembles SCP80 secured packets per ETSI TS 102 225. When a card is equipped the server reads its <strong>EF.ICCID</strong> (2FE2); if a card preset carries the same number (digits, or the raw EF hex), it is selected automatically in all three SCP80 views.</p>
<h3 id="secured-packet" class="text-lg font-medium mb-2">3.1 Secured Packet</h3>
<p class="mb-2">Builds SCP80 secured packets per ETSI TS 102 225. The <strong>Keyset</strong> selector next to the card preset selector picks which keyset of the preset supplies the keys and the counter; the send is refused when the preset does not define that keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The packet's TAR field suggests the preset's TAR entries (each with its description and MSL); applying a preset sets the operation's TAR and seeds the SPI1 from that TAR's <strong>MSL</strong> (Minimum SPI1, TS 102 226 &sect;8.2.1.3.2.4 &mdash; the card checks it before the security processing and answers <code class="font-mono text-sm">0A</code> when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form; a hand-typed TAR keeps the operator's SPI1. The <strong>Counter</strong> field is an override: it starts empty and shows the preset's next counter as its placeholder &mdash; an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).</p>
<p class="mb-2">Builds SCP80 secured packets per ETSI TS 102 225. The <strong>TAR</strong> selector lists the selected preset's TAR entries (role label or description, MSL and default keyset); choosing one applies that TAR's <strong>default keyset</strong> to the form (its KIc/KID and keys &mdash; v3.26.0: the keyset selector is gone, the TAR drives it) and seeds the SPI1 from its <strong>MSL</strong> (Minimum SPI1, TS 102 226 &sect;8.2.1.3.2.4 &mdash; the card checks it before the security processing and answers <code class="font-mono text-sm">0A</code> when the packet's SPI1 is below it). A value below the MSL shows an amber warning under the form. The start button stays disabled until a card preset is selected and the operation TAR's keyset is usable: a TAR whose MSL requires security (any non-zero MSL &mdash; the counters are keyset-linked) must have a default keyset in the preset, while an MSL 00 TAR may go keyless or still carry one (some cards reject the no-security <code class="font-mono text-sm">00/00</code> KIc/KID form). The send is refused when the preset does not define the named keyset number or when its KIc/KID carry different numbers (TS 102 225 A.2). The <strong>Counter</strong> field is an override: it starts empty and shows the preset's next counter as its placeholder &mdash; an empty field lets the server seed the packet from the stored counter and persist the value the card accepted; a typed value is used for that send only, is never written to the preset, and a lower one never rewinds the stored counter (only forward moves are persisted).</p>
<h4 id="packet-structure" class="font-medium mb-1">Packet structure</h4>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Size</th><th class="text-left py-1 px-2">Description</th></tr></thead>
@@ -314,7 +314,7 @@
<p class="text-sm mb-3">A &ldquo;Verify vs pySim&rdquo; button cross-checks the assembled packet against pySim&rsquo;s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. A &ldquo;Send to Card&rdquo; button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim&rsquo;s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a <strong>concatenated</strong> download (TS 31.115 &sect;4.3) &mdash; the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card&rsquo;s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command&rsquo;s status word and response data are filled into the <strong>R-APDU parser</strong> panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check &mdash; b5 b4 &ne; 00; without one no counter is advanced or saved) and clears the packet &mdash; the server persists the accepted counter into the preset, the form&rsquo;s counter field stays empty (its placeholder follows the stored value) unless the operator typed an override. A <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR&rsquo;s CNTR field is a copy of the packet&rsquo;s), so the notice offers a bounded <strong>Sync counter</strong> probe &mdash; a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored (a search stopped by a security verdict after a counter rejection saves the value too, with an amber warning to check the security settings) &mdash; and a <strong>Go to preset</strong> button for raising the counter by hand; nothing else is written automatically.</p>
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). The <strong>Keyset</strong> selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> wrap it in the TS 102 226 &sect;5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader&rsquo;s form, Le=&rsquo;00&rsquo; returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats &mdash; the report names the one that served it. A <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) that a format refuses is retried in the other formats too &mdash; the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers <code class="font-mono text-sm">61xx</code> and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) &mdash; some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template &mdash; and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card&rsquo;s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists &mdash; the package&rsquo;s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance &mdash; so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.</p>
<p class="mb-2">Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). v3.26.0: the operation's keyset is the ISD TAR entry's <strong>default keyset</strong> &mdash; the keyset selector is gone &mdash; and the server resolves it (and its keys) from the preset store. The start button stays disabled until a card preset is selected and the ISD TAR's keyset is usable: a non-zero MSL requires a default keyset, an MSL 00 TAR may go keyless. The <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> wrap it in the TS 102 226 &sect;5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader&rsquo;s form, Le=&rsquo;00&rsquo; returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats &mdash; the report names the one that served it. A <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) that a format refuses is retried in the other formats too &mdash; the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers <code class="font-mono text-sm">61xx</code> and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) &mdash; some cards serve the expanded next page only wrapped in the <code class="font-mono text-sm">AA</code> template &mdash; and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card&rsquo;s inferred <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists &mdash; the package&rsquo;s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance &mdash; so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.</p>
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
<table class="w-full text-sm mb-3 border-collapse">
@@ -342,13 +342,13 @@
</section>
<section class="mb-10">
<h3 id="tar-probe" class="text-lg font-medium mb-2">3.3 TAR probe</h3>
<p class="mb-2">The third SCP80 pill probes <strong>which OTA applications the card has registered</strong>: it sends <strong>one secured packet per selected TAR</strong> &mdash; the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1, each row carrying the document it comes from &mdash; with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>) using the view's preset and keyset &mdash; each checked TAR uses its own <strong>MSL</strong> when the preset carries the entry, otherwise the SPI1 from the shared form; a TAR resolving to neither is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 &ne; 00) and request a PoR. The <strong>Execute</strong> button stays disabled until a card preset and one of its keysets are selected.</p>
<p class="mb-2">The list is pre-filled with the standard TARs (all checked; <strong>Select all</strong> / <strong>Select none</strong> switch them quickly) and custom TARs can be added with <strong>Add TAR</strong> (three hex bytes plus an optional description). Custom entries are kept in <code class="font-mono text-sm">localStorage</code> on a best-effort basis &mdash; losing them is harmless.</p>
<p class="mb-2">The third SCP80 pill probes <strong>which OTA applications the card has registered</strong>: it sends <strong>one secured packet per selected TAR</strong> with a harmless C-APDU (default <code class="font-mono text-sm">SELECT MF</code>). v3.26.0: the rows are the selected preset's TAR list (no arbitrary TARs), each row uses its own <strong>MSL</strong> and its own <strong>default keyset</strong> (resolved from the preset store &mdash; the probe's keyset selector is gone), and a TAR whose MSL requires security without a default keyset is refused before anything is sent. The resolved SPI1 must carry a counter check (b5b4 &ne; 00) and request a PoR. The <strong>Execute</strong> button stays disabled until a card preset is selected.</p>
<p class="mb-2">The list shows the selected preset's TAR entries (all checked; <strong>Select all</strong> / <strong>Select none</strong> switch them quickly); each row's tooltip names its MSL and default keyset.</p>
<p class="mb-2">Every TAR is classified from the card's answer: <strong>registered</strong> (ENVELOPE 9000 + a PoR carrying the application's own status word and data), <strong>accepted, no answer</strong> (a PoR without an R-APDU), <strong>accepted, no PoR</strong> (the packet was silently dropped) and <strong>refused</strong> (a non-9000 ENVELOPE, typically <code class="font-mono text-sm">6200</code> for a TAR without a registered application). The result table appears in the operation popup; the line under Execute summarises <code class="font-mono text-sm">registered N / M</code>.</p>
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset &mdash; the probe is refused when the resolved SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset &mdash; the probe is refused when the resolved SPI1 has no counter check, when the preset lacks the row's default keyset (a non-zero MSL requires one) or the entry names an undefined keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself &mdash; a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> &mdash; a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 &sect;5.1.2/A.2): KIc and KID of a keyset must carry the same number (01&ndash;0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows &mdash; one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) &mdash; <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset &mdash; or the selected preset belongs to another card &mdash; all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes &mdash; PIN1/PUK1/PIN2/PUK2 as 4&ndash;8 decimal digits and the ADM key &mdash; stored for later use), <strong>TAR</strong> (the TAR table), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's <strong>CAT (USAT) release</strong> &mdash; the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself &mdash; a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> &mdash; a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 &sect;5.1.2/A.2): KIc and KID of a keyset must carry the same number (01&ndash;0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows &mdash; one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) &mdash; <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset &mdash; or the selected preset belongs to another card &mdash; all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes &mdash; PIN1/PUK1/PIN2/PUK2 as 4&ndash;8 decimal digits and the ADM key &mdash; stored for later use), <strong>TAR</strong> (the TAR table &mdash; each entry carries its TAR, its MSL and its <strong>default keyset</strong>: the keyset every operation addressed to that TAR uses; mandatory for a non-zero MSL &mdash; the counters are keyset-linked &mdash; optional for MSL 00, and a preset with a single keyset fills it automatically), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's <strong>CAT (USAT) release</strong> &mdash; the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
<table class="w-full text-sm mb-3 border-collapse">
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody>
+255 -284
View File
@@ -528,8 +528,6 @@
<select id="sp-card-sel" onchange="cardsApply(this.value)" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
<label class="block mt-2 mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
<select id="sp-keyset-sel" onchange="spKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
</div>
<div id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
@@ -650,9 +648,8 @@
<div class="flex gap-2 mb-3">
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">TAR (3 bytes)</label>
<input id="sp-tar" oninput="spInvalidate()" list="sp-tar-list" class="font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="B00001" maxlength="6" value="B00001">
<datalist id="sp-tar-list"></datalist>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="TAR (from the preset)">TAR (from the preset)</label>
<select id="sp-tar" onchange="spTarChanged()" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800"></select>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300">Counter (5 bytes)</label>
@@ -698,10 +695,6 @@
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
<select id="ram-keyset-sel" onchange="ramKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Operation">Operation</label>
<select id="ram-op" onchange="ramOpChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800">
@@ -934,26 +927,17 @@
<option value="" data-l10n="— Select card —">— Select card —</option>
</select>
</div>
<div class="flex-1">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Keyset">Keyset</label>
<select id="tar-keyset-sel" onchange="tarKeysetChanged()" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"></select>
</div>
</div>
<div id="tar-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 text-sm text-amber-700 dark:text-amber-300"></div>
<div class="mb-1 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1">TARs to probe — TS 101 220 V18.3.0 Annex D, Table D.1</span>
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="TARs to probe (the preset's TAR list)">TARs to probe (the preset's TAR list)</span>
<span class="flex gap-1">
<button type="button" onclick="tarProbeSelectAll(true)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select all">Select all</button>
<button type="button" onclick="tarProbeSelectAll(false)" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Select none">Select none</button>
</span>
</div>
<div id="tar-list" class="mb-2 border border-gray-200 dark:border-slate-700 rounded p-2 max-h-64 overflow-y-auto"></div>
<div class="flex gap-2 items-center mb-3">
<input id="tar-custom" class="w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="6" placeholder="AABBCC">
<input id="tar-custom-note" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="description (optional)">
<button type="button" onclick="tarProbeAddCustom()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 whitespace-nowrap" data-l10n="Add TAR">Add TAR</button>
</div>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Probe APDU (hex)">Probe APDU (hex)</label>
<input id="tar-apdu" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono mb-3" value="00A40000023F00">
<button data-needs="card" data-needs-keyset="tar" onclick="tarExecute()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Execute">Execute</button>
@@ -1719,6 +1703,7 @@
<thead><tr>
<th class="w-28 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TAR">TAR</th>
<th class="w-20 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="MSL">MSL</th>
<th class="w-36 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keyset">Keyset</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Description">Description</th>
<th class="w-12 border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
@@ -1907,7 +1892,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.25.8';
const SIMPLE_VERSION = '3.26.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -5032,20 +5017,71 @@ function spPresetTar(role) {
return cardsRoleTar(c, role);
}
// The TAR field's suggestions: the preset's TAR table, each entry labelled
// with its description (or its role) and MSL - free text stays possible.
function spTarListRender(preset) {
const list = document.getElementById('sp-tar-list');
if (!list) return;
// The SCP80 TAR selector (v3.26.0): the preset's TAR table - the role entries
// labelled by their role, the free rows by their description - each with its
// MSL and default keyset. A change applies the TAR's keyset to the form and
// seeds the SPI1 from its MSL.
function spTarOptionsHtml(preset) {
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
list.innerHTML = cardsTarList(preset).map(e => {
const list = cardsTarList(preset);
if (!list.length) return '<option value="">' + esc(t('— no TAR in the preset —')) + '</option>';
return list.map(e => {
const bits = [e.tar || ''];
const label = e.desc || roleLabel[e.role] || '';
const text = label + (e.msl ? (label ? ' ' : '') + '(MSL ' + e.msl + ')' : '');
return '<option value="' + esc(e.tar || '') + '"' +
(text ? ' label="' + esc(text) + '"' : '') + '></option>';
if (label) bits.push(label);
if (e.msl) bits.push('MSL ' + e.msl);
if (e.keyset) bits.push(t('Keyset') + ' ' + e.keyset);
return '<option value="' + esc(e.tar || '') + '">' + esc(bits.join(' \u00b7 ')) + '</option>';
}).join('');
}
// Render the SCP80 TAR select from a preset (the view's current one when not
// given); the previous selection is kept when the new list carries it.
function spTarRender(preset) {
const sel = document.getElementById('sp-tar');
if (!sel) return;
if (preset === undefined) {
const idx = spPresetIdx();
preset = idx >= 0 ? cards[idx] : null;
}
const prev = sel.value;
sel.disabled = !preset;
sel.innerHTML = preset ? spTarOptionsHtml(preset)
: '<option value="">' + esc(t('— select a card preset —')) + '</option>';
if (prev && Array.prototype.some.call(sel.options, o => o.value === prev)) sel.value = prev;
else if (sel.options.length) sel.selectedIndex = 0;
}
// Seed the SPI1 field from a TAR's MSL (the option is added when the selector
// does not carry it - the MSL is the card's Minimum SPI1, TS 102 226
// 8.2.1.3.2.4).
function spSpi1Seed(msl) {
const spi1Sel = document.getElementById('sp-spi1');
if (!spi1Sel || !msl) return;
if (spi1Sel.options && typeof Option === 'function' &&
!Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) {
spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl));
}
spi1Sel.value = msl;
}
// A TAR selection applies its preset entry's default keyset (KIc/KID and the
// keys; the counter override is cleared) and seeds the SPI1 from its MSL.
function spTarChanged() {
const sel = document.getElementById('sp-tar');
const idx = spPresetIdx();
const preset = idx >= 0 ? cards[idx] : null;
const entry = preset ? cardsTarEntry(preset, sel ? sel.value : '') : null;
if (entry) {
const ks = entry.keyset ? spKeysetFor(preset, entry.keyset) : null;
if (ks) spKeysetApply(ks);
if (entry.msl) spSpi1Seed(entry.msl);
}
spInvalidate();
updateSp();
spKeysetSync();
}
function packToSp(sourceId) {
const val = document.getElementById(sourceId).value;
if (!val) return;
@@ -5055,7 +5091,10 @@ function packToSp(sourceId) {
const key = spTarKeyForPack(sourceId);
if (key) {
_spTarKey = key;
document.getElementById('sp-tar').value = spPresetTar(key);
spTarRender();
const sel = document.getElementById('sp-tar');
const tar = spPresetTar(key);
if (sel && Array.prototype.some.call(sel.options, o => o.value === tar)) sel.value = tar;
}
_spRamFormat = sourceId === 'chain-ram-preview' ? 'auto' : null;
}
@@ -10436,8 +10475,10 @@ function scriptsCapFileChanged() {
// Collect SP params from the SP form (populated by ramApplyCard -> cardsApply)
function getRamSpParams(selId) {
// `preset_id` names the server-side preset the operation belongs to: the
// server persists the counter it consumes there (v3.8.0). `selId` picks
// the view's preset selector (RAM by default, the TAR probe its own).
// server persists the counter it consumes there (v3.8.0) and resolves the
// keyset from the preset's ISD TAR entry (v3.26.0 - the RAM keyset
// selector is gone). `selId` picks the view's preset selector (RAM by
// default, the TAR probe its own).
const sel = document.getElementById(selId || 'ram-card-sel');
const idx = sel ? parseInt(sel.value, 10) : NaN;
const preset = (!isNaN(idx) && cards[idx]) ? cards[idx] : null;
@@ -10445,11 +10486,7 @@ function getRamSpParams(selId) {
preset_id: (preset && preset.id) || '',
spi1: document.getElementById('sp-spi1').value,
spi2: document.getElementById('sp-spi2-hex').value,
kic: document.getElementById('sp-kic-hex').value,
kid: document.getElementById('sp-kid-hex').value,
tar: (document.getElementById('sp-tar').value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase().padEnd(6, '0').slice(0, 6),
kicKey: (document.getElementById('sp-kic-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
kidKey: (document.getElementById('sp-kid-key').value || '').replace(/[^0-9a-fA-F]/g, ''),
tar: preset ? cardsRoleTar(preset, 'isd') : '',
ram_format: (document.getElementById('ram-format') || {}).value || 'auto',
};
// Override only when typed; empty = the server seeds the counter from the
@@ -11928,10 +11965,8 @@ async function ramDeleteFromExplorer(aid, withCascade) {
const ksErr = ramKeysetGuard();
if (ksErr) { alert(ksErr); return; }
const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
// v3.26.0: the keyset fields no longer travel - the server resolves the
// keyset (and its keys) from the preset's ISD TAR entry
const label = withCascade ? t('Delete') + ' (' + t('cascade') + ')' : t('Delete');
if (!confirm(label + ' — ' + t('AID:') + ' ' + aid + '?')) return;
const apdu = ramDeleteApdu(aid, withCascade);
@@ -12494,59 +12529,27 @@ async function ramInstallApp(sp, op) {
}
// ===== TAR probe (v3.8.0) =====
// One secured packet per TAR from the preset's keyset, classified by the
// card's ENVELOPE verdict and the application's PoR. The standard TAR list
// lives on the server (TS 101 220 V18.3.0 Annex D, Table D.1) and is used when
// the list field is empty. Each packet the card accepts consumes a counter,
// which the server persists into the keyset (one per accepted TAR).
// One secured packet per TAR of the selected preset, classified by the card's
// ENVELOPE verdict and the application's PoR. v3.26.0: the probe checks the
// preset's TAR list only - each row uses its TAR's MSL and default keyset, and
// every packet the card accepts consumes a counter, which the server persists
// into that keyset (one per accepted TAR).
// TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1) with
// the document each allocation comes from. Kept in sync with the server's
// TAR_PROBE_TARS by a test; custom TARs live in localStorage (best effort).
const TAR_PROBE_TARS = [
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
{ tar: 'B20100', label: 'Issuer Security Domain (expanded)', doc: 'TS 102 226' },
{ tar: 'B00000', label: 'UICC shared file system RFM (compact)', doc: 'TS 102 226' },
{ tar: 'B00001', label: 'ADF RFM (compact)', doc: 'TS 131 116' },
{ tar: 'B00010', label: 'SIM file system RFM (compact)', doc: 'TS 131 116' },
{ tar: 'B00120', label: 'UICC shared file system RFM (expanded)', doc: 'TS 102 226' },
{ tar: 'B00130', label: 'SIM file system RFM (expanded)', doc: 'TS 131 116' },
{ tar: 'B00140', label: 'ADF RFM (expanded)', doc: 'TS 131 116' },
{ tar: 'B20000', label: 'USAT interpreter', doc: 'TS 131 114' },
{ tar: 'B20200', label: 'Multiplexing application', doc: 'TS 102 225' },
{ tar: 'B20201', label: 'Controlling Authority Security Domain', doc: 'GP Amendment A' },
{ tar: 'B20202', label: 'OMA BCAST audience measurement', doc: 'OMA' },
{ tar: 'B20203', label: 'OMA DM LWM2M application', doc: 'OMA' },
{ tar: 'B00200', label: 'RFU (control)', doc: '' },
];
const TAR_PROBE_STORAGE = 'simple_tar_probe';
// The custom TARs added by the user (best effort: losing them is harmless).
function tarProbeCustomLoad() {
try {
const d = JSON.parse(localStorage.getItem(TAR_PROBE_STORAGE) || '[]');
if (!Array.isArray(d)) return [];
return d.filter(e => e && /^[0-9A-F]{6}$/.test(String(e.tar || '').toUpperCase()))
.map(e => ({ tar: String(e.tar).toUpperCase(), label: String(e.label || ''), custom: true }));
} catch (e) {
return [];
}
}
function tarProbeCustomSave(list) {
try {
localStorage.setItem(TAR_PROBE_STORAGE, JSON.stringify(
(list || []).map(e => ({ tar: e.tar, label: e.label || '' }))));
} catch (e) { /* best effort */ }
}
// The standard rows plus the custom ones.
// The probe's rows: the selected preset's TAR list (the role entries labelled
// by their role, the free rows by their description; the tooltip carries the
// MSL and the default keyset). There is no custom-TAR input anymore.
function tarProbeAllRows() {
return TAR_PROBE_TARS.concat(tarProbeCustomLoad());
const idx = tarPresetIdx();
const preset = idx >= 0 ? cards[idx] : null;
const roleLabel = { isd: 'ISD', uiccRfm: 'UICC RFM', usimRfm: 'ADF RFM' };
return cardsTarList(preset).map(e => ({
tar: String(e.tar || '').toUpperCase(),
label: e.desc || roleLabel[e.role] || '',
doc: e.msl ? 'MSL ' + e.msl + (e.keyset ? ' \u00b7 ' + t('Keyset') + ' ' + e.keyset : '') : '',
}));
}
// The checklist rows (pure): a checkbox per TAR, checked by default, the spec
// document in the tooltip and a remove button on custom rows.
// The checklist rows (pure): a checkbox per TAR, checked by default.
function tarProbeRowsHtml(rows) {
return (rows || []).map(r => {
const title = r.doc ? ' title="' + esc(r.doc) + '"' : '';
@@ -12554,9 +12557,6 @@ function tarProbeRowsHtml(rows) {
'<input type="checkbox" class="rounded border-gray-300 dark:border-slate-600" value="' + esc(r.tar) + '" checked>' +
'<span class="font-mono text-xs">' + esc(r.tar) + '</span>' +
'<span class="text-xs text-gray-500 dark:text-slate-400">' + esc(r.label || '') + '</span>' +
(r.custom
? '<button type="button" onclick="tarProbeRemoveCustom(\'' + esc(r.tar) + '\')" class="ml-auto px-1.5 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-red-100 dark:hover:bg-red-900/40" title="' + esc(t('Remove TAR')) + '">\u2715</button>'
: '') +
'</label>';
}).join('');
}
@@ -12570,34 +12570,20 @@ function tarProbeChecked() {
function tarProbeRender() {
const box = document.getElementById('tar-list');
if (box) box.innerHTML = tarProbeRowsHtml(tarProbeAllRows());
if (!box) return;
const rows = tarProbeAllRows();
if (!rows.length) {
box.innerHTML = '<span class="text-xs text-gray-400 dark:text-slate-500">' +
esc(t('Select a card preset with TARs first')) + '</span>';
return;
}
box.innerHTML = tarProbeRowsHtml(rows);
}
function tarProbeSelectAll(on) {
document.querySelectorAll('#tar-list input[type="checkbox"]').forEach(cb => { cb.checked = !!on; });
}
function tarProbeAddCustom() {
const el = document.getElementById('tar-custom');
const noteEl = document.getElementById('tar-custom-note');
const tar = (el && el.value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (!/^[0-9A-F]{6}$/.test(tar)) { alert(t('A TAR is three hex bytes, e.g. B00001')); return; }
const custom = tarProbeCustomLoad();
if (!custom.some(e => e.tar === tar)) {
custom.push({ tar: tar, label: (noteEl && noteEl.value || '').trim(), custom: true });
tarProbeCustomSave(custom);
}
if (el) el.value = '';
if (noteEl) noteEl.value = '';
tarProbeRender();
}
function tarProbeRemoveCustom(tar) {
const want = String(tar || '').toUpperCase();
tarProbeCustomSave(tarProbeCustomLoad().filter(e => e.tar !== want));
tarProbeRender();
}
function tarProbeVerdictLabel(verdict) {
return { registered: 'registered', no_answer: 'accepted, no answer',
no_por: 'accepted, no PoR', refused: 'refused' }[verdict] || verdict;
@@ -12694,46 +12680,29 @@ function tarPresetIdx() {
}
function tarApplyCard(idx) {
// copy the selected preset into the shared SP form fields (the probe reads
// kic/kid/keys/counter from there, like the RAM view does)
// v3.26.0: the probe reads its rows from the selected preset (each TAR
// carries its own keyset); the shared SP form fields are only the SPI1
// fallback - the ISD stays the form default for the other views.
const i = parseInt(idx, 10);
if (!isNaN(i) && cards[i]) _ramCardIdx = i;
_spTarKey = 'isd'; // the probe's checklist TARs are per-row; the ISD is the form default
_spTarKey = 'isd';
cardsApply(idx);
tarProbeRender();
spPresetWarningRender();
}
function tarKeysetChanged() {
const sel = document.getElementById('tar-keyset-sel');
if (!sel) return;
const kvn = parseInt(sel.value, 10);
const idx = tarPresetIdx();
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
if (ks) spKeysetApply(ks);
spKeysetSync();
}
function tarKeysetGuard() {
const idx = tarPresetIdx();
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
}
async function tarExecute() {
const tars = tarProbeChecked();
if (!tars.length) { alert(t('Select at least one TAR')); return; }
const cardIdx = parseInt(document.getElementById('tar-card-sel').value, 10);
if (!isNaN(cardIdx) && cards[cardIdx]) _ramCardIdx = cardIdx;
spRefreshFromPreset('tar-card-sel');
const ksErr = tarKeysetGuard();
if (ksErr) { alert(ksErr); return; }
const sp = getRamSpParams('tar-card-sel');
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
await tarProbe(sp, tars);
const idx = tarPresetIdx();
const preset = idx >= 0 ? cards[idx] : null;
if (!preset) { alert(t('Select a card preset')); return; }
// v3.26.0: the probe checks the preset's TARs and resolves each row's
// keyset server-side; only the preset and the (fallback) SPI1 travel.
const spi1 = (document.getElementById('sp-spi1') || {}).value || '';
await tarProbe({ preset_id: preset.id, spi1: spi1 }, tars);
}
// Measured NV footprint of an install operation: the server reads GET DATA
@@ -12838,21 +12807,14 @@ async function ramSyncCounter(presetIdx, view) {
const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null;
const box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram');
if (!preset) { alert(t('No card preset selected')); return; }
// The keyset the failed operation used: the originating view's selector
// first, the other view's as the fallback (both hold the keyset number as
// their option value).
const kvnOf = id => {
const sel = document.getElementById(id);
const v = sel && sel.value !== '' ? parseInt(sel.value, 10) : NaN;
return Number.isInteger(v) && v > 0 ? v : null;
};
const own = view === 'sp' ? 'sp-keyset-sel' : 'ram-keyset-sel';
const other = own === 'ram-keyset-sel' ? 'sp-keyset-sel' : 'ram-keyset-sel';
const val = id => (document.getElementById(id) || {}).value || '';
const body = { preset_id: preset.id };
const kvn = kvnOf(own) || kvnOf(other);
if (kvn) body.kvn = kvn;
const tar = val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
// v3.26.0: the keyset follows the operation's TAR - the RAM operations are
// addressed to the ISD role entry, a plain send to its selected TAR (the
// server resolves it; an explicit `kvn` override stays possible through
// the API).
const tar = view === 'ram' ? cardsRoleTar(preset, 'isd')
: val('sp-tar').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
if (tar) body.tar = tar;
if (val('sp-spi1')) body.spi1 = val('sp-spi1');
// The probe needs a PoR: use the transport the failed operation used - the
@@ -13011,10 +12973,8 @@ async function ramExecute() {
// changed in the Cards tab after the RAM form was filled.
spRefreshFromPreset('ram-card-sel');
const sp = getRamSpParams();
if (!sp.kicKey || !sp.kidKey) {
alert(t('Select a card preset with keys first (RAM subtab → Card preset)'));
return;
}
// v3.26.0: the keyset fields no longer travel - the server resolves the
// keyset (and its keys) from the preset's ISD TAR entry
try {
if (op === 'explore') await ramExplore(sp);
else if (op === 'install-cap') await ramInstallCap(sp);
@@ -13543,28 +13503,6 @@ function spKeysetCheck(preset, kicHex, kidHex) {
return '';
}
// The <option> list of a keyset selector: the preset's keysets, plus the key
// version currently in the form when the preset does not define it.
function spKeysetOptionsHtml(preset, kicHex, kidHex) {
const kvn = spKeysetKvnOf(kicHex) || spKeysetKvnOf(kidHex) || 0;
let html = '';
let found = false;
spKeysetList(preset).forEach(ks => {
const v = spKeysetKvnOf(ks.kic);
if (v === null) return;
if (v === kvn) found = true;
html += '<option value="' + v + '"' + (v === kvn ? ' selected' : '') + '>' +
'Keyset ' + v + ' \u2014 ' + esc(ks.kic || '??') + '/' + esc(ks.kid || '??') +
' \u2014 ' + esc(t('counter')) + ' ' + esc(ks.cntr || '') + '</option>';
});
if (kvn && !found) {
html += '<option value="' + kvn + '" selected>Keyset ' + kvn + ' \u2014 ' +
esc(t('not defined in the preset')) + '</option>';
}
if (!html) html = '<option value="">' + esc(t('\u2014 no keyset \u2014')) + '</option>';
return html;
}
// ===== Keyset editor (Cards form) =====
let _cardsKeysetCount = 0;
@@ -13595,12 +13533,20 @@ function cardsKeysetRowsRender(keysets) {
function cardsKeysetAdd() {
cardsKeysetRowsRender(cardsKeysetsFromForm().concat([
{ kic: '', kid: '', kicKey: '', kidKey: '', cntr: '' }]));
cardsTarKeysetRefresh();
}
function cardsKeysetRemove(i) {
const rows = cardsKeysetsFromForm();
rows.splice(i, 1);
cardsKeysetRowsRender(rows);
cardsTarKeysetRefresh();
}
// Refresh the TAR table's keyset selects after the keyset table changed (the
// TAR rows' own values are read from the DOM first, so nothing is lost).
function cardsTarKeysetRefresh() {
cardsTarRowsRender(cardsTarsFromTable());
}
function cardsKeysetKvnUpdate(i) {
@@ -13614,6 +13560,9 @@ function cardsKeysetKvnUpdate(i) {
el.classList.toggle('text-red-500', !!bad);
el.title = bad ? t('KIc and KID must carry the same keyset number (TS 102 225 A.2)')
: t('Keyset number (b8..b5 of KIc/KID)');
// The TAR table's keyset selects follow the numbers as they are typed
// (v3.26.0) - a re-render keeps every field's value (read from the DOM).
cardsTarKeysetRefresh();
}
// The keyset rows as preset entries (empty rows are dropped).
@@ -13648,11 +13597,42 @@ const CARDS_TAR_ROLE_DESC = {
usimRfm: 'ADF FS RFM compact format',
};
// The keyset cell (v3.26.0): a select of the form's current keysets, the only
// one preselected when the form carries exactly one, and an amber marker when
// a TAR whose MSL requires security has none (the server refuses to save such
// a TAR; the marker shows it before the save).
function cardsTarKeysetCell(e, i) {
const keysets = cardsKeysetsFromForm();
const kvn = e.keyset ? Number(e.keyset) : null;
const msl = String(e.msl || '').toUpperCase();
const auto = (!kvn && keysets.length === 1) ? spKeysetKvnOf(keysets[0].kic) : null;
const sel = kvn || auto;
const warn = !sel && msl && msl !== '00';
let html = '<td class="py-1 px-2"><div class="flex items-center gap-1">';
html += '<select id="cards-tr-' + i + '-keyset" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" title="' + esc(t('The keyset packets to this TAR use - a non-zero MSL requires one')) + '">' +
'<option value=""' + (sel ? '' : ' selected') + '>' + esc(t('— none —')) + '</option>';
keysets.forEach(ks => {
const v = spKeysetKvnOf(ks.kic);
if (v === null) return;
html += '<option value="' + v + '"' + (v === sel ? ' selected' : '') + '>' +
esc(t('Keyset') + ' ' + v + ' \u2014 ' + (ks.kic || '??') + '/' + (ks.kid || '??')) + '</option>';
});
html += '</select>';
if (warn) {
html += '<span class="text-amber-500" title="' +
esc(t('This TAR needs a default keyset (its MSL requires security) - set it in the Cards tab')) +
'">\u26a0</span>';
}
html += '</div></td>';
return html;
}
function cardsTarRowHtml(e, i) {
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
const role = CARDS_TAR_ROLES[i] || '';
let cells = '<td class="py-1 px-2"><input id="cards-tr-' + i + '-tar" class="w-full ' + cls + '" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '"></td>' +
'<td class="py-1 px-2"><input id="cards-tr-' + i + '-msl" class="w-full ' + cls + '" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>';
'<td class="py-1 px-2"><input id="cards-tr-' + i + '-msl" class="w-full ' + cls + '" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>' +
cardsTarKeysetCell(e, i);
if (role) {
cells += '<td class="py-1 px-2 text-xs text-gray-600 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '</td>' +
'<td class="py-1 px-2"></td>';
@@ -13673,7 +13653,7 @@ function cardsTarRowsRender(entries) {
const list = entries || [];
const roles = CARDS_TAR_ROLES.map(role => {
const e = list.find(x => x.role === role) || {};
return { tar: e.tar || '', msl: e.msl || '' };
return { tar: e.tar || '', msl: e.msl || '', keyset: e.keyset || null };
});
const free = list.filter(e => !e.role);
_cardsTarCount = free.length;
@@ -13682,7 +13662,7 @@ function cardsTarRowsRender(entries) {
}
function cardsTarAdd() {
cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '' }]));
cardsTarRowsRender(cardsTarsFromTable().concat([{ tar: '', msl: '', desc: '', keyset: null }]));
}
// `i` is the table row index; the role rows are fixed and never removed.
@@ -13703,13 +13683,21 @@ function cardsFreeTarsFromForm() {
return el ? String(el.value).trim() : '';
};
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc') };
if (!e.tar && !e.msl && !e.desc) continue;
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: get('desc'),
keyset: cardsTarKeysetFromForm(idx) };
if (!e.tar && !e.msl && !e.desc && !e.keyset) continue;
out.push(e);
}
return out;
}
// The keyset select of a TAR row as a number (null for none).
function cardsTarKeysetFromForm(idx) {
const el = document.getElementById('cards-tr-' + idx + '-keyset');
const v = el && el.value ? parseInt(el.value, 10) : NaN;
return (Number.isInteger(v) && v > 0) ? v : null;
}
// The TAR table exactly as displayed: the role rows as typed (an emptied field
// stays empty) plus the free rows - the re-render source for Add/Remove, so an
// emptied field is not silently refilled with its default.
@@ -13721,7 +13709,8 @@ function cardsTarsFromTable() {
};
return { role: role,
tar: get('tar').replace(/\s/g, '').toUpperCase(),
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '' };
msl: get('msl').replace(/\s/g, '').toUpperCase(), desc: '',
keyset: cardsTarKeysetFromForm(i) };
});
return out.concat(cardsFreeTarsFromForm());
}
@@ -13732,45 +13721,24 @@ function cardsTarsFromTable() {
function cardsTarsFromForm() {
return cardsTarsFromTable().map(e => e.role
? { role: e.role, tar: e.tar || cardsRoleDefault(e.role),
msl: e.msl || CARDS_MSL_DEFAULT, desc: '' }
msl: e.msl || CARDS_MSL_DEFAULT, desc: '', keyset: e.keyset || null }
: e);
}
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
// rows.
// The role rows back to their pre-settled defaults (TAR + MSL 16, no keyset)
// and no free rows.
function cardsTarsReset() {
cardsTarRowsRender(CARDS_TAR_ROLES.map(role => ({
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '' })));
role: role, tar: cardsRoleDefault(role), msl: CARDS_MSL_DEFAULT, desc: '',
keyset: null })));
}
// ===== Keyset selectors (SCP80 / RAM) =====
// The selector follows the KIc/KID bytes: a keyset of the selected preset that
// matches their keyset number is selected; a number the preset does not define
// shows as "not defined" (and the packet actions refuse it).
// ===== Keyset resolution (SCP80 / RAM / TAR probe) =====
// v3.26.0: the keyset selectors are gone - each TAR carries its default keyset
// in the preset, the SCP80 TAR select applies it to the form and the RAM
// operations use the ISD role entry (the server resolves the same rule). Only
// the start buttons' availability refresh remains here.
function spKeysetSync() {
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
// A keyset only exists inside a card preset: with no preset selected the
// selector is disabled and cleared, so a stale "not defined in the preset"
// entry can never linger.
const sync = (selId, idxFn) => {
const sel = document.getElementById(selId);
if (!sel) return;
const idx = idxFn();
const preset = idx >= 0 ? cards[idx] : null;
if (!preset) {
sel.disabled = true;
sel.innerHTML = '<option value="">' + esc(t('\u2014 select a card preset \u2014')) + '</option>';
return;
}
sel.disabled = false;
sel.innerHTML = spKeysetOptionsHtml(preset, kic, kid);
};
sync('sp-keyset-sel', spPresetIdx);
sync('ram-keyset-sel', ramPresetIdx);
sync('tar-keyset-sel', tarPresetIdx);
// The start buttons follow the same state: they wait for a preset and a
// keyset defined in it.
pysimApplyAvailability();
}
@@ -13797,26 +13765,6 @@ function spKeysetApply(ks) {
updateSp();
}
function spKeysetChanged() {
const sel = document.getElementById('sp-keyset-sel');
if (!sel) return;
const kvn = parseInt(sel.value, 10);
const idx = spPresetIdx();
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
if (ks) spKeysetApply(ks);
spKeysetSync();
}
function ramKeysetChanged() {
const sel = document.getElementById('ram-keyset-sel');
if (!sel) return;
const kvn = parseInt(sel.value, 10);
const idx = ramPresetIdx();
const ks = (idx >= 0 && !isNaN(kvn)) ? spKeysetFor(cards[idx], kvn) : null;
if (ks) spKeysetApply(ks);
spKeysetSync();
}
// A warning for the SCP80/RAM views when the equipped card and the selected
// preset disagree (pure). Autoselection keeps them in step, so this covers
// what it cannot fix: a card with no preset at all, or a manually selected
@@ -13869,30 +13817,64 @@ function ramPresetIdx() {
// '' when the SCP80 form may build a packet, else the reason. With a preset
// selected the keyset number must be one of its keysets; without one only the
// byte rules apply (hand-entered keys stay possible, a workbench flow).
// The click-time guard (v3.26.0): the operation's TAR must have a usable
// default keyset in the preset - a TAR whose MSL requires security needs one
// (the counters are keyset-linked; MSL 00 may go keyless) - and the form's
// KIc/KID must stay consistent with it (the fields remain hand-editable).
function spTarKeysetGuard(preset, tar) {
if (!preset) return '';
const entry = cardsTarEntry(preset, tar);
if (!entry) return '';
if (entry.keyset) {
if (!spKeysetFor(preset, entry.keyset)) {
return t('Keyset') + ' ' + entry.keyset + ' ' + t('is not defined in the preset');
}
return '';
}
const msl = String(entry.msl || '');
if (msl && msl !== '00') {
return t('TAR') + ' ' + entry.tar + ': ' +
t('needs a default keyset (its MSL requires security) - set it in the Cards tab');
}
return '';
}
function spKeysetGuard() {
const idx = spPresetIdx();
if (idx < 0) return '';
const preset = cards[idx];
const sel = document.getElementById('sp-tar');
const tarErr = spTarKeysetGuard(preset, sel ? sel.value : '');
if (tarErr) return tarErr;
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
return spKeysetCheck(preset, kic, kid);
}
function ramKeysetGuard() {
const idx = ramPresetIdx();
if (idx < 0) return '';
const preset = cards[idx];
// The RAM operations are addressed to the ISD TAR (v3.26.0)
const tarErr = spTarKeysetGuard(preset, cardsRoleTar(preset, 'isd'));
if (tarErr) return tarErr;
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(idx >= 0 ? cards[idx] : null, kic, kid);
return spKeysetCheck(preset, kic, kid);
}
// Whether a view's start button may run: its preset must be selected and the
// form's keyset number one of that preset's keysets (the click-time guards keep
// the byte-level checks). '00'/'00' (no security) with a preset selected stays
// allowed - it is a deliberate form choice.
// operation TAR's keyset usable (the click-time guards keep the byte-level
// checks). '00'/'00' (no security) with a preset selected stays allowed - it
// is a deliberate form choice.
function spKeysetStartReady(view) {
const idx = view === 'ram' ? ramPresetIdx() : (view === 'tar' ? tarPresetIdx() : spPresetIdx());
if (idx < 0) return false;
const kic = (document.getElementById('sp-kic-hex') || {}).value;
const kid = (document.getElementById('sp-kid-hex') || {}).value;
return spKeysetCheck(cards[idx], kic, kid) === '';
if (view === 'ram') {
if (ramPresetIdx() < 0) return false;
return ramKeysetGuard() === '';
}
if (view === 'tar') return tarPresetIdx() >= 0;
if (spPresetIdx() < 0) return false;
return spKeysetGuard() === '';
}
function cardsFormValues() {
@@ -14257,40 +14239,22 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
const c = cards[parseInt(idx)];
if (!c) return false;
// The TAR of the current operation (ISD for RAM/GP, the RFM TAR for the RFM
// chains) and its MSL: a preset apply seeds the SPI1 from the MSL - the
// card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4). keepTar leaves the packet
// TAR alone (the plain send may target a manually entered TAR): its SPI1 is
// the operator's value too.
// chains) and its MSL + default keyset (v3.26.0): a preset apply selects
// the TAR in the selector and applies its keyset to the form, seeding the
// SPI1 from its MSL - the card's Minimum SPI1 (TS 102 226 8.2.1.3.2.4).
// keepTar leaves the packet TAR (and its keyset) alone: the plain send
// keeps the operator's selection.
spTarRender(c);
if (!keepTar) {
const roleTar = cardsRoleTar(c, _spTarKey);
document.getElementById('sp-tar').value = roleTar;
const msl = cardsTarMsl(c, roleTar) || CARDS_MSL_DEFAULT;
const spi1Sel = document.getElementById('sp-spi1');
if (spi1Sel) {
if (spi1Sel.options && typeof Option === 'function' &&
!Array.prototype.some.call(spi1Sel.options, o => o.value === msl)) {
spi1Sel.appendChild(new Option(msl + ' \u2014 MSL', msl));
}
spi1Sel.value = msl;
const sel = document.getElementById('sp-tar');
if (sel && Array.prototype.some.call(sel.options, o => o.value === roleTar)) {
sel.value = roleTar;
}
}
// The keyset: keep the keyset number the form carries when this preset
// defines it, otherwise start from the preset's first keyset.
const kicNow = (document.getElementById('sp-kic-hex') || {}).value;
const kidNow = (document.getElementById('sp-kid-hex') || {}).value;
const kvnNow = spKeysetKvnOf(kicNow) || spKeysetKvnOf(kidNow) || 0;
const ks = (kvnNow && spKeysetFor(c, kvnNow)) || spKeysetList(c)[0] || null;
if (ks) {
const kicByte = parseInt(ks.kic, 16);
document.getElementById('sp-kic-idx').value = ((kicByte >> 4) & 0x0F).toString(16).toUpperCase();
document.getElementById('sp-kic-alg').value = (kicByte & 0x0F).toString(16).padStart(2, '0');
updateSpKic();
const kidByte = parseInt(ks.kid, 16);
document.getElementById('sp-kid-idx').value = ((kidByte >> 4) & 0x0F).toString(16).toUpperCase();
document.getElementById('sp-kid-alg').value = (kidByte & 0x0F).toString(16).padStart(2, '0');
updateSpKid();
document.getElementById('sp-kic-key').value = ks.kicKey || '';
document.getElementById('sp-kid-key').value = ks.kidKey || '';
const entry = cardsTarEntry(c, sel ? sel.value : roleTar);
spSpi1Seed((entry && entry.msl) || CARDS_MSL_DEFAULT);
const ks = (entry && entry.keyset) ? spKeysetFor(c, entry.keyset) : null;
if (ks) spKeysetApply(ks);
}
// The counter field is an override: applying a preset clears it (empty =
// auto, the server seeds from the store); the pre-send refresh keeps a
@@ -14301,8 +14265,6 @@ function cardsApplyFields(idx, keepTar, keepCntr) {
if (cntrEl) cntrEl.value = '';
}
spCntrPlaceholder();
if (!keepTar) document.getElementById('sp-tar').value = cardsRoleTar(c, _spTarKey);
spTarListRender(c);
spInvalidate();
updateSp();
spKeysetSync();
@@ -14317,7 +14279,7 @@ function cardsApply(idx, keepTar, keepCntr) {
// warning must not outlive the selection it described.
spKeysetSync();
spPresetWarningRender();
spTarListRender(null);
spTarRender(null);
return;
}
_genSpBuild();
@@ -23972,6 +23934,15 @@ const LANG_RU = {
'TARs (MSL)': 'TAR (MSL)',
'Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.': 'У каждого TAR есть MSL (минимальный SPI1) — карта проверяет его до обработки защиты (TS 102 226 8.2.1.3.2.4); из него выводится SPI1 пакета.',
'More TARs — each with its MSL and an optional description': 'Дополнительные TAR — у каждого свой MSL и необязательное описание',
'TAR (from the preset)': 'TAR (из пресета)',
'TARs to probe (the preset\'s TAR list)': 'TAR для проверки (список TAR пресета)',
'— no TAR in the preset —': '— в пресете нет TAR —',
'— none —': '— нет —',
'Select a card preset': 'Выберите пресет карты',
'Select a card preset with TARs first': 'Сначала выберите пресет карты с TAR',
'The keyset packets to this TAR use - a non-zero MSL requires one': 'Keyset, который используют пакеты к этому TAR — при MSL не 00 он обязателен',
'This TAR needs a default keyset (its MSL requires security) - set it in the Cards tab': 'Этому TAR нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»',
'needs a default keyset (its MSL requires security) - set it in the Cards tab': 'нужен keyset по умолчанию (его MSL требует защиты) — задайте его на вкладке «Карты»',
'Add TAR': 'Добавить TAR',
'Remove TAR': 'Удалить TAR',
'Description (optional)': 'Описание (необязательно)',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v433';
const CACHE = 'simple-v434';
const URLS = [
'index.html',
'help.html',
+23 -8
View File
@@ -34,12 +34,12 @@ for (const fn of ['cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrEffective', 'spCntrAhead',
'spCntrAdjust',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'spTarListRender',
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard',
'ramKeysetGuard',
'esc',
]) {
code += extractFunc(html, fn) + '\n';
}
@@ -61,9 +61,22 @@ globalThis.t = s => s;
function fakeEnv(selValue, selId, presetCntr) {
const els = {};
for (const id of ['sp-spi1', 'sp-spi2-hex', 'sp-kic-idx', 'sp-kic-alg',
'sp-kid-idx', 'sp-kid-alg', 'sp-tar', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
'sp-kid-idx', 'sp-kid-alg', 'sp-cntr', 'sp-kic-key', 'sp-kid-key']) {
els[id] = { value: '' };
}
// the TAR is a select (v3.26.0): the stub models the surface the renderer
// touches - setting innerHTML parses the option values and selectedIndex
// moves the value, like a real <select>
els['sp-tar'] = {
value: '', disabled: false, options: [], _sel: 0, _html: '',
set innerHTML(v) {
this._html = v;
this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] }));
},
get innerHTML() { return this._html; },
set selectedIndex(i) { this._sel = i; if (this.options[i]) this.value = this.options[i].value; },
get selectedIndex() { return this._sel; },
};
// the combined key bytes the server reads (updateSpKic maintains them in
// the real form): the fake preset's keyset resolves to kvn 1
els['sp-kic-hex'] = { value: '15' };
@@ -114,14 +127,16 @@ test('spRefreshFromPreset for the RAM selector targets the ISD TAR', () => {
assert.strictEqual(env.els['sp-tar'].value, '000000');
});
test('the pre-send refresh keeps a hand-edited packet TAR (v3.6.21)', () => {
test('the pre-send refresh keeps the selected packet TAR', () => {
// v3.26.0: the TAR is a preset selector - the pre-send refresh must keep
// the operator's selection (a TAR outside the preset is not offered)
const env = fakeEnv('0', 'sp-card-sel', '0000000020');
env.els['sp-tar'].value = 'AF4D01'; // e.g. a push/link trigger target
env.els['sp-tar'].value = 'B00000'; // the UICC RFM entry of the preset
assert.strictEqual(spRefreshFromPreset('sp-card-sel', true), '0000000020');
assert.strictEqual(env.els['sp-tar'].value, 'AF4D01', 'the typed TAR must survive the send');
assert.strictEqual(env.els['sp-tar'].value, 'B00000', 'the selected TAR must survive the send');
assert.strictEqual(env.els['sp-cntr'].value, '', 'the counter field stays an override');
assert.match(env.els['sp-cntr'].placeholder, /0000000020/);
// an explicit preset apply (or a pack) still sets the TAR
// an explicit preset apply (or a pack) selects the operation's TAR
cardsApplyFields(0);
assert.strictEqual(env.els['sp-tar'].value, 'B00000');
});
+78 -37
View File
@@ -35,13 +35,13 @@ for (const fn of ['cardsFormValues', 'cardsClearForm', 'cardsEdit',
'spCntrAhead', 'spCntrLocalSync', 'ramSaveCntr',
'cardsApplyFields', 'cardsApply', 'ramApplyCard',
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
'spTarKeyForPack', 'spPresetTar', 'spTarListRender', 'packToSp',
'spTarKeyForPack', 'spPresetTar', 'packToSp',
'esc', 'escHtml',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
'ramKeysetChanged', 'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'ramPresetIdx', 'tarPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'spMslWarningText',
'cardsTarRowHtml', 'cardsTarRowsRender', 'cardsTarAdd', 'cardsTarRemove',
@@ -73,12 +73,11 @@ const CARD_IDS = ['cards-name','cards-iccid','cards-release','cards-pin1','cards
// the TAR table rows: 0-2 are the fixed role rows, 3+ the free rows
'cards-tr-0-tar','cards-tr-0-msl','cards-tr-1-tar','cards-tr-1-msl',
'cards-tr-2-tar','cards-tr-2-msl',
'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc',
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc'];
'cards-tr-3-tar','cards-tr-3-msl','cards-tr-3-desc','cards-tr-3-keyset',
'cards-tr-4-tar','cards-tr-4-msl','cards-tr-4-desc','cards-tr-4-keyset'];
const SP_IDS = ['sp-card-sel','sp-tar','sp-spi1','sp-spi2','sp-spi2-sm','sp-spi2-hex',
'sp-kic-idx','sp-kic-alg','sp-kic-hex','sp-kid-idx','sp-kid-alg','sp-kid-hex',
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex','sp-tar-list',
'sp-keyset-sel','ram-keyset-sel'];
'sp-cntr','sp-kic-key','sp-kid-key','sp-apdu','sp-result','sp-spi1-hex'];
const CHAIN_IDS = ['chain-sim-preview','chain-usim-preview','chain-ram-preview',
'ber-result','hota-preview'];
@@ -91,10 +90,10 @@ function ksRow(els, i, ks) {
}
function tarRow(els, i, e) {
for (const field of ['tar', 'msl', 'desc']) {
for (const field of ['tar', 'msl', 'desc', 'keyset']) {
const id = 'cards-tr-' + i + '-' + field;
if (!els[id]) els[id] = fakeEl();
els[id].value = e[field] || '';
els[id].value = (e[field] === undefined || e[field] === null) ? '' : String(e[field]);
}
}
@@ -108,6 +107,34 @@ function presetFixture(extra) {
}, extra || {});
}
// A <select>-like fake (the TAR selector): setting innerHTML parses the option
// values (and the selected one) and selectedIndex moves the value, like a real
// element. The accessors need defineProperties - Object.assign would flatten
// them into data properties.
function selectEl() {
const el = fakeEl();
el.options = [];
el._sel = 0;
el._html = '';
el.disabled = false;
Object.defineProperties(el, {
innerHTML: {
get() { return this._html; },
set(v) {
this._html = v;
this.options = Array.from(v.matchAll(/value="([^"]*)"/g), m => ({ value: m[1] }));
const sel = v.match(/<option value="([^"]*)" selected/);
if (sel) this.value = sel[1];
},
},
selectedIndex: {
get() { return this._sel; },
set(i) { this._sel = i; if (this.options[i]) this.value = this.options[i].value; },
},
});
return el;
}
function fakeEl() {
const classes = new Set();
return {
@@ -134,6 +161,7 @@ let apiCalls = [];
function setup() {
const els = {};
for (const id of CARD_IDS.concat(SP_IDS, CHAIN_IDS)) els[id] = fakeEl();
els['sp-tar'] = selectEl(); // the TAR selector (v3.26.0)
globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] };
globalThis.cards = [];
globalThis._cardsEditIdx = null;
@@ -257,9 +285,9 @@ test('clearing the card form keeps the role TAR/MSL defaults', () => {
assert.match(els['cards-tars'].innerHTML, /cards-tr-2-tar"[^>]*value="B00001"/);
// ... and the table reads back as the three role entries
assert.deepStrictEqual(cardsTarsFromForm(), [
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: null },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '', keyset: null },
]);
});
@@ -312,10 +340,10 @@ test('the form values carry the codes and the TAR table', () => {
_cardsTarCount = 1;
const v = cardsFormValues();
assert.deepStrictEqual(v.tars, [
{ role: 'isd', tar: '000001', msl: '0A', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '' },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' },
{ role: 'isd', tar: '000001', msl: '0A', desc: '', keyset: null },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: null },
{ role: 'usimRfm', tar: 'B00003', msl: '1A', desc: '', keyset: null },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet', keyset: null },
]);
assert.strictEqual(v.pin1, '1234');
assert.strictEqual(v.puk1, '12345678');
@@ -342,6 +370,11 @@ test('saving a preset posts the form with the pre-settled TAR table', async () =
els['cards-name'].value = 'New card';
ksRow(els, 0, { kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' });
globalThis._cardsKeysetCount = 1;
// the editor's render preselects the only keyset on every TAR row
// (v3.26.0); the fake DOM is static, so the test does it explicitly
tarRow(els, 0, { keyset: 1 });
tarRow(els, 1, { keyset: 1 });
tarRow(els, 2, { keyset: 1 });
// the role fields are pre-settled by the markup; the test's fake elements
// start empty - the defaults fill in
els['cards-adm'].value = 'aa bb';
@@ -352,9 +385,9 @@ test('saving a preset posts the form with the pre-settled TAR table', async () =
assert.deepStrictEqual(apiCalls[0].body.keysets, [
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }]);
assert.deepStrictEqual(apiCalls[0].body.tars, [
{ role: 'isd', tar: '000000', msl: '16', desc: '' },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '' },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '' },
{ role: 'isd', tar: '000000', msl: '16', desc: '', keyset: 1 },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', desc: '', keyset: 1 },
{ role: 'usimRfm', tar: 'B00001', msl: '16', desc: '', keyset: 1 },
]);
assert.strictEqual(apiCalls[0].body.adm, 'AABB');
});
@@ -457,11 +490,14 @@ test('applying a preset uses the TAR of the current operation and seeds the SPI1
cardsApply(0);
assert.strictEqual(els['sp-tar'].value, 'B0000D');
assert.strictEqual(els['sp-spi1'].value, '1A');
// an empty stored TAR -> spec default
// a preset without TAR entries (hand-made; the store fills the three
// roles): the selector has no options and the SPI1 falls back to the
// default MSL
globalThis.cards = [{ name: 'D', keysets: [], tars: [] }];
globalThis._spTarKey = 'isd';
cardsApply(0);
assert.strictEqual(els['sp-tar'].value, '000000');
assert.strictEqual(els['sp-tar'].value, '');
assert.match(els['sp-tar'].innerHTML, /no TAR in the preset/);
assert.strictEqual(els['sp-spi1'].value, '16');
});
@@ -494,15 +530,18 @@ test('spPresetTar reads the selected preset and falls back to the spec defaults'
assert.strictEqual(spPresetTar('uiccRfm'), 'B00000');
});
test('the TAR field suggestions come from the preset table', () => {
test('the TAR selector lists the preset table', () => {
// v3.26.0: the TAR is a select of the preset's entries (role label or
// description, MSL and default keyset in the option text)
const els = setup();
spTarListRender(presetFixture({ tars: [
{ role: 'isd', tar: '000000', msl: '16' },
spTarRender(presetFixture({ tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }));
assert.match(els['sp-tar-list'].innerHTML, /value="000000"[^>]*label="ISD \(MSL 16\)"/);
assert.match(els['sp-tar-list'].innerHTML, /value="AF4D02"[^>]*label="My applet \(MSL 0A\)"/);
spTarListRender(null);
assert.strictEqual(els['sp-tar-list'].innerHTML, '');
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
spTarRender(null);
assert.match(els['sp-tar'].innerHTML, /select a card preset/);
assert.strictEqual(els['sp-tar'].disabled, true);
});
test('packing a chain selects the TAR of its target', () => {
@@ -533,17 +572,18 @@ test('packing a chain selects the TAR of its target', () => {
assert.strictEqual(els['sp-apdu'].value, 'AA0100');
});
test('packing without a selected preset uses the spec default TAR', () => {
test('packing without a selected preset keeps the role for the next apply', () => {
// v3.26.0: the TAR options come from the preset - without one the selector
// stays on its placeholder and the role is applied when a preset arrives
const els = setup();
els['chain-sim-preview'].value = 'A0A4000002';
packToSp('chain-sim-preview');
assert.strictEqual(globalThis._spTarKey, 'uiccRfm');
assert.strictEqual(els['sp-tar'].value, '', 'no preset -> no TAR options yet');
globalThis.cards = [presetFixture()];
els['sp-card-sel'].value = '0';
cardsApply(0);
assert.strictEqual(els['sp-tar'].value, 'B00000');
els['chain-usim-preview'].value = '00A4000002';
packToSp('chain-usim-preview');
assert.strictEqual(els['sp-tar'].value, 'B00001');
els['chain-ram-preview'].value = '80E29000';
packToSp('chain-ram-preview');
assert.strictEqual(els['sp-tar'].value, '000000');
});
test('the TAR row values are left-aligned like the headers', () => {
@@ -586,7 +626,8 @@ test('the TAR table renders the role rows and adds/removes free rows', () => {
assert.strictEqual(_cardsTarCount, 2);
// the new row renders empty
tarRow(els, 4, { tar: '', msl: '', desc: '' });
assert.deepStrictEqual(cardsFreeTarsFromForm(), [{ tar: 'AF4D02', msl: '0A', desc: 'two' }]);
assert.deepStrictEqual(cardsFreeTarsFromForm(),
[{ tar: 'AF4D02', msl: '0A', desc: 'two', keyset: null }]);
// the role rows are never removable
cardsTarRemove(0);
assert.strictEqual(_cardsTarCount, 2);
+6
View File
@@ -43,6 +43,12 @@ code += extractFunc(html, 'cardsScp80Complete') + '\n';
code += extractFunc(html, 'cardsScp81Complete') + '\n';
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
code += extractFunc(html, 'spKeysetStartReady') + '\n';
code += extractFunc(html, 'spKeysetGuard') + '\n';
code += extractFunc(html, 'ramKeysetGuard') + '\n';
code += extractFunc(html, 'spTarKeysetGuard') + '\n';
code += extractFunc(html, 'cardsTarEntry') + '\n';
code += extractFunc(html, 'cardsRoleTar') + '\n';
code += extractFunc(html, 'cardsRoleDefault') + '\n';
code += extractFunc(html, 'spPresetIdx') + '\n';
code += extractFunc(html, 'ramPresetIdx') + '\n';
code += extractFunc(html, 'tarPresetIdx') + '\n';
+46 -57
View File
@@ -23,8 +23,9 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml']) {
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spCounterTracked', 'spPresetIdx', 'ramPresetIdx',
'tarPresetIdx', 'spKeysetSync', 'ramRender', 'esc', 'escHtml',
'cardsTarList', 'cardsTarEntry', 'cardsRoleTar']) {
code += extractFunc(html, fn) + '\n';
}
code += 'function t(s){return s;}\n';
@@ -91,20 +92,23 @@ test('spKeysetCheck enforces the keyset-number rules', () => {
assert.match(spKeysetCheck(PRESET, '15', 'ZZ'), /KID/);
});
test('spKeysetOptionsHtml lists the keysets and marks an undefined number', () => {
const html1 = spKeysetOptionsHtml(PRESET, '15', '15');
assert.match(html1, /value="1" selected/);
assert.match(html1, /value="2"/);
assert.ok(html1.indexOf('0000000005') > 0, 'the counters are shown');
// a version the preset does not define is offered but marked
const html2 = spKeysetOptionsHtml(PRESET, '35', '35');
assert.match(html2, /value="3" selected/);
assert.match(html2, /not defined in the preset/);
// a preset without keysets: the form's version shows as undefined
const html3 = spKeysetOptionsHtml({ keysets: [] }, '15', '15');
assert.match(html3, /not defined in the preset/);
// no keyset number in the form and no keysets at all
assert.match(spKeysetOptionsHtml({}, '', ''), /no keyset/);
test('spTarKeysetGuard follows the TAR default keyset (v3.26.0)', () => {
const preset = Object.assign({}, PRESET, { tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ role: 'uiccRfm', tar: 'B00000', msl: '16', keyset: 2 },
{ role: 'usimRfm', tar: 'B00001', msl: '00' },
{ tar: 'B00011', msl: '16', keyset: 3 }] });
assert.strictEqual(spTarKeysetGuard(preset, '000000'), '');
assert.strictEqual(spTarKeysetGuard(preset, 'B00001'), ''); // MSL 00: keyless is fine
// a TAR naming a keyset the preset does not define
assert.match(spTarKeysetGuard(preset, 'B00011'), /is not defined in the preset/);
// a security TAR without a default keyset
const noKeyset = Object.assign({}, PRESET, { tars: [
{ role: 'isd', tar: '000000', msl: '16' }] });
assert.match(spTarKeysetGuard(noKeyset, '000000'), /needs a default keyset/);
// a TAR outside the preset is the server's business; no preset: nothing
assert.strictEqual(spTarKeysetGuard(preset, 'B00099'), '');
assert.strictEqual(spTarKeysetGuard(null, '000000'), '');
});
test('the Cards form wires the keyset editor', () => {
@@ -117,9 +121,14 @@ test('the Cards form wires the keyset editor', () => {
// the keyset rows are built with the KIc/KID/key/counter inputs
assert.match(html, /cards-ks-' \+ i \+ '-kic-key/);
assert.match(html, /cards-ks-' \+ i \+ '-cntr/);
// the selectors exist in both SCP80 views
assert.match(html, /id="sp-keyset-sel" onchange="spKeysetChanged\(\)"/);
assert.match(html, /id="ram-keyset-sel" onchange="ramKeysetChanged\(\)"/);
// v3.26.0: the keyset selectors are gone - the SCP80 TAR select drives
// the keyset, the RAM operations use the preset's ISD entry, and the TAR
// table gained a keyset column
assert.match(html, /id="sp-tar" onchange="spTarChanged\(\)"/);
assert.ok(!html.includes('id="sp-keyset-sel"'));
assert.ok(!html.includes('id="ram-keyset-sel"'));
assert.ok(!html.includes('id="tar-keyset-sel"'));
assert.match(html, /id="cards-tr-' \+ i \+ '-keyset"/);
// the packet actions run the guard
assert.ok(html.includes('const ksErr = spKeysetGuard();'));
assert.ok(html.includes('const ksErr = ramKeysetGuard();'));
@@ -136,14 +145,9 @@ test('spCounterTracked follows SPI1 b5b4 (TS 102 225 5.1.1)', () => {
assert.strictEqual(spCounterTracked('zz'), false);
});
test('ramRender rebuilds the keyset selector from the remembered preset', () => {
// regression: the auto-selection runs before the RAM select has options, so
// its keyset list was built with no preset and stayed on "not defined"
test('ramRender keeps the remembered preset (v3.26.0: no keyset selector)', () => {
const els = {
'ram-card-sel': { value: '', innerHTML: '', appendChild() {} },
'ram-keyset-sel': { value: '', innerHTML: '' },
'sp-keyset-sel': { value: '', innerHTML: '' },
'tar-keyset-sel': { value: '', innerHTML: '' },
'sp-kic-hex': { value: '25' },
'sp-kid-hex': { value: '25' },
};
@@ -160,9 +164,6 @@ test('ramRender rebuilds the keyset selector from the remembered preset', () =>
ramRender();
assert.strictEqual(els['ram-card-sel'].value, '0',
'the preset select keeps the remembered card');
assert.match(els['ram-keyset-sel'].innerHTML, /value="2" selected/);
assert.ok(!els['ram-keyset-sel'].innerHTML.includes('not defined'),
'the keyset selector must not show a stale "not defined" entry');
});
test('the RAM and TAR views rebuild their keyset lists when their preset is known', () => {
@@ -179,44 +180,32 @@ test('the RAM and TAR views rebuild their keyset lists when their preset is know
assert.ok(html.includes("if (name === 'tar') { tarRender(); tarProbeRender(); }"));
});
test('the keyset selector is disabled and cleared while no preset is selected', () => {
test('the TAR selector waits for a preset and lists its entries', () => {
const els = {
'sp-keyset-sel': { value: '', innerHTML: '' },
'ram-keyset-sel': { value: '', innerHTML: '' },
'tar-keyset-sel': { value: '', innerHTML: '' },
'sp-tar': { value: '', disabled: false, innerHTML: '', options: [] },
'sp-card-sel': { value: '' },
'ram-card-sel': { value: '' },
'tar-card-sel': { value: '' },
'sp-kic-hex': { value: '' },
'sp-kid-hex': { value: '' },
};
globalThis.document = { getElementById: id => els[id] || null };
globalThis.cards = [];
spKeysetSync();
for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) {
assert.strictEqual(els[id].disabled, true, id + ' must be disabled');
assert.match(els[id].innerHTML, /select a card preset/);
assert.ok(!els[id].innerHTML.includes('not defined'), id);
}
// a selected preset enables the selector and lists its keysets
globalThis.cards = [{ name: 'EP', keysets: [
{ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }] }];
spTarRender();
assert.strictEqual(els['sp-tar'].disabled, true, 'no preset: the selector waits');
assert.match(els['sp-tar'].innerHTML, /select a card preset/);
// a selected preset enables the selector and lists its TARs with their
// MSL and default keyset
globalThis.cards = [{ name: 'EP', tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
els['sp-card-sel'].value = '0';
els['ram-card-sel'].value = '0';
els['tar-card-sel'].value = '0';
els['sp-kic-hex'].value = '15';
els['sp-kid-hex'].value = '15';
spKeysetSync();
for (const id of ['sp-keyset-sel', 'ram-keyset-sel', 'tar-keyset-sel']) {
assert.strictEqual(els[id].disabled, false, id + ' must be enabled');
assert.match(els[id].innerHTML, /value="1" selected/);
}
spTarRender();
assert.strictEqual(els['sp-tar'].disabled, false);
assert.match(els['sp-tar'].innerHTML, /value="000000"[^>]*>000000 \u00b7 ISD \u00b7 MSL 16 \u00b7 Keyset 1</);
assert.match(els['sp-tar'].innerHTML, /value="AF4D02"[^>]*>AF4D02 \u00b7 My applet \u00b7 MSL 0A</);
});
test('clearing the preset selection clears the keyset selectors', () => {
test('clearing the preset selection resets the TAR selector', () => {
const at = html.indexOf('function cardsApply(');
assert.ok(at > 0);
const body = html.slice(at, at + 600);
assert.ok(body.includes('spKeysetSync();'),
'cardsApply must sync the keyset selectors when no preset is selected');
assert.ok(body.includes('spTarRender(null);'),
'cardsApply must reset the TAR selector when no preset is selected');
});
+13 -10
View File
@@ -30,9 +30,9 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow',
'ramCntrLowBox', 'ramCntrLowClear',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged',
'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'spPresetIdx', 'tarPresetIdx',
'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply',
'ramPresetIdx', 'spKeysetGuard', 'ramKeysetGuard',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -1038,13 +1038,12 @@ test('every SCP80/RAM flow checks the low counter and stops', () => {
assert.ok(go.includes('cardsEdit('), go);
assert.ok(go.includes("getElementById('cards-ks-' + row + '-cntr')"), go);
// the notice's sync probe posts to the bounded server endpoint with the
// preset, the keyset the failed operation used and its SPI/TAR
// preset, the operation's TAR (v3.26.0: the RAM view uses the ISD entry,
// the server resolves the keyset) and its SPI
const sync = extractFunc(html, 'ramSyncCounter');
assert.ok(sync.includes("'/api/counter-probe'"), sync);
assert.ok(sync.includes("own = view === 'sp'"), sync);
assert.ok(sync.includes('kvnOf(own)'), sync);
assert.ok(sync.includes('kvnOf(other)'), sync);
assert.ok(sync.includes('spKeysetSync()'), 'the keyset labels must refresh');
assert.ok(sync.includes("view === 'ram' ? cardsRoleTar(preset, 'isd')"), sync);
assert.ok(sync.includes('spKeysetSync()'), 'the availability must refresh');
assert.ok(sync.includes('spCntrPlaceholder()'),
'the stored value must refresh the field placeholder (the field stays an override)');
assert.ok(sync.includes("ramListingSpi2('40')"),
@@ -1372,9 +1371,13 @@ test('getRamSpParams reads the computed SPI2 byte, not the base select', () => {
assert.ok(/spi2: document\.getElementById\('sp-spi2-hex'\)/.test(fn), fn);
});
test('getRamSpParams normalises the packet TAR to three bytes', () => {
test('getRamSpParams sends the preset ISD TAR and no keyset fields (v3.26.0)', () => {
// the server resolves the keyset from the preset's ISD TAR entry - the
// PWA sends only the preset id, the ISD TAR, SPI and the counter override
const fn = extractFunc(html, 'getRamSpParams');
assert.ok(/tar:[\s\S]*?padEnd\(6, '0'\)\.slice\(0, 6\),/.test(fn), fn);
assert.ok(fn.includes("tar: preset ? cardsRoleTar(preset, 'isd') : ''"), fn);
assert.ok(!fn.includes('kic:'), 'the keyset fields are resolved server-side');
assert.ok(!fn.includes('kicKey:'), 'the keys travel from the preset store');
});
test('ramListingSpi2 requests the SMS-submit PoR for the listing queries', () => {
+8 -2
View File
@@ -35,14 +35,20 @@ for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr
'ramCntrLowBox', 'ramCntrLowClear',
'escHtml', 'esc',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'spTarOptionsHtml', 'spTarRender', 'spSpi1Seed', 'spTarKeysetGuard', 'cardsTarKeysetCell', 'cardsTarKeysetFromForm', 'cardsTarKeysetRefresh', 'cardsTarRowsRender', 'cardsTarRowHtml', 'cardsTarsFromTable', 'cardsFreeTarsFromForm', 'cardsRoleTar', 'cardsTarEntry', 'cardsTarList', 'cardsRoleDefault', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender',
'cardsKeysetAdd', 'cardsKeysetRemove', 'cardsKeysetKvnUpdate',
'spKeysetChanged', 'ramKeysetChanged', 'ramPresetIdx', 'spKeysetGuard',
'ramPresetIdx', 'spKeysetGuard',
'ramKeysetGuard',
]) {
code += extractFunc(html, fn) + '\n';
}
code = 'var _cardsKeysetCount = 0;\n' + code;
// the role defaults the TAR helpers read (v3.26.0)
for (const c of ['CARDS_TAR_DEFAULTS', 'CARDS_TAR_ROLES', 'CARDS_MSL_DEFAULT']) {
const m = new RegExp('const\\s+' + c + '\\s*=\\s*([^;]+);').exec(html);
if (!m) throw new Error('const ' + c + ' not found');
code = 'var ' + c + ' = ' + m[1] + ';\n' + code;
}
eval(code);
function fakeEnv(por, success) {
+33 -65
View File
@@ -23,78 +23,43 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['esc', 'escHtml', 'tarProbeVerdictLabel', 'tarProbeHtml',
'tarProbeRowsHtml', 'tarProbeCustomLoad', 'tarProbeCustomSave',
'tarProbeAllRows', 'tarProbeChecked']) {
'tarProbeRowsHtml', 'tarProbeAllRows', 'tarProbeChecked', 'cardsTarList']) {
code += extractFunc(html, fn) + '\n';
}
code += "var TAR_PROBE_TARS = JSON.parse('" +
JSON.stringify(eval('(' + html.match(/const TAR_PROBE_TARS = \[[\s\S]*?\n\];/)[0]
.replace('const TAR_PROBE_TARS = ', '').replace(/;$/, '') + ')')) + "');\n";
code += "var TAR_PROBE_STORAGE = 'simple_tar_probe';\n";
code += 'function t(s){return s;}\n';
eval(code);
function fakeStorage() {
const map = {};
globalThis.localStorage = {
getItem: k => (k in map ? map[k] : null),
setItem: (k, v) => { map[k] = String(v); },
};
return map;
}
test('the standard list covers the Annex D allocations', () => {
const tars = TAR_PROBE_TARS.map(e => e.tar);
assert.ok(tars.length >= 10, tars.length);
for (const want of ['000000', 'B20100', 'B00000', 'B00001', 'B00010',
'B00120', 'B00130', 'B00140', 'B20000', 'B20200', 'B20201', 'B00200']) {
assert.ok(tars.includes(want), want);
}
// every row carries the spec document it comes from
TAR_PROBE_TARS.forEach(e => {
assert.match(e.tar, /^[0-9A-F]{6}$/);
assert.ok(e.label, e.tar);
});
});
test('tarProbeRowsHtml renders checked rows and marks custom ones', () => {
test('tarProbeRowsHtml renders the preset rows checked', () => {
// v3.26.0: the rows come from the preset - no custom rows, no remove
// buttons
const out = tarProbeRowsHtml([
{ tar: '000000', label: 'Issuer Security Domain (compact)', doc: 'TS 102 226' },
{ tar: '123456', label: 'my applet', custom: true },
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
{ tar: 'AF4D02', label: 'My applet' },
]);
assert.match(out, /value="000000" checked/);
assert.match(out, /title="TS 102 226"/);
assert.match(out, /Issuer Security Domain \(compact\)/);
assert.match(out, /value="123456" checked/);
assert.match(out, /tarProbeRemoveCustom\('123456'\)/, 'custom rows get a remove button');
assert.ok(!/tarProbeRemoveCustom\('000000'\)/.test(out), 'standard rows have none');
assert.match(out, /title="MSL 16/);
assert.match(out, /ISD/);
assert.match(out, /value="AF4D02" checked/);
assert.ok(!out.includes('tarProbeRemoveCustom'), 'no custom rows anymore');
// escaping
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x', custom: true }]);
const evil = tarProbeRowsHtml([{ tar: 'AA<BB', label: '<i>x' }]);
assert.ok(!evil.includes('AA<BB'));
assert.ok(!evil.includes('<i>x'));
});
test('custom TARs round-trip through localStorage (best effort)', () => {
fakeStorage();
assert.deepStrictEqual(tarProbeCustomLoad(), []);
tarProbeCustomSave([{ tar: '123456', label: 'mine' }, { tar: 'aabbcc' }]);
const back = tarProbeCustomLoad();
assert.deepStrictEqual(back, [{ tar: '123456', label: 'mine', custom: true },
{ tar: 'AABBCC', label: '', custom: true }]);
// garbage is dropped, never fatal
localStorage.setItem('simple_tar_probe', '{not json');
assert.deepStrictEqual(tarProbeCustomLoad(), []);
localStorage.setItem('simple_tar_probe', JSON.stringify([{ tar: 'zz' }, { tar: 'B0B0B0' }]));
assert.deepStrictEqual(tarProbeCustomLoad().map(e => e.tar), ['B0B0B0']);
});
test('tarProbeAllRows appends the custom TARs to the standard ones', () => {
fakeStorage();
tarProbeCustomSave([{ tar: '123456', label: 'mine' }]);
const rows = tarProbeAllRows();
assert.strictEqual(rows.length, TAR_PROBE_TARS.length + 1);
assert.strictEqual(rows[rows.length - 1].tar, '123456');
assert.strictEqual(rows[rows.length - 1].custom, true);
test('tarProbeAllRows lists the selected preset TARs (v3.26.0)', () => {
globalThis.cards = [{ name: 'C', tars: [
{ role: 'isd', tar: '000000', msl: '16', keyset: 1 },
{ tar: 'AF4D02', msl: '0A', desc: 'My applet' }] }];
globalThis.tarPresetIdx = () => 0;
assert.deepStrictEqual(tarProbeAllRows(), [
{ tar: '000000', label: 'ISD', doc: 'MSL 16 \u00b7 Keyset 1' },
{ tar: 'AF4D02', label: 'My applet', doc: 'MSL 0A' },
]);
// no preset: no rows
globalThis.tarPresetIdx = () => -1;
assert.deepStrictEqual(tarProbeAllRows(), []);
delete globalThis.tarPresetIdx;
});
test('tarProbeChecked reads the checked boxes in list order', () => {
@@ -141,21 +106,24 @@ test('the SCP80 tab wires the TAR probe pill', () => {
assert.match(html, /<div id="scp80-sub-tar" class="hidden">/);
assert.ok(html.includes("document.getElementById('scp80-sub-tar').classList.toggle('hidden', name !== 'tar');"));
assert.ok(html.includes("tar: 'tar-probe'"), 'the help anchor points at the new section');
// its own preset + keyset selectors and the warning banner
// its own preset selector and the warning banner; v3.26.0: the probe
// checks the preset's TAR list - no keyset selector, no custom TARs
assert.match(html, /id="tar-card-sel" onchange="tarApplyCard\(this.value\)"/);
assert.match(html, /id="tar-keyset-sel" onchange="tarKeysetChanged\(\)"/);
assert.match(html, /id="tar-preset-warning"/);
// the checklist, the add form, the APDU and the Execute button
assert.ok(!html.includes('id="tar-keyset-sel"'), 'the probe keyset selector is gone');
assert.ok(!html.includes('tarProbeAddCustom'), 'no arbitrary TARs anymore');
assert.match(html, /TARs to probe \(the preset's TAR list\)/);
// the checklist, the APDU and the Execute button
assert.match(html, /id="tar-list"/);
assert.match(html, /onclick="tarProbeSelectAll\(true\)"/);
assert.match(html, /onclick="tarProbeAddCustom\(\)"/);
assert.match(html, /id="tar-apdu"[^>]*value="00A40000023F00"/);
assert.match(html, /onclick="tarExecute\(\)"/);
assert.match(html, /id="tar-result"/);
// the run
// the run: the preset and the checked TARs travel - the server resolves
// each row's keyset
assert.ok(html.includes("pysimFetch('/api/tar-probe', body)"));
assert.ok(html.includes('tars: tars'));
assert.ok(html.includes("getRamSpParams('tar-card-sel')"));
assert.ok(!html.includes("getRamSpParams('tar-card-sel')"));
assert.ok(html.includes('if (data.final_cntr) ramSaveCntr(data.final_cntr);'));
// the RAM-era leftovers are gone
assert.ok(!html.includes('id="ram-tar-rows"'));
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.25.8"
version = "3.26.0"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+57 -5
View File
@@ -69,7 +69,7 @@ from pathlib import Path
DEFAULT_DIR = '.pysim-simple-server'
DEFAULT_FILENAME = 'card_presets.json'
SCHEMA_VERSION = 5
SCHEMA_VERSION = 6
# The fields a preset carries: the localStorage shape plus the id. The OTA key
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
@@ -88,7 +88,12 @@ DIGIT_FIELDS = ('pin1', 'puk1', 'pin2', 'puk2')
KEYSET_HEX_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
# The TAR table: the three mandatory role entries (their values are pre-settled
# in the Cards form) and free entries with an optional description. Every
# entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
# entry carries the TAR, its MSL (Minimum SPI1, one hex byte) and its **default
# keyset** (v3.26.0): the keyset every operation addressed to that TAR uses.
# The keyset is mandatory for any non-zero MSL (the counters are keyset-linked,
# TS 102 225 A.1) and may be omitted only for MSL 00 - where setting one is
# still allowed, to pass its KIc/KID in the packet (some cards reject the
# no-security '00/00' form even when SPI declares no security).
TAR_ROLES = ('isd', 'uiccRfm', 'usimRfm')
TAR_ROLE_DEFAULTS = {
'isd': {'tar': '000000', 'msl': '16'},
@@ -194,6 +199,13 @@ def tar_msl(preset, tar_hex):
return str((tar_entry(preset, tar_hex) or {}).get('msl') or '')
def tar_keyset(preset, tar_hex):
"""The default keyset number (int 1-15) of a TAR, or None when the preset
has no such entry or the entry carries no keyset (allowed for an MSL 00
entry - the packet is then built keyless; v3.26.0)."""
return (tar_entry(preset, tar_hex) or {}).get('keyset') or None
class PresetError(Exception):
"""A refused mutation (validation, duplicate ICCID, ...)."""
@@ -306,6 +318,14 @@ class PresetStore:
raw = [{k: fields.get(k) for k in LEGACY_KEY_FIELDS}]
out['keysets'] = [self._normalise_keyset(ks) for ks in raw
if isinstance(ks, dict)]
# v3.26.0: a preset with a single keyset fills every TAR entry that has
# no default keyset with it - the TARs' keysets drive the operations.
nums = [keyset_kvn(ks) for ks in out['keysets']]
nums = [n for n in nums if n]
if len(nums) == 1:
for t in out['tars']:
if not t.get('keyset'):
t['keyset'] = nums[0]
pid = str(fields.get('id') or '').strip().lower()
out['id'] = pid or uuid.uuid4().hex
return out
@@ -328,6 +348,7 @@ class PresetStore:
'tar': re.sub(r'\s', '', str(t.get('tar') or '')).upper(),
'msl': re.sub(r'\s', '', str(t.get('msl') or '')).upper(),
'desc': str(t.get('desc') or '').strip(),
'keyset': self._normalise_keyset_ref(t.get('keyset')),
})
else:
msl = re.sub(r'\s', '', str(fields.get('spi1') or '')).upper()
@@ -338,6 +359,7 @@ class PresetStore:
'tar': re.sub(r'\s', '', str(fields.get(src) or '')).upper(),
'msl': msl,
'desc': '',
'keyset': None,
})
out = []
for role in TAR_ROLES:
@@ -346,16 +368,37 @@ class PresetStore:
out.append({'role': role,
'tar': (cur or {}).get('tar') or d['tar'],
'msl': (cur or {}).get('msl') or d['msl'],
'desc': ''})
'desc': '',
'keyset': (cur or {}).get('keyset')})
for e in entries:
if e['role']:
continue
if not (e['tar'] or e['msl'] or e['desc']):
if not (e['tar'] or e['msl'] or e['desc'] or e.get('keyset')):
continue # a blank editor row
# free entries carry no `role`: the three roles are the fixed ones
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc']})
out.append({'tar': e['tar'], 'msl': e['msl'], 'desc': e['desc'],
'keyset': e.get('keyset')})
return out
@staticmethod
def _normalise_keyset_ref(v):
"""A TAR's default keyset number (int 1-15) or None. Accepts an int
and the decimal ('1'..'15') or hex-ish ('01'..'0F') string forms; an
unusable value becomes None - the validation reports a missing keyset
where one is mandatory (v3.26.0)."""
if v is None or v == '':
return None
s = str(v).strip().upper()
if re.fullmatch(r'[0-9]{1,2}', s):
n = int(s, 10)
if 1 <= n <= KVN_MAX:
return n
if re.fullmatch(r'[0-9A-F]{1,2}', s):
n = int(s, 16)
if 1 <= n <= KVN_MAX:
return n
return None
def _normalise_keyset(self, ks):
"""One keyset, cleaned. The counter is kept as typed here and
validated + padded in _validate_keyset() - a bad value must be
@@ -402,6 +445,15 @@ class PresetStore:
'byte, TS 102 226 8.2.1.3.2.4)' % t['tar'])
if not re.fullmatch(r'[0-9A-F]{2}', t['msl']):
raise PresetError('MSL must be one hex byte: %r' % t['msl'])
if t.get('keyset'):
if not any(keyset_kvn(ks) == t['keyset'] for ks in p['keysets']):
raise PresetError('TAR %s names keyset %02X, which this '
'preset does not define'
% (t['tar'], t['keyset']))
elif t['msl'] != '00':
raise PresetError('TAR %s needs a default keyset (its MSL %s '
'requires security) - set it in the Cards tab'
% (t['tar'], t['msl']))
if t['tar'] in seen_tars:
raise PresetError('duplicate TAR %s in this preset' % t['tar'])
seen_tars.add(t['tar'])
+236 -79
View File
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.25.8'
VERSION = '3.26.0'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1700,6 +1700,54 @@ def _preset_keyset_check(server, preset_id, kic, kid):
return None
def _preset_tar_keyset(server, preset_id, tar):
"""The default keyset number of a TAR in the named preset (v3.26.0), or
None (no preset, no such TAR entry, or an MSL 00 entry without one)."""
preset = _preset_by_id(server, preset_id)
if preset is None:
return None
return presets.tar_keyset(preset, tar)
def _preset_keyset_by_kvn(preset, kvn):
"""The preset's keyset dict with number `kvn`, or None."""
return next((ks for ks in _preset_keysets(preset)
if presets.keyset_kvn(ks) == kvn), None)
def _resolve_operation_keyset(server, body, tar):
"""Fill an operation's keyset fields from the preset's default keyset for
`tar` (v3.26.0).
An explicit KIc/KID in the request is an API override and stays as it is;
otherwise the TAR's default keyset supplies kic/kid/kicKey/kidKey. A TAR
whose MSL requires security must have one - the operation refuses instead
of sending a keyless packet the card would reject (or, worse, process as
no-security). Returns ``(kvn, error)``."""
explicit_kic = str((body or {}).get('kic') or '').strip().upper()
explicit_kid = str((body or {}).get('kid') or '').strip().upper()
if explicit_kic or explicit_kid:
return (_kvn_of(explicit_kic, explicit_kid)[0] or None), None
preset = _preset_by_id(server, (body or {}).get('preset_id'))
if preset is None:
return None, None
kvn = presets.tar_keyset(preset, tar)
if not kvn:
msl = presets.tar_msl(preset, tar)
if msl and msl != '00':
return None, ('TAR %s needs a default keyset (its MSL %s requires '
'security) - set it in the Cards tab' % (tar, msl))
return None, None
keyset = _preset_keyset_by_kvn(preset, kvn)
if keyset is None:
return None, ('keyset %02X is not defined in preset %s - add it in the '
'Cards tab' % (kvn, preset.get('name') or
(body or {}).get('preset_id')))
for field in ('kic', 'kid', 'kicKey', 'kidKey'):
body[field] = str(keyset.get(field) or '').strip()
return kvn, None
def _preset_counter_persist(server, preset_id, cntr, source, kvn=None):
"""Persist the counter an operation accepted into the server-side preset
store (v3.8.0), into the keyset it used (each keyset has its own dedicated
@@ -1916,6 +1964,7 @@ def _counter_probe_params(preset, body):
keysets = _preset_keysets(preset)
if not keysets:
raise ValueError('the preset defines no keyset')
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
want = body.get('kvn')
if want not in (None, ''):
try:
@@ -1926,7 +1975,13 @@ def _counter_probe_params(preset, body):
if keyset is None:
raise ValueError('keyset %d is not defined in the preset' % want)
else:
keyset = keysets[0]
# v3.26.0: the TAR's default keyset, else the first - the probe is a
# repair tool and keeps working for a legacy preset without TAR keysets.
tar_kvn = presets.tar_keyset(preset, tar)
keyset = (next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
if tar_kvn else None)
if keyset is None:
keyset = keysets[0]
kic = str(keyset.get('kic') or '').strip().upper()
kid = str(keyset.get('kid') or '').strip().upper()
kic_key = str(keyset.get('kicKey') or '').strip()
@@ -1953,7 +2008,6 @@ def _counter_probe_params(preset, body):
except (TypeError, ValueError):
raise ValueError('invalid attempt budget')
max_attempts = max(1, min(64, max_attempts))
tar = str(body.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'), prefer_msl=True)
if not _counter_tracked(spi1):
raise ValueError('the probe needs a counter check - SPI1 %s has none '
@@ -6133,26 +6187,6 @@ _CARD_FREE_GET = frozenset([
'/api/scp81/log', '/api/bip/log',
])
# TAR values allocated by ETSI (TS 101 220 V18.3.0 Annex D, Table D.1): the
# TAR probe sends one harmless command to each and reports the card's answer.
# 'B00200' is not an application - it is the start of the RFU range, kept as a
# control (a card must not answer it).
TAR_PROBE_TARS = [
('000000', 'Issuer Security Domain (compact)'),
('B20100', 'Issuer Security Domain (expanded)'),
('B00000', 'UICC shared file system RFM (compact)'),
('B00001', 'ADF RFM (compact)'),
('B00010', 'SIM file system RFM (compact)'),
('B00120', 'UICC shared file system RFM (expanded)'),
('B00130', 'SIM file system RFM (expanded)'),
('B00140', 'ADF RFM (expanded)'),
('B20000', 'USAT interpreter'),
('B20200', 'Multiplexing application'),
('B20201', 'Controlling Authority Security Domain'),
('B20202', 'OMA BCAST audience measurement'),
('B20203', 'OMA DM LWM2M application'),
('B00200', 'RFU (control)'),
]
TAR_PROBE_DEFAULT_APDU = '00A40000023F00' # SELECT MF: harmless, applet-visible
# The TARs the tool treats as remote-management targets: the preset's role
@@ -6447,6 +6481,24 @@ def _test_preset_error(script, preset):
if not _keyset_complete(ks):
return ('step %d: keyset %d is incomplete (KIc, KID, both '
'keys and a counter)' % (i + 1, want))
else:
# v3.26.0: the step's TAR picks its default keyset - it must exist
# and be complete; a TAR whose MSL requires security must have one.
tar = p.get('tar') or presets.role_tar(preset, 'isd')
tar_kvn = presets.tar_keyset(preset, tar)
if tar_kvn:
ks = next((k for k in keysets if presets.keyset_kvn(k) == tar_kvn), None)
if ks is None:
return ('step %d: keyset %d (the default of TAR %s) is not '
'defined in the preset' % (i + 1, tar_kvn, tar))
if not _keyset_complete(ks):
return ('step %d: keyset %d (the default of TAR %s) is '
'incomplete (KIc, KID, both keys and a counter)'
% (i + 1, tar_kvn, tar))
elif (presets.tar_msl(preset, tar) or '') not in ('', '00'):
return ('step %d: TAR %s needs a default keyset (its MSL %s '
'requires security) - set it in the Cards tab'
% (i + 1, tar, presets.tar_msl(preset, tar)))
tar = p.get('tar') or presets.role_tar(preset, 'isd')
if not tar:
return 'step %d: no TAR (neither in the step nor in the preset)' % (i + 1)
@@ -6781,8 +6833,10 @@ def _test_run_scp80(server, step, ctx):
preset = ctx['preset']
p = step['params']
# The keyset: a step may name a keyset number (`kvn`), otherwise the
# preset's first keyset is used (each keyset has its own keys and counter -
# TS 102 225 Annex A.1).
# default keyset of the step's TAR is used (v3.26.0; each keyset has its
# own keys and counter - TS 102 225 Annex A.1). A TAR whose MSL requires
# security must have one - the counters are keyset-linked; an MSL 00 step
# falls back to the preset's first keyset.
keysets = _preset_keysets(preset)
keyset = None
if p.get('kvn') is not None:
@@ -6792,7 +6846,22 @@ def _test_run_scp80(server, step, ctx):
raise testscript.ScriptError('keyset %d is not defined in the '
'selected card preset' % want)
elif keysets:
keyset = keysets[0]
tar = str(p.get('tar') or presets.role_tar(preset, 'isd') or '').strip().upper()
tar_kvn = presets.tar_keyset(preset, tar)
if tar_kvn:
keyset = next((ks for ks in keysets
if presets.keyset_kvn(ks) == tar_kvn), None)
if keyset is None:
raise testscript.ScriptError(
'keyset %d (the default of TAR %s) is not defined in the '
'selected card preset' % (tar_kvn, tar))
else:
msl = presets.tar_msl(preset, tar)
if msl and msl != '00':
raise testscript.ScriptError(
'TAR %s needs a default keyset (its MSL %s requires '
'security) - set it in the Cards tab' % (tar, msl))
keyset = keysets[0]
kic = p.get('kic') or (keyset or {}).get('kic') or ''
kid = p.get('kid') or (keyset or {}).get('kid') or ''
kic_key = p.get('kicKey') or (keyset or {}).get('kicKey') or ''
@@ -8043,6 +8112,23 @@ class PysimHandler(BaseHTTPRequestHandler):
err = _preset_keyset_check(self.server, body.get('preset_id'), kic, kid)
return (kvn or None), err
def _op_keyset(self, body, tar=None):
"""v3.26.0: resolve an operation's keyset from the preset's TAR table
(the RAM operations default to the ISD role's TAR), then run the
KIc/KID checks. An explicit request KIc/KID stays an API override.
Returns ``(kvn, kic, kid, error)`` with the effective values."""
t = str(tar if tar is not None else (body.get('tar') or '')).strip().upper()
if not t and body.get('preset_id'):
preset = _preset_by_id(self.server, body.get('preset_id'))
t = presets.role_tar(preset, 'isd') if preset else ''
_kvn, err = _resolve_operation_keyset(self.server, body, t)
if err:
return None, '', '', err
kic = str(body.get('kic') or '').strip().upper()
kid = str(body.get('kid') or '').strip().upper()
kvn, err = self._keyset_guard(body, kic, kid)
return kvn, kic, kid, err
def _log_req(self, body=None):
if self.server.log_requests:
if body is not None:
@@ -9216,11 +9302,15 @@ class PysimHandler(BaseHTTPRequestHandler):
# deliberately ask for more security than the TAR's minimum), with
# a warning below the TAR's MSL; without one it is derived from the
# MSL of the packet's TAR (TS 102 226 8.2.1.3.2.4 - the card checks
# the MSL before the security processing).
# the MSL before the security processing). An APDU send without a
# TAR uses the preset's ISD role entry (v3.26.0).
try:
spi1, spi_warn = _spi1_for_tar(
_preset_by_id(self.server, body.get('preset_id')),
str(body.get('tar') or '').strip().upper(), body.get('spi1'))
preset_ref = _preset_by_id(self.server, body.get('preset_id'))
tar_ref = str(body.get('tar') or '').strip().upper()
if not tar_ref and apdu and preset_ref:
tar_ref = presets.role_tar(preset_ref, 'isd')
spi1, spi_warn = _spi1_for_tar(preset_ref, tar_ref,
body.get('spi1'))
except ValueError as e:
resp = {'error': str(e)}
self._send_json(resp, 400)
@@ -9245,9 +9335,15 @@ class PysimHandler(BaseHTTPRequestHandler):
# RAM operation: SCP80-wrap the raw GP command. The packet
# may exceed one SMS (pySim refuses that), so use our own
# encoder and let _send_secured_packet segment it.
kic = body.get('kic', '25')
kid = body.get('kid', '25')
tar = body.get('tar', '000000')
# The keyset: the preset's default for the packet's TAR
# (v3.26.0) unless the request names KIc/KID itself.
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
if keyset_err:
resp = {'error': keyset_err}
self._send_json(resp, 400)
self._log_resp(resp)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: an explicit `cntr` wins; otherwise the
@@ -9494,26 +9590,26 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
sd_aid = body.get('sd_aid', '').replace(' ', '')
install_params_hex = body.get('install_params', '').replace(' ', '')
stk_params_hex = body.get('stk_params', '').replace(' ', '')
make_selectable = body.get('make_selectable', True)
privileges_hex = body.get('privileges', '').replace(' ', '') or '00'
# The keyset number must be consistent and defined in the preset
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
# before the format probe consumes a counter.
kvn, keyset_err = self._keyset_guard(body, kic, kid)
# The keyset: the preset's default for the operation's TAR (the
# ISD role entry; v3.26.0) unless the request names KIc/KID
# itself - then the number must be consistent and defined in
# the preset (TS 102 225 A.2 / the "keyset must exist" rule),
# checked before the format probe consumes a counter.
kvn, kic, kid, keyset_err = self._op_keyset(body)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
@@ -9707,19 +9803,23 @@ class PysimHandler(BaseHTTPRequestHandler):
return
sp_state = {
'spi1': spi1, 'spi2': body.get('spi2', '01'),
'kic': body.get('kic', '25'), 'kid': body.get('kid', '25'),
'tar': tar, 'cntr': body.get('cntr', '00000000'),
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'include_cpi': body.get('includeCpi', True)}
# The keyset number must be consistent and defined in the preset
# The keyset: the preset's default for the operation's TAR
# (v3.26.0) unless the request names KIc/KID itself - then the
# number must be consistent and defined in the preset
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
# before the format probe consumes a counter.
kvn, keyset_err = self._keyset_guard(body, sp_state['kic'], sp_state['kid'])
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
sp_state['kic'] = kic
sp_state['kid'] = kid
sp_state['kic_key'] = body.get('kicKey', '')
sp_state['kid_key'] = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
@@ -9832,18 +9932,19 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp(err)
return
spi2 = body.get('spi2', '01')
kic = body.get('kic', '25')
kid = body.get('kid', '25')
cntr = body.get('cntr', '00000000')
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
steps = []
kvn, keyset_err = self._keyset_guard(body, kic, kid)
# The keyset: the preset's default for the operation's TAR
# (the ISD role entry; v3.26.0) unless the request names
# KIc/KID itself.
kvn, kic, kid, keyset_err = self._op_keyset(body)
if keyset_err:
err = {'success': False, 'error': keyset_err}
self._send_json(err, 400)
self._log_resp(err)
return
kic_key = body.get('kicKey', '')
kid_key = body.get('kidKey', '')
# The counter: explicit `cntr` wins, else the preset's value
# for this keyset (the server owns the counters, v3.20.0).
cntr = _preset_counter_seed(self.server, body, kvn) or cntr
@@ -10487,40 +10588,59 @@ class PysimHandler(BaseHTTPRequestHandler):
return
body = self._read_body()
self._log_req(body)
kic = body.get('kic', '')
kid = body.get('kid', '')
kvn, keyset_err = self._keyset_guard(body, kic, kid)
if keyset_err:
err = {'success': False, 'error': keyset_err}
# v3.26.0: the probe checks the preset's TAR list only - no
# arbitrary TARs - and each row uses its TAR's default keyset.
if not body.get('preset_id'):
err = {'success': False,
'error': 'preset_id is required - the probe checks the '
'preset TAR list'}
self._send_json(err, 400)
self._log_resp(err)
return
preset = _preset_by_id(self.server, body.get('preset_id'))
if not preset:
err = {'success': False, 'error': 'unknown preset'}
self._send_json(err, 400)
self._log_resp(err)
return
# The probe sends real secured packets: it needs a counter check
# (every accepted packet advances and is persisted) and a PoR. Each
# TAR uses its own MSL when the preset carries it; the caller's SPI1
# is the fallback for the arbitrary TARs the checklist probes.
# (every accepted packet advances and is persisted) and a PoR.
# Each TAR uses its own MSL and its own default keyset.
spi1_base = str(body.get('spi1') or '').strip().upper()
spi2 = str(body.get('spi2') or '01').strip().upper()
preset = _preset_by_id(self.server, body.get('preset_id'))
apdu = (body.get('apdu') or TAR_PROBE_DEFAULT_APDU).replace(' ', '').upper()
labels = {tar: label for tar, label in TAR_PROBE_TARS}
role_label = {'isd': 'ISD', 'uiccRfm': 'UICC RFM', 'usimRfm': 'ADF RFM'}
by_tar = {}
for t in (preset.get('tars') or []):
tar = str(t.get('tar') or '').strip().upper()
if tar:
by_tar[tar] = t
wanted = body.get('tars')
if isinstance(wanted, list) and wanted:
entries = []
for t in wanted:
tar = str(t).replace(' ', '').upper()
entries.append((tar, labels.get(tar, '')))
e = by_tar.get(tar)
if e is None:
err = {'success': False,
'error': 'TAR %s is not in the preset - the probe '
'checks the preset TAR list only' % tar}
self._send_json(err, 400)
self._log_resp(err)
return
entries.append((tar, e))
else:
entries = list(TAR_PROBE_TARS)
# Resolve every TAR's SPI1 before the first packet is sent: a
# missing MSL must refuse the whole probe, not leave it half-run.
entries = [(tar, e) for tar, e in by_tar.items()]
# Resolve every TAR's SPI1 and keyset before the first packet is
# sent: a missing MSL or keyset must refuse the whole probe, not
# leave it half-run.
resolved = []
for tar, label in entries:
for tar, e in entries:
try:
tar_spi1, _ = _spi1_for_tar(preset, tar, spi1_base,
prefer_msl=True)
except ValueError as e:
err = {'success': False, 'error': str(e)}
except ValueError as exc:
err = {'success': False, 'error': str(exc)}
self._send_json(err, 400)
self._log_resp(err)
return
@@ -10532,31 +10652,67 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json(err, 400)
self._log_resp(err)
return
resolved.append((tar, label, tar_spi1))
row_kvn = e.get('keyset')
row_ks = None
if row_kvn:
row_ks = _preset_keyset_by_kvn(preset, row_kvn)
if row_ks is None:
err = {'success': False,
'error': 'keyset %02X (TAR %s) is not defined in '
'the preset' % (row_kvn, tar)}
self._send_json(err, 400)
self._log_resp(err)
return
else:
msl = str(e.get('msl') or '')
if msl and msl != '00':
err = {'success': False,
'error': 'TAR %s needs a default keyset (its MSL '
'%s requires security) - set it in the '
'Cards tab' % (tar, msl)}
self._send_json(err, 400)
self._log_resp(err)
return
label = (str(e.get('desc') or '').strip()
or role_label.get(e.get('role') or '', ''))
resolved.append((tar, label, tar_spi1, row_kvn, row_ks))
explicit_cntr = str(body.get('cntr') or '').strip().upper()
state = {'steps': [], 'encode_error': None, 'failure': {},
# the counter: explicit `cntr` wins, else the preset's
# value for this keyset (the server owns the counters,
# v3.20.0)
'cntr': _preset_counter_seed(self.server, body, kvn) or '00000000',
'preset_id': body.get('preset_id'), 'kvn': kvn}
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': kic, 'kid': kid, 'tar': '000000',
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
'cntr': '00000000', 'preset_id': body.get('preset_id'),
'kvn': None}
sp = {'spi1': spi1_base, 'spi2': spi2, 'kic': '00', 'kid': '00',
'tar': '000000', 'kic_key': '', 'kid_key': '',
'include_cpi': body.get('includeCpi', True)}
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s, kvn=%s\n'
% (len(entries), apdu, spi1_base or '-', spi2, kvn))
sys.stderr.write('TAR-PROBE: %d TAR(s), APDU %s, SPI %s/%s\n'
% (len(entries), apdu, spi1_base or '-', spi2))
_ram_progress_begin('tar-probe', len(entries))
try:
for index, (tar, _label, tar_spi1) in enumerate(resolved):
for index, (tar, _label, tar_spi1, row_kvn, row_ks) in enumerate(resolved):
_ram_progress_step(index, 'TAR ' + tar)
sp['tar'] = tar
sp['spi1'] = tar_spi1
sp['kic'] = str((row_ks or {}).get('kic') or '00')
sp['kid'] = str((row_ks or {}).get('kid') or '00')
sp['kic_key'] = str((row_ks or {}).get('kicKey') or '')
sp['kid_key'] = str((row_ks or {}).get('kidKey') or '')
state['kvn'] = row_kvn
# Each keyset has its own counter (TS 102 225 A.1): seed
# the row's from the store (an explicit `cntr` override
# applies to the first row only).
if index == 0 and explicit_cntr:
state['cntr'] = explicit_cntr
else:
state['cntr'] = (_preset_counter_seed(
self.server, {'preset_id': body.get('preset_id')},
row_kvn) or '00000000')
_ram_send_gp_apdu(self.server, scc, sp, state, 'TAR ' + tar, apdu)
finally:
_ram_progress_end()
results = []
for (tar, label, tar_spi1), step in zip(resolved, state['steps']):
for (tar, label, tar_spi1, row_kvn, _ks), step in zip(resolved, state['steps']):
results.append({
'tar': tar, 'label': label, 'spi1': tar_spi1,
'kvn': row_kvn,
'verdict': _tar_probe_verdict(step),
'sw': step.get('sw'), 'por_status': step.get('por_status'),
'por_sw': step.get('por_sw'), 'por_data': step.get('por_data'),
@@ -10565,7 +10721,8 @@ class PysimHandler(BaseHTTPRequestHandler):
registered = sum(1 for r in results if r['verdict'] == 'registered')
resp = {'success': True, 'results': results, 'steps': state['steps'],
'registered': registered, 'total': len(results),
'final_cntr': state['cntr'], 'kvn': kvn,
'final_cntr': state['cntr'],
'kvn': resolved[0][3] if resolved else None,
'spi1': spi1_base, 'spi2': spi2, 'apdu': apdu}
self._send_json(resp)
self._log_resp(resp)
+91 -27
View File
@@ -582,21 +582,6 @@ class CntrLowGuardHttpTests(unittest.TestCase):
self.assertNotIn('suggested_cntr', resp)
class TarProbeListTests(unittest.TestCase):
"""The PWA's TAR checklist must match the server's TAR_PROBE_TARS - the
panel is pre-populated client-side, the server's list is the API fallback,
and both must describe the same allocations (TS 101 220 Annex D)."""
def test_the_pwa_and_the_server_share_the_standard_tar_list(self):
from pysim_simple_server import server as srv
html = (ROOT / 'frontend' / 'index.html').read_text(encoding='utf-8')
block = html[html.index('const TAR_PROBE_TARS = ['):]
block = block[:block.index('\n];')]
pwa = re.findall(r"tar:\s*'([0-9A-F]{6})',\s*label:\s*'([^']*)'", block)
self.assertEqual(pwa, list(srv.TAR_PROBE_TARS))
self.assertTrue(pwa, 'the PWA list must not be empty')
class PresetStoreHttpTests(unittest.TestCase):
"""The card preset endpoints (v3.8.0): the PWA's Cards tab talks to the
server store instead of localStorage, and the counter an operation
@@ -655,7 +640,18 @@ class PresetStoreHttpTests(unittest.TestCase):
def _preset(self, **kw):
p = {'name': 'C', 'keysets': [self._keyset()]}
if 'tars' not in kw:
# v3.26.0: the TARs name their default keyset
p['tars'] = [{'role': r, 'tar': t, 'msl': '16'}
for r, t in (('isd', '000000'), ('uiccRfm', 'B00000'),
('usimRfm', 'B00001'))]
p.update(kw)
if 'tars' not in kw and len(p.get('keysets') or []) > 1:
# a multi-keyset fixture must name its TAR keysets (a single-keyset
# one is auto-filled by the store): point them at the first keyset
kvn = int(str((p['keysets'][0] or {}).get('kic') or '10'), 16) >> 4
for t in p['tars']:
t['keyset'] = kvn
return p
def test_the_get_route_is_card_free_and_reports_the_store(self):
@@ -1136,6 +1132,55 @@ class PresetStoreHttpTests(unittest.TestCase):
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000006')
self.assertEqual(self.store.find_keyset(p['id'], 1)['cntr'], '0000000001')
def test_an_operation_uses_the_tar_default_keyset(self):
# v3.26.0: without explicit KIc/KID the RAM-style APDU send resolves
# the keyset from the preset's TAR table (the ISD role entry)
p = self.store.add(self._preset(iccid='8970119000004600098', keysets=[
self._keyset(kic='25', kid='25', kicKey='CC' * 16, kidKey='DD' * 16,
cntr='0000000005')]))
built = []
patches = [
mock.patch.object(self.srv, '_build_secured_packet',
side_effect=lambda *a, **k: (built.append(a) or ('00' * 30, {}))),
mock.patch.object(self.srv, '_send_secured_packet', lambda *a, **k: {
'success': True, 'sw': '9000', 'bytes': 34, 'segments': 1,
'response_data': '027100000000'}),
mock.patch.object(self.srv, '_decode_por', lambda *a, **k: {
'response_status': 'por_ok', 'tar': '000000', 'cntr': '0000000005',
'pcntr': 0, 'rpl': 11, 'rhl': 10, 'raw': '',
'decoded': {'number_of_commands': 1, 'last_status_word': '9000',
'last_response_data': ''}}),
]
self.server.app = object()
for patch in patches:
patch.start()
try:
status, resp = self._post('/api/send-ota', {
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id']})
finally:
for patch in reversed(patches):
patch.stop()
self.assertEqual(status, 200, resp)
args = built[0]
self.assertEqual((args[2], args[3]), ('25', '25')) # the TAR keyset's KIc/KID
self.assertEqual(args[5], '0000000005') # ... and its counter
self.assertEqual(resp['final_cntr'], '0000000006')
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000006')
def test_a_security_tar_without_a_keyset_refuses_the_operation(self):
# a legacy (v5) preset loads leniently; an operation addressed to a
# security TAR without a default keyset refuses instead of sending a
# keyless packet the card would reject (v3.26.0)
p = self.store.add(self._preset(keysets=[self._keyset(),
self._keyset(kic='29', kid='29')]))
# simulate the legacy file: the ISD TAR carries no default keyset
self.store._presets[0]['tars'][0]['keyset'] = None
self.server.app = object()
status, resp = self._post('/api/send-ota', {
'apdu': '00A40000023F00', 'spi2': '01', 'preset_id': p['id']})
self.assertEqual(status, 400, resp)
self.assertIn('needs a default keyset', resp['error'])
def test_a_ram_install_refuses_an_undefined_key_version(self):
p = self.store.add(self._preset(iccid='8970119000004600098'))
status, resp = self._post('/api/ram-install', {
@@ -1200,8 +1245,15 @@ class PresetStoreHttpTests(unittest.TestCase):
self.assertEqual(sent[0].lower(), '0d00010000b0000000000000010000a40000023f00')
def test_the_tar_probe_classifies_each_tar(self):
p = self.store.add(self._preset(iccid='8970119000004600098', keysets=[
self._keyset(kic='25', kid='25', kicKey='AA', kidKey='BB', cntr='0000000005')]))
# v3.26.0: the probe checks the preset's TAR list only and each row
# uses its TAR's default keyset (a single-keyset preset auto-fills it)
p = self.store.add(self._preset(iccid='8970119000004600098',
tars=[{'role': 'isd', 'tar': '000000', 'msl': '16'},
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'},
{'tar': 'B00200', 'msl': '16', 'desc': 'Vendor'}],
keysets=[self._keyset(kic='25', kid='25', kicKey='AA', kidKey='BB',
cntr='0000000005')]))
seen = []
def fake_step(server, scc, sp, state, name, apdu, silent=False):
@@ -1222,8 +1274,7 @@ class PresetStoreHttpTests(unittest.TestCase):
self.server.app = object() # the probe only checks truthiness
with mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_step):
status, resp = self._post('/api/tar-probe', {
'preset_id': p['id'], 'spi1': '16', 'kic': '25', 'kid': '25',
'kicKey': 'AA', 'kidKey': 'BB', 'cntr': '0000000005',
'preset_id': p['id'], 'spi1': '16',
'tars': ['000000', 'B00000', 'B00001', 'B00200']})
self.assertEqual(status, 200, resp)
self.assertEqual(seen, ['000000', 'B00000', 'B00001', 'B00200'])
@@ -1233,19 +1284,32 @@ class PresetStoreHttpTests(unittest.TestCase):
self.assertEqual(resp['registered'], 2)
self.assertEqual(resp['total'], 4)
self.assertEqual(resp['kvn'], 2)
self.assertEqual(resp['results'][0]['label'], 'Issuer Security Domain (compact)')
self.assertEqual([r['kvn'] for r in resp['results']], [2, 2, 2, 2])
self.assertEqual(resp['results'][0]['label'], 'ISD')
self.assertEqual(resp['results'][3]['label'], 'Vendor')
self.assertEqual(resp['results'][0]['por_sw'], '6D00')
def test_the_tar_probe_needs_a_counter_check_and_a_defined_keyset(self):
def test_the_tar_probe_refuses_what_it_cannot_probe(self):
self.server.app = object()
status, resp = self._post('/api/tar-probe', {'spi1': '00', 'kic': '15', 'kid': '15'})
# a preset is required - the probe checks its TAR list
status, resp = self._post('/api/tar-probe', {'spi1': '16'})
self.assertEqual(status, 400, resp)
self.assertIn('preset_id', resp['error'])
p = self.store.add(self._preset(iccid='8970119000004600098'))
# a TAR outside the preset is refused (no arbitrary TARs)
status, resp = self._post('/api/tar-probe',
{'preset_id': p['id'], 'spi1': '16',
'tars': ['B0FFFF']})
self.assertEqual(status, 400, resp)
self.assertIn('not in the preset', resp['error'])
# an MSL 00 TAR has no counter check: the probe refuses it
p00 = self.store.add(self._preset(iccid='8970119000004600010',
tars=[{'role': 'isd', 'tar': '000000', 'msl': '00'},
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16'},
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16'}]))
status, resp = self._post('/api/tar-probe', {'preset_id': p00['id']})
self.assertEqual(status, 400, resp)
self.assertIn('counter check', resp['error'])
p = self.store.add(self._preset(iccid='8970119000004600098'))
status, resp = self._post('/api/tar-probe',
{'preset_id': p['id'], 'kic': '35', 'kid': '35'})
self.assertEqual(status, 400, resp)
self.assertIn('not defined', resp['error'])
def test_a_store_write_failure_answers_a_json_500(self):
with mock.patch.object(self.store, 'add', side_effect=OSError('read-only file system')):
+70 -2
View File
@@ -28,8 +28,20 @@ def keyset(**kw):
return ks
# The three role entries with their default keyset (v3.26.0) - the base for
# the multi-keyset fixtures, where no auto-fill happens.
ROLE_TARS = [{'role': role, 'tar': tar, 'msl': '16', 'keyset': 1}
for role, tar in (('isd', '000000'), ('uiccRfm', 'B00000'),
('usimRfm', 'B00001'))]
def preset(**kw):
p = {'name': 'C', 'keysets': [keyset()]}
if 'tars' not in kw:
# v3.26.0: the TARs carry their default keyset - the fixtures name
# keyset 1, so a multi-keyset preset still validates (a single-keyset
# one would be auto-filled anyway).
p['tars'] = [dict(t) for t in ROLE_TARS]
p.update(kw)
return p
@@ -174,6 +186,54 @@ class StoreCrudTests(unittest.TestCase):
self.assertEqual(self.store.find_keyset(p['id'], 2)['cntr'], '0000000005')
self.assertIsNone(self.store.find_keyset(p['id'], 3))
def test_a_tar_keyset_is_mandatory_for_a_nonzero_msl(self):
# v3.26.0: the counters are keyset-linked, so any non-zero MSL needs a
# default keyset; MSL 00 may omit it (the packet is built keyless) and
# may still carry one (some cards reject the '00/00' KIc/KID form)
two = [keyset(), keyset(kic='29', kid='29')]
with self.assertRaises(presets.PresetError) as ctx:
self.store.add(preset(keysets=two, tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '16', 'desc': 'push'}]))
self.assertIn('needs a default keyset', str(ctx.exception))
p = self.store.add(preset(keysets=two, tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '00', 'desc': 'push'}]))
self.assertIsNone(p['tars'][3]['keyset'])
p = self.store.add(preset(keysets=two, tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '00', 'keyset': 1}]))
self.assertEqual(p['tars'][3]['keyset'], 1)
def test_a_tar_keyset_must_exist_in_the_preset(self):
with self.assertRaises(presets.PresetError) as ctx:
self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '16', 'keyset': 3}]))
self.assertIn('does not define', str(ctx.exception))
def test_a_single_keyset_fills_the_tar_defaults(self):
# v3.26.0: one keyset in the preset sets every TAR's default keyset
p = self.store.add(preset(tars=[{'tar': 'B00011', 'msl': '16'},
{'tar': 'B00012', 'msl': '00'}]))
self.assertTrue(all(t['keyset'] == 1 for t in p['tars']))
# several keysets leave an MSL 00 entry alone...
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '00'}]))
self.assertIsNone(p['tars'][3]['keyset'])
# ...and a non-zero MSL must name one explicitly
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '16', 'keyset': 2}]))
self.assertEqual(p['tars'][3]['keyset'], 2)
def test_tar_keyset_string_forms_and_the_helper(self):
p = self.store.add(preset(keysets=[keyset(), keyset(kic='29', kid='29')],
tars=ROLE_TARS + [
{'tar': 'B00011', 'msl': '16', 'keyset': '02'},
{'tar': 'B00012', 'msl': '16', 'keyset': '2'}]))
self.assertEqual([t['keyset'] for t in p['tars']], [1, 1, 1, 2, 2])
self.assertEqual(presets.tar_keyset(p, 'B00011'), 2)
self.assertIsNone(presets.tar_keyset(p, 'B00099'))
def test_validation_requires_at_least_one_keyset(self):
with self.assertRaises(presets.PresetError) as ctx:
self.store.add(preset(keysets=[]))
@@ -271,11 +331,19 @@ class StoreCrudTests(unittest.TestCase):
self.assertEqual(up['id'], p['id'])
self.assertEqual(up['name'], 'Renamed')
self.assertEqual(up['keysets'][0]['kic'], '15')
# the keysets are replaceable as a whole (the Cards tab edit path)
up = self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
# replacing the keysets with another number must re-point the TARs'
# default keysets in the same update (the Cards-form edit path)
with self.assertRaises(presets.PresetError) as ctx:
self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
cntr='0000000009')]})
self.assertIn('does not define', str(ctx.exception))
tars = [dict(t, keyset=2) for t in up['tars']]
up = self.store.update(p['id'], {'keysets': [keyset(kic='29', kid='29',
cntr='0000000009')],
'tars': tars})
self.assertEqual(presets.keyset_kvn(up['keysets'][0]), 2)
self.assertEqual(up['keysets'][0]['cntr'], '0000000009')
self.assertTrue(all(t['keyset'] == 2 for t in up['tars']))
self.assertIsNone(self.store.update('nope', {'name': 'x'}))
def test_the_counter_must_be_hex_and_at_most_ten_digits(self):
+8 -6
View File
@@ -25,11 +25,12 @@ def _resolver(name):
return {n.upper(): c for c, n in S.PROACTIVE_TYPE_NAMES.items()}.get(name.upper())
def preset_tars(isd='000000', msl='16'):
"""The three mandatory role entries of a card preset's TAR table."""
return [{'role': 'isd', 'tar': isd, 'msl': msl, 'desc': ''},
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': ''},
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': ''}]
def preset_tars(isd='000000', msl='16', keyset=1):
"""The three mandatory role entries of a card preset's TAR table (v3.26.0:
each carries its default keyset - the fixtures name keyset 1)."""
return [{'role': 'isd', 'tar': isd, 'msl': msl, 'desc': '', 'keyset': keyset},
{'role': 'uiccRfm', 'tar': 'B00000', 'msl': '16', 'desc': '', 'keyset': keyset},
{'role': 'usimRfm', 'tar': 'B00001', 'msl': '16', 'desc': '', 'keyset': keyset}]
class TestValidation(unittest.TestCase):
@@ -1974,7 +1975,8 @@ class TestSuiteRunner(SuiteRunnerTestCase):
preset = {'id': 'p1',
'keysets': [{'kic': '15', 'kid': '15', 'kicKey': '00' * 16,
'kidKey': '00' * 16, 'cntr': '0000000010'}],
'tars': [{'role': 'isd', 'tar': 'B00000', 'msl': '16'}]}
'tars': [{'role': 'isd', 'tar': 'B00000', 'msl': '16',
'keyset': 1}]}
members = [({'script_id': 'a' * 32, 'role': 'member', 'on_fail': 'stop'}, script),
({'script_id': 'b' * 32, 'role': 'member', 'on_fail': 'stop'}, script)]
with mock.patch.object(S, '_build_secured_packet',