v3.20.0 (fix): the install-parameter composition - one System Specific Parameters EF
Two reported SCP80/RAM issues, both confirmed against the specs:
- docs/api.md called `stk_params` a "Hex CA TLV", but TS 102 226 8.2.1.3.2.1
puts the CA *inside* the System Specific Parameters EF ('EF'), and the PWA
builds `EF <len> CA <len> ...` (the SIM access domain leads the CA value as
`01 <ad>` / `00`). A client following the doc sent a CA at the wrong level.
- the RAM composition appended the STK part after a caller-composed
C9+EF(quotas) prefix, producing TWO EF objects. The card read the first
(the quotas) and ignored the CA: the install was accepted (por_ok) but the
toolkit parameters were never registered, so no STK menu appeared (live
2026-10-05). Per GPD_SPE_013 v1.1 Table 6-5 (pySim's
gen_install_parameters) the quotas (C7/C8) and the CA share ONE EF; an
EA-form STK part is a sibling of EF (the reference TCA loader's
`C9 00 EF 00 EA ...`).
- server: `_compose_install_params()` (used by `_cap_install_apdu` ->
`_cap_apdu_sequence`, hence /api/ram-install, /api/ram-install-app,
/api/scp81/gen-install and the PWA's install script template) enforces the
rule: a caller-composed EF plus an EF-form `stk_params`, a bare CA, a
malformed EF and quotas next to a non-empty `install_params` are refused
(400 / `{"ok": false}` with the fix named); with an empty `install_params`
the quotas and an EF-form CA are composed into one EF
(`C900 EF{C7?,C8?,CA}`), an EA part stays a sibling.
- the documented `nv_quota`/`volatile_quota` fields are now real (they were
named in the docs but implemented nowhere): integers composing C8/C7.
- PWA: `composeRamInstallParams()` composes the same single EF itself (a
CA-form STK field takes the quotas into its EF, the merged EF travels in
`stk_params`), the RAM install bodies send it, the placeholder shows it
with a note when the quotas moved into the STK EF, and a refused request
(no steps) reports the error instead of "FAILED at step: undefined".
- docs/api.md: the corrected `stk_params` description with both accepted
shapes, the composition rule paragraph with the refusal cases, the fixed
example (`C90000` -> `C900`), and the /api/ram-install-app and
/api/scp81/gen-install cross-references.
- tests: composition unit tests (merge/refusals/EA-sibling) and the
ram-install-app 400 paths (Python); `ramQuotaTlvs`/`composeRamInstallParams`/
`ramInstallParamsCompose` pins incl. the CA merge (Node).
Tests: 824 frontend + 740 Python; sw cache simple-v395.
This commit is contained in:
+30
-7
@@ -812,7 +812,7 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
|
|||||||
{
|
{
|
||||||
"cap_hex": "DECAFFED...",
|
"cap_hex": "DECAFFED...",
|
||||||
"sd_aid": "A000000003000000",
|
"sd_aid": "A000000003000000",
|
||||||
"install_params": "C90000",
|
"install_params": "C900",
|
||||||
"stk_params": "",
|
"stk_params": "",
|
||||||
"nv_quota": 0,
|
"nv_quota": 0,
|
||||||
"volatile_quota": 0,
|
"volatile_quota": 0,
|
||||||
@@ -830,14 +830,31 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `cap_hex` | yes | Even-length hex of the `.cap` file (zipped Java Card CAP), max 48 kB (98304 hex chars) |
|
| `cap_hex` | yes | Even-length hex of the `.cap` file (zipped Java Card CAP), max 48 kB (98304 hex chars) |
|
||||||
| `sd_aid` | no | Security Domain AID for INSTALL[for load]; empty → default ISD `A000000003000000` |
|
| `sd_aid` | no | Security Domain AID for INSTALL[for load]; empty → default ISD `A000000003000000` |
|
||||||
| `install_params` | no | Hex C9 TLV install parameters; if empty, `gen_install_parameters()` is used with the quota/stk params |
|
| `install_params` | no | Hex of the install-parameter field — the mandatory `C9` TLV (application-specific parameters; `C900` when empty), the System Specific Parameters `EF`, extra raw TLVs. When empty the server **composes** it from `volatile_quota`/`nv_quota` and `stk_params` (see below) |
|
||||||
| `stk_params` | no | Hex CA TLV (TS 102 226 §8.2.1.3.2.1) for SIM toolkit app-specific params |
|
| `stk_params` | no | The STK part appended to the install parameters: the **EF-wrapped** CA (`EF <len> CA <len> …`, TS 102 226 §8.2.1.3.2.1 — the SIM access domain leads the CA value as `01 <ad>` / `00`) or the UICC template (`EA <len> 80 … [81 …]`). A bare `CA` is refused (it belongs inside the EF) |
|
||||||
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) for `gen_install_parameters()` |
|
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) coded as `C8` / `C7` in the install EF; only composed when `install_params` is empty |
|
||||||
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
|
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
|
||||||
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
||||||
| `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it |
|
| `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it |
|
||||||
| `includeCpi` | no | As in `/api/send-ota`: `true` (default) marks the delivered SMS with the OTA CPI IE; `false` drops it (the full 140-octet single-SM budget then applies) |
|
| `includeCpi` | no | As in `/api/send-ota`: `true` (default) marks the delivered SMS with the OTA CPI IE; `false` drops it (the full 140-octet single-SM budget then applies) |
|
||||||
|
|
||||||
|
**Install-parameter composition (TS 102 226 §8.2.1.3.2.1; GPD_SPE_013 v1.1
|
||||||
|
Table 6-5).** The memory quotas (`C7`/`C8`) and an EF-form STK part (the CA)
|
||||||
|
must share **one** System Specific Parameters `EF`; an EA-form STK part is a
|
||||||
|
sibling of `EF` (the reference TCA loader sends `C9 00 EF 00 EA …`). A
|
||||||
|
second `EF` makes the card read the quotas and ignore the CA (live
|
||||||
|
2026-10-05: `por_ok` but no toolkit registration and no menu), so the server
|
||||||
|
refuses the combination with a 400 and names the fix:
|
||||||
|
|
||||||
|
- caller-composed `install_params` that already carry an `EF` **plus** an
|
||||||
|
EF-form `stk_params` → refused;
|
||||||
|
- `nv_quota`/`volatile_quota` next to non-empty `install_params` → refused;
|
||||||
|
- with an empty `install_params` the server composes
|
||||||
|
`C900 EF{C7?,C8?,CA}` (or `C900 EF{quotas} EA…` for the EA form).
|
||||||
|
|
||||||
|
The PWA composes the same single EF itself, so its RAM installs never hit the
|
||||||
|
refusal.
|
||||||
|
|
||||||
**Response (success):**
|
**Response (success):**
|
||||||
```json
|
```json
|
||||||
{"success": true, "failed_step": null,
|
{"success": true, "failed_step": null,
|
||||||
@@ -922,7 +939,8 @@ Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**)
|
|||||||
| `module_aid` | cond | Executable module / applet class AID (required for `mode: "install"`) |
|
| `module_aid` | cond | Executable module / applet class AID (required for `mode: "install"`) |
|
||||||
| `instance_aid` | no | Application AID; empty → the module AID |
|
| `instance_aid` | no | Application AID; empty → the module AID |
|
||||||
| `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` |
|
| `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` |
|
||||||
| `install_params` / `stk_params` | no | As in `/api/ram-install` (the PWA composes `C9`+`EF`+raw and appends the STK part) |
|
| `install_params` / `stk_params` | no | As in `/api/ram-install`: the `C9` field (with any extra TLVs) and the STK part; an EF-form STK part must not follow a caller-composed `EF` (the quotas and the CA share one `EF`, v3.20.0) |
|
||||||
|
| `nv_quota` / `volatile_quota` | no | As in `/api/ram-install`: composed into the install `EF` only when `install_params` is empty |
|
||||||
| `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` |
|
| `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` |
|
||||||
| `includeCpi` | no | As in `/api/ram-install`: `true` (default) marks the delivered SMS with the OTA CPI IE |
|
| `includeCpi` | no | As in `/api/ram-install`: `true` (default) marks the delivered SMS with the OTA CPI IE |
|
||||||
|
|
||||||
@@ -1502,10 +1520,15 @@ the returned list. The `.cap` is parsed server-side (same parser as
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00",
|
{"cap_hex": "504B0304...", "sd_aid": "A000000003000000", "privileges": "00",
|
||||||
"install_params": "", "stk_params": "", "make_selectable": true}
|
"install_params": "", "stk_params": "", "nv_quota": 0, "volatile_quota": 0,
|
||||||
|
"make_selectable": true}
|
||||||
```
|
```
|
||||||
|
|
||||||
`sd_aid` empty = the ISD. Responds with `{"ok": true, "apdus": [...],
|
`sd_aid` empty = the ISD. `install_params`, `stk_params` and the
|
||||||
|
`nv_quota`/`volatile_quota` primitives follow the composition rule of
|
||||||
|
`/api/ram-install` (the quotas and an EF-form STK part share one System
|
||||||
|
Specific Parameters `EF`); an invalid combination answers
|
||||||
|
`{"ok": false, "error": "..."}`. Responds with `{"ok": true, "apdus": [...],
|
||||||
"load_file_aid": ..., "module_aid": ...}`.
|
"load_file_aid": ..., "module_aid": ...}`.
|
||||||
|
|
||||||
### `GET /api/scp81/script`
|
### `GET /api/scp81/script`
|
||||||
|
|||||||
+75
-32
@@ -2783,10 +2783,13 @@ function ramResetGrants() {
|
|||||||
|
|
||||||
// Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
|
// Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
|
||||||
// form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
|
// form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
|
||||||
// `v` carries the field values; returns the CA/EA TLV hex, or null when a
|
// `v` carries the field values; returns the STK part of the install
|
||||||
// value cannot be coded: each TAR is 3 bytes (6 hex digits), and menu item
|
// parameters - the EF-wrapped CA (`EF { CA ... }`, TS 102 226 8.2.1.3.2.1;
|
||||||
// identifiers 128..255 are reserved for the toolkit framework, so only
|
// composeRamInstallParams() merges the memory quotas into that EF) or the
|
||||||
// 01..7F may be requested (TS 102 226 8.2.1.3.2.3). In EA mode `v.fsAccess`
|
// top-level EA template - or null when a value cannot be coded: each TAR is
|
||||||
|
// 3 bytes (6 hex digits), and menu item identifiers 128..255 are reserved for
|
||||||
|
// the toolkit framework, so only 01..7F may be requested (TS 102 226
|
||||||
|
// 8.2.1.3.2.3). In EA mode `v.fsAccess`
|
||||||
// adds the UICC file-access parameters (tag '81') and `v.adfAccess` extends
|
// adds the UICC file-access parameters (tag '81') and `v.adfAccess` extends
|
||||||
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
|
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
|
||||||
// grants access via the CA Access Domain field instead.
|
// grants access via the CA Access Domain field instead.
|
||||||
@@ -2888,6 +2891,7 @@ function updateStkParamsHex() {
|
|||||||
// field is edited again (this clears the flag).
|
// field is edited again (this clears the flag).
|
||||||
if (el.dataset) delete el.dataset.manual;
|
if (el.dataset) delete el.dataset.manual;
|
||||||
// A rejected value must not vanish silently: say why the field is empty.
|
// A rejected value must not vanish silently: say why the field is empty.
|
||||||
|
updateInstallParamsHex();
|
||||||
const hint = document.getElementById('ram-stk-hint');
|
const hint = document.getElementById('ram-stk-hint');
|
||||||
if (!hint) return;
|
if (!hint) return;
|
||||||
if (tk === null) {
|
if (tk === null) {
|
||||||
@@ -2956,35 +2960,68 @@ function ramQuotaValue(n) {
|
|||||||
: '04' + v.toString(16).padStart(8, '0').toUpperCase();
|
: '04' + v.toString(16).padStart(8, '0').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The applet-specific prefix of the install parameters, in the vendor's
|
// The quota TLVs of the install EF: `C7 <len> <quota>` (volatile) and
|
||||||
// order: `C9 <applet-specific parameters>` + `EF <C7/C8 memory quotas>`.
|
// `C8 <len> <quota>` (non-volatile), in the vendor's order.
|
||||||
// The server appends the STK (`EA`/`EF+CA`) part after it. C9 is mandatory
|
function ramQuotaTlvs(c7, c8) {
|
||||||
// in the Install Parameters (GP Table 11-49), so an empty field is sent as
|
let out = '';
|
||||||
// `C9 00` - like the reference tools; a prefix without C9 makes the card
|
if (String(c7 ?? '') !== '' && !isNaN(parseInt(c7, 10))) out += 'C7' + ramQuotaValue(c7);
|
||||||
// reject the data field with 6A80 (live 2026-09-28).
|
if (String(c8 ?? '') !== '' && !isNaN(parseInt(c8, 10))) out += 'C8' + ramQuotaValue(c8);
|
||||||
function ramParamsPrefix(c9Hex, c7, c8) {
|
|
||||||
const c9 = (c9Hex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
|
||||||
let out = 'C9' + berLenStr(c9.length / 2) + c9;
|
|
||||||
let ef = '';
|
|
||||||
if (String(c7 ?? '') !== '' && !isNaN(parseInt(c7, 10))) ef += 'C7' + ramQuotaValue(c7);
|
|
||||||
if (String(c8 ?? '') !== '' && !isNaN(parseInt(c8, 10))) ef += 'C8' + ramQuotaValue(c8);
|
|
||||||
if (ef) out += 'EF' + berLenStr(ef.length / 2) + ef;
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ramInstallParamsPrefix() {
|
// Compose the INSTALL [for install] parameters. The memory quotas (C7/C8)
|
||||||
return ramParamsPrefix(
|
// and an EF-form STK part (the CA) share ONE System Specific Parameters EF
|
||||||
document.getElementById('rc-c9').value,
|
// (TS 102 226 8.2.1.3.2.1; GPD_SPE_013 v1.1 Table 6-5 - pySim's
|
||||||
document.getElementById('rc-quota-c7').value,
|
// gen_install_parameters); an EA-form (or opaque) STK part is a sibling of
|
||||||
document.getElementById('rc-quota-c8').value);
|
// EF, like the reference TCA loader's `C9 00 EF 00 EA ...`. A second EF
|
||||||
|
// makes the card read the quotas and ignore the CA (live 2026-10-05: por_ok
|
||||||
|
// but no toolkit registration, no menu), so the combination is refused
|
||||||
|
// server-side: never send two.
|
||||||
|
// returns {install_params, stk_params}
|
||||||
|
function composeRamInstallParams(c9Hex, c7, c8, extraHex, stkHex) {
|
||||||
|
const c9 = (c9Hex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
const extra = (extraHex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
const stk = (stkHex || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
const quota = ramQuotaTlvs(c7, c8);
|
||||||
|
const installParams = 'C9' + berLenStr(c9.length / 2) + c9;
|
||||||
|
// C9 is mandatory (GP Table 11-49): an empty field is sent as `C9 00`.
|
||||||
|
const stkTlvs = stk ? parseTlvList(stk) : [];
|
||||||
|
const stkEf = (stkTlvs.length === 1 && stkTlvs[0].tag === 'EF' &&
|
||||||
|
stkTlvs[0].raw === stk) ? stkTlvs[0] : null;
|
||||||
|
if (stkEf) {
|
||||||
|
// the quotas belong inside this EF; the STK field carries the merged
|
||||||
|
// EF and the server appends it (one EF in the final parameters)
|
||||||
|
const inner = quota + stkEf.value;
|
||||||
|
return {
|
||||||
|
install_params: installParams + extra,
|
||||||
|
stk_params: 'EF' + berLenStr(inner.length / 2) + inner,
|
||||||
|
quota_in_stk: !!quota,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
install_params: installParams + (quota ? 'EF' + berLenStr(quota.length / 2) + quota : '') + extra,
|
||||||
|
stk_params: stk,
|
||||||
|
quota_in_stk: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The RAM install form's composed install parameters.
|
||||||
|
function ramInstallParamsCompose() {
|
||||||
|
const val = id => (document.getElementById(id) || {}).value || '';
|
||||||
|
return composeRamInstallParams(
|
||||||
|
val('rc-c9'), val('rc-quota-c7'), val('rc-quota-c8'),
|
||||||
|
val('ram-install-params-hex'), val('ram-stk-params'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInstallParamsHex() {
|
function updateInstallParamsHex() {
|
||||||
const prefix = ramInstallParamsPrefix();
|
|
||||||
const el = document.getElementById('ram-install-params-hex');
|
const el = document.getElementById('ram-install-params-hex');
|
||||||
// The field carries extra raw TLVs; the generated prefix is shown in the
|
if (!el) return;
|
||||||
// placeholder so the composed parameters stay visible.
|
const composed = ramInstallParamsCompose();
|
||||||
el.placeholder = prefix ? prefix + ' …' : 'extra TLVs (optional)';
|
// The field carries extra raw TLVs; the generated prefix (with an EF-form
|
||||||
|
// STK part already carrying the quotas) is shown in the placeholder.
|
||||||
|
let text = composed.install_params ? composed.install_params + ' …' : 'extra TLVs (optional)';
|
||||||
|
if (composed.quota_in_stk) text += ' (' + t('quotas in the STK EF') + ')';
|
||||||
|
el.placeholder = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== GSM 7-bit alphabet =====
|
// ===== GSM 7-bit alphabet =====
|
||||||
@@ -11210,14 +11247,14 @@ async function ramInstallCap(sp) {
|
|||||||
// never be sent silently.
|
// never be sent silently.
|
||||||
updateRcPriv();
|
updateRcPriv();
|
||||||
|
|
||||||
|
const composed = ramInstallParamsCompose();
|
||||||
const body = {
|
const body = {
|
||||||
cap_hex: capHex,
|
cap_hex: capHex,
|
||||||
preset_id: sp.preset_id,
|
preset_id: sp.preset_id,
|
||||||
sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
sd_aid: (document.getElementById('ram-sd-aid').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
||||||
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
||||||
install_params: (ramInstallParamsPrefix() +
|
install_params: composed.install_params,
|
||||||
(document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, '')).toUpperCase(),
|
stk_params: composed.stk_params,
|
||||||
stk_params: (document.getElementById('ram-stk-params').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
|
||||||
make_selectable: document.getElementById('ram-make-sel').checked,
|
make_selectable: document.getElementById('ram-make-sel').checked,
|
||||||
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
|
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
|
||||||
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
||||||
@@ -11263,6 +11300,7 @@ async function ramInstallApp(sp, op) {
|
|||||||
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
||||||
updateStkParamsHex();
|
updateStkParamsHex();
|
||||||
}
|
}
|
||||||
|
const composed = ramInstallParamsCompose();
|
||||||
const body = {
|
const body = {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
preset_id: sp.preset_id,
|
preset_id: sp.preset_id,
|
||||||
@@ -11270,9 +11308,8 @@ async function ramInstallApp(sp, op) {
|
|||||||
module_aid: module,
|
module_aid: module,
|
||||||
instance_aid: instance,
|
instance_aid: instance,
|
||||||
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
|
||||||
install_params: (ramInstallParamsPrefix() +
|
install_params: composed.install_params,
|
||||||
(document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, '')).toUpperCase(),
|
stk_params: composed.stk_params,
|
||||||
stk_params: (document.getElementById('ram-stk-params').value || '').replace(/[^0-9a-fA-F]/g, ''),
|
|
||||||
make_selectable: document.getElementById('ram-make-sel').checked,
|
make_selectable: document.getElementById('ram-make-sel').checked,
|
||||||
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
|
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
|
||||||
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
||||||
@@ -11719,6 +11756,10 @@ function ramShowInstallResult(data, okText, capMem) {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
resultEl.textContent = (okText || t('OK')) + probeNote;
|
resultEl.textContent = (okText || t('OK')) + probeNote;
|
||||||
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
|
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
|
||||||
|
} else if (!data.steps && data.error) {
|
||||||
|
// a refused request (400): nothing was sent to the card
|
||||||
|
resultEl.textContent = t('Install FAILED:') + ' ' + data.error;
|
||||||
|
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||||
} else {
|
} else {
|
||||||
const hint = ramInstallFailHint(data, capMem || null);
|
const hint = ramInstallFailHint(data, capMem || null);
|
||||||
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '') +
|
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '') +
|
||||||
@@ -20824,6 +20865,8 @@ const LANG_RU = {
|
|||||||
'Step': 'Шаг',
|
'Step': 'Шаг',
|
||||||
'Install OK': 'Установка OK',
|
'Install OK': 'Установка OK',
|
||||||
'Install FAILED at step:': 'Установка не удалась на шаге:',
|
'Install FAILED at step:': 'Установка не удалась на шаге:',
|
||||||
|
'Install FAILED:': 'Установка не удалась:',
|
||||||
|
'quotas in the STK EF': 'квоты в EF STK-параметров',
|
||||||
'Hint:': 'Подсказка:',
|
'Hint:': 'Подсказка:',
|
||||||
'Set all 0.0': 'Все 0.0',
|
'Set all 0.0': 'Все 0.0',
|
||||||
'CAP compatibility test': 'Проверка совместимости CAP',
|
'CAP compatibility test': 'Проверка совместимости CAP',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v394';
|
const CACHE = 'simple-v395';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ function extractFunc(src, name) {
|
|||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const fn of ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams',
|
for (const fn of ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams',
|
||||||
'updateStkParamsHex', 'adfAidFromFci', 'ramParamsPrefix', 'ramQuotaValue',
|
'updateStkParamsHex', 'updateInstallParamsHex', 'adfAidFromFci', 'ramQuotaValue', 'ramQuotaTlvs',
|
||||||
'ramInstallParamsPrefix']) code += extractFunc(html, fn) + '\n';
|
'composeRamInstallParams', 'ramInstallParamsCompose',
|
||||||
|
'parseTlvList', 'parseBerLen']) code += extractFunc(html, fn) + '\n';
|
||||||
eval(code);
|
eval(code);
|
||||||
|
|
||||||
const base = { mode: 'ea', priority: '0', timers: '0', textLen: '0', menus: '0',
|
const base = { mode: 'ea', priority: '0', timers: '0', textLen: '0', menus: '0',
|
||||||
@@ -128,24 +129,61 @@ test('adfAidFromFci extracts the DF name (tag 84) from an FCI', () => {
|
|||||||
assert.strictEqual(adfAidFromFci('ZZZZ'), '');
|
assert.strictEqual(adfAidFromFci('ZZZZ'), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ramParamsPrefix composes C9 + EF (C7/C8) like the vendor scripts', () => {
|
test('composeRamInstallParams composes C9 + EF (C7/C8) like the vendor scripts', () => {
|
||||||
// The vendor's working install (samples/uicc/applets/STK3):
|
// The vendor's working install (samples/uicc/applets/STK3):
|
||||||
// C9 22 <config> EF 08 C7 02 0000 C8 02 0000 EA ...
|
// C9 22 <config> EF 08 C7 02 0000 C8 02 0000 EA ...
|
||||||
// C9 is mandatory (GP Table 11-49): an empty field is sent as C9 00 -
|
// C9 is mandatory (GP Table 11-49): an empty field is sent as C9 00 -
|
||||||
// without it the card rejects the data field with 6A80 (live 2026-09-28).
|
// without it the card rejects the data field with 6A80 (live 2026-09-28).
|
||||||
assert.strictEqual(ramParamsPrefix('', '', ''), 'C900');
|
assert.strictEqual(composeRamInstallParams('', '', '', '', '').install_params, 'C900');
|
||||||
assert.strictEqual(ramParamsPrefix('082905112000012066', '', ''),
|
assert.strictEqual(
|
||||||
|
composeRamInstallParams('082905112000012066', '', '', '', '').install_params,
|
||||||
'C909082905112000012066');
|
'C909082905112000012066');
|
||||||
assert.strictEqual(ramParamsPrefix('', '0', '0'), 'C900EF08C7020000C8020000');
|
assert.strictEqual(composeRamInstallParams('', '0', '0', '', '').install_params,
|
||||||
assert.strictEqual(ramParamsPrefix('', '32768', ''), 'C900EF06C70400008000');
|
'C900EF08C7020000C8020000');
|
||||||
assert.strictEqual(ramParamsPrefix('AA', '', '1234'), 'C901AAEF04C80204D2');
|
assert.strictEqual(composeRamInstallParams('', '32768', '', '', '').install_params,
|
||||||
|
'C900EF06C70400008000');
|
||||||
|
assert.strictEqual(composeRamInstallParams('AA', '', '1234', '', '').install_params,
|
||||||
|
'C901AAEF04C80204D2');
|
||||||
|
// extra raw TLVs follow the generated prefix
|
||||||
|
assert.strictEqual(composeRamInstallParams('', '', '', 'CB02AABB', '').install_params,
|
||||||
|
'C900CB02AABB');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the RAM form composes the C9/quota prefix into the install parameters', () => {
|
test('an EF-form STK part takes the quotas into its own EF (one EF, TS 102 226 8.2.1.3.2.1)', () => {
|
||||||
|
// Two EFs made the card read the quotas and ignore the CA (live
|
||||||
|
// 2026-10-05: por_ok but no STK menu): the CA and the quotas share one
|
||||||
|
// System Specific Parameters EF (GPD_SPE_013 Table 6-5).
|
||||||
|
const ca = stkParamsBuild(vals({ mode: 'ca' })); // EF0ACA08...
|
||||||
|
assert.strictEqual(ca, 'EF0ACA080000000000000000');
|
||||||
|
const c = composeRamInstallParams('', '100', '', '', ca);
|
||||||
|
assert.strictEqual(c.install_params, 'C900');
|
||||||
|
assert.strictEqual(c.stk_params, 'EF0EC7020064CA080000000000000000');
|
||||||
|
assert.strictEqual(c.quota_in_stk, true);
|
||||||
|
// without quotas the EF passes through unchanged
|
||||||
|
const c2 = composeRamInstallParams('', '', '', '', ca);
|
||||||
|
assert.strictEqual(c2.install_params, 'C900');
|
||||||
|
assert.strictEqual(c2.stk_params, ca);
|
||||||
|
assert.strictEqual(c2.quota_in_stk, false);
|
||||||
|
// an EA-form STK part is a sibling of EF: the quotas keep their own EF
|
||||||
|
const ea = stkParamsBuild(vals({ mode: 'ea' }));
|
||||||
|
const c3 = composeRamInstallParams('', '100', '', '', ea);
|
||||||
|
assert.strictEqual(c3.install_params, 'C900EF04C7020064');
|
||||||
|
assert.strictEqual(c3.stk_params, ea);
|
||||||
|
assert.strictEqual(c3.quota_in_stk, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the RAM form composes the install parameters from its fields', () => {
|
||||||
const els = fakeForm({ 'rc-c9': 'AA', 'rc-quota-c7': '0' });
|
const els = fakeForm({ 'rc-c9': 'AA', 'rc-quota-c7': '0' });
|
||||||
assert.strictEqual(ramInstallParamsPrefix(), 'C901AAEF04C7020000');
|
assert.deepStrictEqual(ramInstallParamsCompose(),
|
||||||
|
{ install_params: 'C901AAEF04C7020000', stk_params: '', quota_in_stk: false });
|
||||||
els['rc-quota-c8'].value = '5';
|
els['rc-quota-c8'].value = '5';
|
||||||
assert.strictEqual(ramInstallParamsPrefix(), 'C901AAEF08C7020000C8020005');
|
assert.strictEqual(ramInstallParamsCompose().install_params, 'C901AAEF08C7020000C8020005');
|
||||||
|
// a CA-form STK field takes the quotas into its EF
|
||||||
|
els['ram-stk-params'].value = 'EF0ACA080000000000000000';
|
||||||
|
const c = ramInstallParamsCompose();
|
||||||
|
assert.strictEqual(c.install_params, 'C901AA');
|
||||||
|
assert.strictEqual(c.stk_params, 'EF12C7020000C8020005CA080000000000000000');
|
||||||
|
assert.strictEqual(c.quota_in_stk, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateStkParamsHex names the rejection reason', () => {
|
test('updateStkParamsHex names the rejection reason', () => {
|
||||||
@@ -166,7 +204,12 @@ test('updateStkParamsHex names the rejection reason', () => {
|
|||||||
|
|
||||||
test('the install body composes the C9/quota prefix', () => {
|
test('the install body composes the C9/quota prefix', () => {
|
||||||
const fn = extractFunc(html, 'ramInstallCap');
|
const fn = extractFunc(html, 'ramInstallCap');
|
||||||
assert.ok(/install_params: \(ramInstallParamsPrefix\(\)/.test(fn), fn);
|
assert.ok(/const composed = ramInstallParamsCompose\(\)/.test(fn), fn);
|
||||||
|
assert.ok(/install_params: composed\.install_params/.test(fn), fn);
|
||||||
|
assert.ok(/stk_params: composed\.stk_params/.test(fn), fn);
|
||||||
|
const app = extractFunc(html, 'ramInstallApp');
|
||||||
|
assert.ok(/const composed = ramInstallParamsCompose\(\)/.test(app), app);
|
||||||
|
assert.ok(/install_params: composed\.install_params/.test(app), app);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the ADF AID From-card button is wired', () => {
|
test('the ADF AID From-card button is wired', () => {
|
||||||
|
|||||||
+139
-17
@@ -724,16 +724,112 @@ def _encode_scts(dt=None):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _single_ef_value(hex_str):
|
||||||
|
"""The value of a lone EF (System Specific Parameters) TLV, or None when
|
||||||
|
the input is not EF-form at all. A blob that starts with the EF tag but
|
||||||
|
does not parse as exactly one TLV is refused - appending it could add a
|
||||||
|
second EF object (TS 102 226 8.2.1.3.2.1), and a bare CA is refused too:
|
||||||
|
the CA belongs *inside* the EF."""
|
||||||
|
if not hex_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = bytes.fromhex(hex_str)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('stk_params is not valid hex')
|
||||||
|
if data[0] == 0xCA:
|
||||||
|
raise ValueError('the CA (SIM file access and toolkit parameters) '
|
||||||
|
'belongs inside the System Specific Parameters EF '
|
||||||
|
'(TS 102 226 8.2.1.3.2.1): send EF <len> CA ...')
|
||||||
|
if data[0] != 0xEF:
|
||||||
|
return None
|
||||||
|
ln, off = _ber_len_at(data, 1)
|
||||||
|
if off + ln != len(data):
|
||||||
|
raise ValueError('stk_params: malformed EF TLV (the length does not '
|
||||||
|
'match the value)')
|
||||||
|
return data[off:off + ln].hex().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _quota_tlv(tag, value):
|
||||||
|
"""A C7/C8 memory-quota TLV: the value is a 2-byte integer up to 32767
|
||||||
|
and a 4-byte integer above (GP Card Spec 9.7 / Table 11-49; pySim's
|
||||||
|
``StripHeaderAdapter(GreedyBytes, 4, steps=[2,4])``)."""
|
||||||
|
try:
|
||||||
|
v = int(str(value).strip(), 10)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValueError('%s quota must be an integer in bytes' % tag)
|
||||||
|
if not 0 <= v <= 0xFFFFFFFF:
|
||||||
|
raise ValueError('%s quota out of range (0..4294967295): %d' % (tag, v))
|
||||||
|
return tag + ('%02X%04X' % (2, v) if v <= 0x7FFF else '%02X%08X' % (4, v))
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_install_params(install_params, stk_params, nv_quota=None,
|
||||||
|
volatile_quota=None):
|
||||||
|
"""The INSTALL [for install] parameter field, with the composition the
|
||||||
|
spec mandates enforced:
|
||||||
|
|
||||||
|
- the memory quotas (C7 volatile / C8 non-volatile) and an EF-form STK
|
||||||
|
part (the CA) share **one** System Specific Parameters EF (TS 102 226
|
||||||
|
8.2.1.3.2.1; GPD_SPE_013 v1.1 Table 6-5 - pySim's
|
||||||
|
``gen_install_parameters``);
|
||||||
|
- an EF-form STK part next to a caller-composed EF is refused: the card
|
||||||
|
takes the first EF (quotas) and the CA is ignored - live 2026-10-05:
|
||||||
|
por_ok but no toolkit registration, no menu;
|
||||||
|
- an EA-form (or opaque) STK part is a sibling of EF (the reference TCA
|
||||||
|
loader sends ``C9 00 EF 00 EA ...``);
|
||||||
|
- the quotas are composed from the primitives only when no
|
||||||
|
``install_params`` were given (there is no EF to place them into
|
||||||
|
otherwise).
|
||||||
|
|
||||||
|
Raises ValueError (-> HTTP 400) on a refusal.
|
||||||
|
"""
|
||||||
|
raw = (install_params or '').replace(' ', '')
|
||||||
|
stk = (stk_params or '').replace(' ', '')
|
||||||
|
has_quota = (str(nv_quota or '').strip() != ''
|
||||||
|
or str(volatile_quota or '').strip() != '')
|
||||||
|
stk_ef = _single_ef_value(stk)
|
||||||
|
if raw:
|
||||||
|
if has_quota:
|
||||||
|
raise ValueError('memory quotas are composed only when '
|
||||||
|
'install_params is empty; merge them into the '
|
||||||
|
'install EF instead')
|
||||||
|
try:
|
||||||
|
tlvs = _ber_tlv_list(bytes.fromhex(raw))
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('install_params is not valid hex')
|
||||||
|
if stk_ef is not None and any(tag == 0xEF for tag, _ in tlvs):
|
||||||
|
raise ValueError('the EF-form STK part would add a second System '
|
||||||
|
'Specific Parameters (EF) object (TS 102 226 '
|
||||||
|
'8.2.1.3.2.1): merge the CA into the install '
|
||||||
|
'parameters\' EF, or pass the quotas via '
|
||||||
|
'volatile_quota/nv_quota with an empty '
|
||||||
|
'install_params')
|
||||||
|
return (raw + stk).upper()
|
||||||
|
# Compose from the primitives: C9 is mandatory (GP Table 11-49).
|
||||||
|
inner = ''
|
||||||
|
if str(volatile_quota or '').strip() != '':
|
||||||
|
inner += _quota_tlv('C7', volatile_quota)
|
||||||
|
if str(nv_quota or '').strip() != '':
|
||||||
|
inner += _quota_tlv('C8', nv_quota)
|
||||||
|
if stk_ef is not None:
|
||||||
|
inner += stk_ef
|
||||||
|
stk = ''
|
||||||
|
params = 'C900'
|
||||||
|
if inner:
|
||||||
|
params += 'EF' + _ber_len(len(inner) // 2) + inner
|
||||||
|
return (params + stk).upper()
|
||||||
|
|
||||||
|
|
||||||
def _cap_install_apdu(loadfile_aid, module_aid, instance_aid='', privileges='00',
|
def _cap_install_apdu(loadfile_aid, module_aid, instance_aid='', privileges='00',
|
||||||
install_params='', stk_params='', make_selectable=True):
|
install_params='', stk_params='', make_selectable=True,
|
||||||
|
nv_quota=None, volatile_quota=None):
|
||||||
"""INSTALL [for install] APDU (GP Card Spec 11.5.2.3.2, Table 11-43) - the
|
"""INSTALL [for install] APDU (GP Card Spec 11.5.2.3.2, Table 11-43) - the
|
||||||
final step of `_cap_apdu_sequence` and the /api/ram-install-app operation.
|
final step of `_cap_apdu_sequence` and the /api/ram-install-app operation.
|
||||||
Case 3: no trailing Le (a trailing byte becomes a phantom command on the
|
Case 3: no trailing Le (a trailing byte becomes a phantom command on the
|
||||||
card's SCP80 layer)."""
|
card's SCP80 layer)."""
|
||||||
instance = instance_aid or module_aid
|
instance = instance_aid or module_aid
|
||||||
params = install_params if install_params else 'C900'
|
params = _compose_install_params(install_params, stk_params,
|
||||||
if stk_params:
|
nv_quota=nv_quota,
|
||||||
params += stk_params
|
volatile_quota=volatile_quota)
|
||||||
p1 = 0x0C if make_selectable else 0x04
|
p1 = 0x0C if make_selectable else 0x04
|
||||||
data = (_lv(loadfile_aid) + _lv(module_aid) + _lv(instance) +
|
data = (_lv(loadfile_aid) + _lv(module_aid) + _lv(instance) +
|
||||||
_lv(privileges or '00') + _lv(params) + '00')
|
_lv(privileges or '00') + _lv(params) + '00')
|
||||||
@@ -750,7 +846,8 @@ def _cap_make_selectable_apdu(instance_aid, privileges='00'):
|
|||||||
|
|
||||||
def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
||||||
privileges='00', install_params='', stk_params='',
|
privileges='00', install_params='', stk_params='',
|
||||||
make_selectable=True, block_size=240, instance_aid=None):
|
make_selectable=True, block_size=240, instance_aid=None,
|
||||||
|
nv_quota=None, volatile_quota=None):
|
||||||
"""RAM (GP) APDU sequence for a parsed .cap: INSTALL [for load], LOAD
|
"""RAM (GP) APDU sequence for a parsed .cap: INSTALL [for load], LOAD
|
||||||
blocks (240-byte payloads, block counter in P2, last block P1=0x80),
|
blocks (240-byte payloads, block counter in P2, last block P1=0x80),
|
||||||
INSTALL [for install]. Shared by the SCP80 delivery path and the SCP81
|
INSTALL [for install]. Shared by the SCP80 delivery path and the SCP81
|
||||||
@@ -776,7 +873,8 @@ def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
|||||||
apdus.append(_cap_install_apdu(
|
apdus.append(_cap_install_apdu(
|
||||||
loadfile_aid, module_aid, instance_aid=instance_aid,
|
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||||
privileges=privileges, install_params=install_params,
|
privileges=privileges, install_params=install_params,
|
||||||
stk_params=stk_params, make_selectable=make_selectable))
|
stk_params=stk_params, make_selectable=make_selectable,
|
||||||
|
nv_quota=nv_quota, volatile_quota=volatile_quota))
|
||||||
return apdus
|
return apdus
|
||||||
|
|
||||||
|
|
||||||
@@ -3677,7 +3775,11 @@ def _scp81_gen_install(body):
|
|||||||
privileges=re.sub(r'\s', '', body.get('privileges') or '') or '00',
|
privileges=re.sub(r'\s', '', body.get('privileges') or '') or '00',
|
||||||
install_params=re.sub(r'\s', '', body.get('install_params') or ''),
|
install_params=re.sub(r'\s', '', body.get('install_params') or ''),
|
||||||
stk_params=re.sub(r'\s', '', body.get('stk_params') or ''),
|
stk_params=re.sub(r'\s', '', body.get('stk_params') or ''),
|
||||||
make_selectable=bool(body.get('make_selectable', True)))
|
make_selectable=bool(body.get('make_selectable', True)),
|
||||||
|
nv_quota=body.get('nv_quota'), volatile_quota=body.get('volatile_quota'))
|
||||||
|
except ValueError as e:
|
||||||
|
# the install-parameter composition rule (or a CAP parse refusal)
|
||||||
|
return {'ok': False, 'error': str(e)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'ok': False, 'error': 'cap parse failed: %s' % e}
|
return {'ok': False, 'error': 'cap parse failed: %s' % e}
|
||||||
_BIP.log('gen-install', apdus=len(seq), load_file_aid=loadfile_aid,
|
_BIP.log('gen-install', apdus=len(seq), load_file_aid=loadfile_aid,
|
||||||
@@ -8111,12 +8213,24 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||||
'include_cpi': body.get('includeCpi', True)}
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
|
|
||||||
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install].
|
||||||
seq = _cap_apdu_sequence(
|
# The install parameters are composed per the spec: an
|
||||||
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
# EF-form STK part shares one System Specific Parameters EF
|
||||||
privileges=privileges_hex,
|
# with the quotas (TS 102 226 8.2.1.3.2.1; v3.20.0) - an
|
||||||
install_params=install_params_hex, stk_params=stk_params_hex,
|
# invalid combination is refused before anything is sent.
|
||||||
make_selectable=make_selectable, block_size=block_size)
|
try:
|
||||||
|
seq = _cap_apdu_sequence(
|
||||||
|
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
||||||
|
privileges=privileges_hex,
|
||||||
|
install_params=install_params_hex, stk_params=stk_params_hex,
|
||||||
|
make_selectable=make_selectable, block_size=block_size,
|
||||||
|
nv_quota=body.get('nv_quota'),
|
||||||
|
volatile_quota=body.get('volatile_quota'))
|
||||||
|
except ValueError as e:
|
||||||
|
err = {'success': False, 'error': str(e)}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
|
||||||
# Live progress for the PWA's operation modal (the request holds
|
# Live progress for the PWA's operation modal (the request holds
|
||||||
# _CARD_LOCK until the chain is done; /api/status carries this)
|
# _CARD_LOCK until the chain is done; /api/status carries this)
|
||||||
@@ -8226,10 +8340,18 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._send_json(err, 400)
|
self._send_json(err, 400)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
return
|
return
|
||||||
apdu = _cap_install_apdu(
|
try:
|
||||||
loadfile_aid, module_aid, instance_aid=instance_aid,
|
apdu = _cap_install_apdu(
|
||||||
privileges=privileges, install_params=install_params_hex,
|
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||||
stk_params=stk_params_hex, make_selectable=make_selectable)
|
privileges=privileges, install_params=install_params_hex,
|
||||||
|
stk_params=stk_params_hex, make_selectable=make_selectable,
|
||||||
|
nv_quota=body.get('nv_quota'),
|
||||||
|
volatile_quota=body.get('volatile_quota'))
|
||||||
|
except ValueError as e:
|
||||||
|
err = {'success': False, 'error': str(e)}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
step_name = 'INSTALL [for install]'
|
step_name = 'INSTALL [for install]'
|
||||||
# SCP80 params: the SPI1 comes from the MSL of the packet's
|
# SCP80 params: the SPI1 comes from the MSL of the packet's
|
||||||
# TAR (the operation uses the preset's table); a caller-supplied
|
# TAR (the operation uses the preset's table); a caller-supplied
|
||||||
|
|||||||
@@ -715,6 +715,31 @@ class PresetStoreHttpTests(unittest.TestCase):
|
|||||||
self.assertEqual(resp['final_cntr'], '0000000002')
|
self.assertEqual(resp['final_cntr'], '0000000002')
|
||||||
self.assertEqual(self.store.get(pid)['keysets'][0]['cntr'], '0000000002')
|
self.assertEqual(self.store.get(pid)['keysets'][0]['cntr'], '0000000002')
|
||||||
|
|
||||||
|
def test_ram_install_app_rejects_the_two_ef_combination(self):
|
||||||
|
"""v3.20.0: an EF-form STK part next to a caller-composed EF is
|
||||||
|
refused before anything is built (TS 102 226 8.2.1.3.2.1)."""
|
||||||
|
status, resp = self._post('/api/presets', self._preset(iccid='8970119000004600098'))
|
||||||
|
self.assertEqual(status, 200, resp)
|
||||||
|
pid = resp['preset']['id']
|
||||||
|
self.server.app = object()
|
||||||
|
status, resp = self._post('/api/ram-install-app', {
|
||||||
|
'mode': 'install',
|
||||||
|
'preset_id': pid, 'kic': '15', 'kid': '15',
|
||||||
|
'kicKey': 'AA', 'kidKey': 'BB',
|
||||||
|
'loadfile_aid': 'F0414C46416101', 'module_aid': 'F0414C4641610101',
|
||||||
|
'install_params': 'C900EF04C7020064', 'stk_params': 'EF04CA0201F0'})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('second System Specific Parameters', resp['error'])
|
||||||
|
# quotas next to a caller-composed install_params are refused too
|
||||||
|
status, resp = self._post('/api/ram-install-app', {
|
||||||
|
'mode': 'install',
|
||||||
|
'preset_id': pid, 'kic': '15', 'kid': '15',
|
||||||
|
'kicKey': 'AA', 'kidKey': 'BB',
|
||||||
|
'loadfile_aid': 'F0414C46416101', 'module_aid': 'F0414C4641610101',
|
||||||
|
'install_params': 'C900EF04C7020064', 'volatile_quota': 100})
|
||||||
|
self.assertEqual(status, 400, resp)
|
||||||
|
self.assertIn('composed only when install_params is empty', resp['error'])
|
||||||
|
|
||||||
def test_create_update_delete_round_trip(self):
|
def test_create_update_delete_round_trip(self):
|
||||||
status, resp = self._post('/api/presets', self._preset(iccid='8970119000004600098'))
|
status, resp = self._post('/api/presets', self._preset(iccid='8970119000004600098'))
|
||||||
self.assertEqual(status, 200, resp)
|
self.assertEqual(status, 200, resp)
|
||||||
|
|||||||
@@ -1514,6 +1514,63 @@ class CapApduSequenceTest(unittest.TestCase):
|
|||||||
'F0414C46416101', 'F0414C4641610101',
|
'F0414C46416101', 'F0414C4641610101',
|
||||||
make_selectable=False).startswith('80E60400'))
|
make_selectable=False).startswith('80E60400'))
|
||||||
|
|
||||||
|
def test_install_params_composition_enforces_one_ef(self):
|
||||||
|
# TS 102 226 8.2.1.3.2.1 / GPD_SPE_013 v1.1 Table 6-5: the memory
|
||||||
|
# quotas (C7/C8) and an EF-form STK part (CA) share ONE System
|
||||||
|
# Specific Parameters EF. Two EFs made the card read the quotas and
|
||||||
|
# ignore the CA (live 2026-10-05: por_ok, no toolkit registration).
|
||||||
|
from pysim_simple_server.server import _compose_install_params
|
||||||
|
# primitives: quotas and the CA are placed in one EF
|
||||||
|
self.assertEqual(_compose_install_params('', '', nv_quota=200,
|
||||||
|
volatile_quota=100),
|
||||||
|
'C900EF08C7020064C80200C8')
|
||||||
|
self.assertEqual(_compose_install_params('', 'EF04CA0201F0'),
|
||||||
|
'C900EF04CA0201F0')
|
||||||
|
self.assertEqual(_compose_install_params('', 'EF04CA0201F0', nv_quota=200,
|
||||||
|
volatile_quota=100),
|
||||||
|
'C900EF0CC7020064C80200C8CA0201F0')
|
||||||
|
# a 4-byte quota above 32767 (GP Card Spec 9.7)
|
||||||
|
self.assertEqual(_compose_install_params('', '', volatile_quota=32768),
|
||||||
|
'C900EF06C70400008000')
|
||||||
|
# an EA-form STK part is a sibling of EF (the reference TCA form)
|
||||||
|
self.assertEqual(_compose_install_params('', 'EA0480000000',
|
||||||
|
volatile_quota=100),
|
||||||
|
'C900EF04C7020064EA0480000000')
|
||||||
|
# a caller-composed EF plus an EF-form STK part is refused
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_compose_install_params('C900EF04C7020064', 'EF04CA0201F0')
|
||||||
|
# a bare CA is refused: it belongs inside the EF
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_compose_install_params('', 'CA0401F00000')
|
||||||
|
# a malformed EF is refused
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_compose_install_params('', 'EF05CA0100')
|
||||||
|
# quotas next to raw install parameters are refused
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_compose_install_params('C900', '', nv_quota=100)
|
||||||
|
# a bad quota value is refused
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_compose_install_params('', '', volatile_quota='-1')
|
||||||
|
# a caller-composed EF with an EA part stays as it was
|
||||||
|
self.assertEqual(_compose_install_params('C900EF04C7020064', 'EA0480000000'),
|
||||||
|
'C900EF04C7020064EA0480000000')
|
||||||
|
|
||||||
|
def test_install_apdu_carries_the_composed_parameters(self):
|
||||||
|
from pysim_simple_server.server import _cap_install_apdu
|
||||||
|
# CA-form STK + quotas: `C9 00 EF{C7,C8,CA}` - one EF (the form the
|
||||||
|
# PWA composes; an external caller gets the same via the primitives).
|
||||||
|
apdu = _cap_install_apdu('F0414C46416101', 'F0414C4641610101',
|
||||||
|
stk_params='EF0ACA080000000000000000',
|
||||||
|
volatile_quota=100)
|
||||||
|
self.assertEqual(
|
||||||
|
apdu,
|
||||||
|
'80E60C0030' + '07F0414C46416101' + '08F0414C4641610101'
|
||||||
|
+ '08F0414C4641610101' + '0100'
|
||||||
|
+ '12C900EF0EC7020064CA080000000000000000' + '00')
|
||||||
|
# case 3: the length is header + Lc data, no trailing Le
|
||||||
|
lc = int(apdu[8:10], 16)
|
||||||
|
self.assertEqual(len(apdu), 10 + 2 * lc)
|
||||||
|
|
||||||
def test_make_selectable_apdu(self):
|
def test_make_selectable_apdu(self):
|
||||||
# GP Card Spec 11.5.2.3.3, Table 11-44: '00' '00' lv(AID)
|
# GP Card Spec 11.5.2.3.3, Table 11-44: '00' '00' lv(AID)
|
||||||
# lv(privileges) lv(params) lv(token); case 3.
|
# lv(privileges) lv(params) lv(token); case 3.
|
||||||
|
|||||||
Reference in New Issue
Block a user