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
+55 -12
View File
@@ -23,8 +23,9 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['berLenStr', 'stkParamsBuild', 'buildRcToolkitParams',
'updateStkParamsHex', 'adfAidFromFci', 'ramParamsPrefix', 'ramQuotaValue',
'ramInstallParamsPrefix']) code += extractFunc(html, fn) + '\n';
'updateStkParamsHex', 'updateInstallParamsHex', 'adfAidFromFci', 'ramQuotaValue', 'ramQuotaTlvs',
'composeRamInstallParams', 'ramInstallParamsCompose',
'parseTlvList', 'parseBerLen']) code += extractFunc(html, fn) + '\n';
eval(code);
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'), '');
});
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):
// 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 -
// without it the card rejects the data field with 6A80 (live 2026-09-28).
assert.strictEqual(ramParamsPrefix('', '', ''), 'C900');
assert.strictEqual(ramParamsPrefix('082905112000012066', '', ''),
assert.strictEqual(composeRamInstallParams('', '', '', '', '').install_params, 'C900');
assert.strictEqual(
composeRamInstallParams('082905112000012066', '', '', '', '').install_params,
'C909082905112000012066');
assert.strictEqual(ramParamsPrefix('', '0', '0'), 'C900EF08C7020000C8020000');
assert.strictEqual(ramParamsPrefix('', '32768', ''), 'C900EF06C70400008000');
assert.strictEqual(ramParamsPrefix('AA', '', '1234'), 'C901AAEF04C80204D2');
assert.strictEqual(composeRamInstallParams('', '0', '0', '', '').install_params,
'C900EF08C7020000C8020000');
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' });
assert.strictEqual(ramInstallParamsPrefix(), 'C901AAEF04C7020000');
assert.deepStrictEqual(ramInstallParamsCompose(),
{ install_params: 'C901AAEF04C7020000', stk_params: '', quota_in_stk: false });
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', () => {
@@ -166,7 +204,12 @@ test('updateStkParamsHex names the rejection reason', () => {
test('the install body composes the C9/quota prefix', () => {
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', () => {