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:
2026-10-05 23:01:42 +03:00
parent ff3feb6079
commit 82c5733906
7 changed files with 382 additions and 69 deletions
+75 -32
View File
@@ -2783,10 +2783,13 @@ function ramResetGrants() {
// Pure SIM/UICC toolkit install-parameter builder shared by the RAM install
// form (buildRcToolkitParams) and the RAM/GP chain rows (buildTkParams).
// `v` carries the field values; returns the CA/EA TLV hex, 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`
// `v` carries the field values; returns the STK part of the install
// parameters - the EF-wrapped CA (`EF { CA ... }`, TS 102 226 8.2.1.3.2.1;
// composeRamInstallParams() merges the memory quotas into that EF) or the
// 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
// them with an ADF entry (AID from `v.adfAid`, default ADF.USIM); the SIM path
// grants access via the CA Access Domain field instead.
@@ -2888,6 +2891,7 @@ function updateStkParamsHex() {
// field is edited again (this clears the flag).
if (el.dataset) delete el.dataset.manual;
// A rejected value must not vanish silently: say why the field is empty.
updateInstallParamsHex();
const hint = document.getElementById('ram-stk-hint');
if (!hint) return;
if (tk === null) {
@@ -2956,35 +2960,68 @@ function ramQuotaValue(n) {
: '04' + v.toString(16).padStart(8, '0').toUpperCase();
}
// The applet-specific prefix of the install parameters, in the vendor's
// order: `C9 <applet-specific parameters>` + `EF <C7/C8 memory quotas>`.
// The server appends the STK (`EA`/`EF+CA`) part after it. C9 is mandatory
// in the Install Parameters (GP Table 11-49), so an empty field is sent as
// `C9 00` - like the reference tools; a prefix without C9 makes the card
// reject the data field with 6A80 (live 2026-09-28).
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;
// The quota TLVs of the install EF: `C7 <len> <quota>` (volatile) and
// `C8 <len> <quota>` (non-volatile), in the vendor's order.
function ramQuotaTlvs(c7, c8) {
let out = '';
if (String(c7 ?? '') !== '' && !isNaN(parseInt(c7, 10))) out += 'C7' + ramQuotaValue(c7);
if (String(c8 ?? '') !== '' && !isNaN(parseInt(c8, 10))) out += 'C8' + ramQuotaValue(c8);
return out;
}
function ramInstallParamsPrefix() {
return ramParamsPrefix(
document.getElementById('rc-c9').value,
document.getElementById('rc-quota-c7').value,
document.getElementById('rc-quota-c8').value);
// Compose the INSTALL [for install] parameters. The memory quotas (C7/C8)
// 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 EA-form (or opaque) STK part is a sibling of
// 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() {
const prefix = ramInstallParamsPrefix();
const el = document.getElementById('ram-install-params-hex');
// The field carries extra raw TLVs; the generated prefix is shown in the
// placeholder so the composed parameters stay visible.
el.placeholder = prefix ? prefix + ' …' : 'extra TLVs (optional)';
if (!el) return;
const composed = ramInstallParamsCompose();
// 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 =====
@@ -11210,14 +11247,14 @@ async function ramInstallCap(sp) {
// never be sent silently.
updateRcPriv();
const composed = ramInstallParamsCompose();
const body = {
cap_hex: capHex,
preset_id: sp.preset_id,
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, ''),
install_params: (ramInstallParamsPrefix() +
(document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, '')).toUpperCase(),
stk_params: (document.getElementById('ram-stk-params').value || '').replace(/[^0-9a-fA-F]/g, ''),
install_params: composed.install_params,
stk_params: composed.stk_params,
make_selectable: document.getElementById('ram-make-sel').checked,
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
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)) {
updateStkParamsHex();
}
const composed = ramInstallParamsCompose();
const body = {
mode: mode,
preset_id: sp.preset_id,
@@ -11270,9 +11308,8 @@ async function ramInstallApp(sp, op) {
module_aid: module,
instance_aid: instance,
privileges: (document.getElementById('rc-priv').value || '00').replace(/[^0-9a-fA-F]/g, ''),
install_params: (ramInstallParamsPrefix() +
(document.getElementById('ram-install-params-hex').value || '').replace(/[^0-9a-fA-F]/g, '')).toUpperCase(),
stk_params: (document.getElementById('ram-stk-params').value || '').replace(/[^0-9a-fA-F]/g, ''),
install_params: composed.install_params,
stk_params: composed.stk_params,
make_selectable: document.getElementById('ram-make-sel').checked,
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
@@ -11719,6 +11756,10 @@ function ramShowInstallResult(data, okText, capMem) {
if (data.success) {
resultEl.textContent = (okText || t('OK')) + probeNote;
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 {
const hint = ramInstallFailHint(data, capMem || null);
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '') +
@@ -20824,6 +20865,8 @@ const LANG_RU = {
'Step': 'Шаг',
'Install OK': 'Установка OK',
'Install FAILED at step:': 'Установка не удалась на шаге:',
'Install FAILED:': 'Установка не удалась:',
'quotas in the STK EF': 'квоты в EF STK-параметров',
'Hint:': 'Подсказка:',
'Set all 0.0': 'Все 0.0',
'CAP compatibility test': 'Проверка совместимости CAP',