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
+25
View File
@@ -715,6 +715,31 @@ class PresetStoreHttpTests(unittest.TestCase):
self.assertEqual(resp['final_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):
status, resp = self._post('/api/presets', self._preset(iccid='8970119000004600098'))
self.assertEqual(status, 200, resp)
+57
View File
@@ -1514,6 +1514,63 @@ class CapApduSequenceTest(unittest.TestCase):
'F0414C46416101', 'F0414C4641610101',
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):
# GP Card Spec 11.5.2.3.3, Table 11-44: '00' '00' lv(AID)
# lv(privileges) lv(params) lv(token); case 3.