feat: single-APDU INSTALL [for install] / [for make selectable] ops (v3.6.18)
Iterating the final install step no longer needs a delete + re-load of the .cap: - `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`, P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable] (`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already loaded package. The APDU builders are shared with `_cap_apdu_sequence` (`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift) and the per-step send/PoR/counter logic is the extracted shared `_ram_send_gp_apdu` (used by /api/ram-install too). - PWA: the RAM op selector gains "INSTALL [for install] (loaded package)" and "INSTALL [for make selectable]" with Load File / Module / Instance AID inputs (instance defaults to the module AID); the result renders through the shared `ramShowInstallResult` and `final_cntr` keeps the preset in sync. - Explore: an **Install** button on a package's module AIDs that have no instance yet, and a **Make selectable** button on INSTALLED (0x03) instances - both switch the op and pre-fill the AIDs. - tests: the install APDU pinned to the live no-STK packet and to the sequence tail, the make-selectable APDU, ramHasInstance, the Explore buttons and the op wiring; docs/api.md updated. 650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
This commit is contained in:
+36
@@ -38,6 +38,7 @@ a 3.x PWA).
|
|||||||
| `/api/help` | POST | pySim help for a given command |
|
| `/api/help` | POST | pySim help for a given command |
|
||||||
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
| `/api/send-ota` | POST | SCP80 OTA secured packet delivery |
|
||||||
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
| `/api/ram-install` | POST | Install a Java Card `.cap` file via SCP80 (INSTALL[for load] → LOAD ×N → INSTALL[for install]) |
|
||||||
|
| `/api/ram-install-app` | POST | Single INSTALL [for install] / [for make selectable] for an already loaded package (no re-load) |
|
||||||
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
|
| `/api/cap-info` | POST | Validate a `.cap` archive and estimate its code/NVRAM/RAM requirements (read-only) |
|
||||||
| `/api/test/run` | POST | Start a test script (actions + proactive expectations) |
|
| `/api/test/run` | POST | Start a test script (actions + proactive expectations) |
|
||||||
| `/api/test/status` | GET | Test script run state and per-step results |
|
| `/api/test/status` | GET | Test script run state and per-step results |
|
||||||
@@ -561,6 +562,41 @@ sets `success: false`, `failed_step` and a detailed `error` such as
|
|||||||
|
|
||||||
The `steps` array contains one entry per GP command. `final_cntr` is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.
|
The `steps` array contains one entry per GP command. `final_cntr` is the counter value after all successful steps (use it to update the card preset). The response is not streamed — all steps run server-side before the JSON is returned.
|
||||||
|
|
||||||
|
### `POST /api/ram-install-app`
|
||||||
|
|
||||||
|
Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**) APDU for an *already loaded* package — one APDU, so the install parameters can be iterated without deleting and re-loading the `.cap`. The APDU builders are shared with `/api/ram-install` (`_cap_install_apdu` / `_cap_make_selectable_apdu`; both case 3 — no trailing `Le`, which the card's SCP80 layer counts as a phantom command) and the step verdict/counter logic is the same (`final_cntr` is returned on success **and** on failure).
|
||||||
|
|
||||||
|
**Request body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "install",
|
||||||
|
"loadfile_aid": "F0414C46416101",
|
||||||
|
"module_aid": "F0414C4641610101",
|
||||||
|
"instance_aid": "",
|
||||||
|
"privileges": "00",
|
||||||
|
"install_params": "C900",
|
||||||
|
"stk_params": "",
|
||||||
|
"make_selectable": true,
|
||||||
|
"spi1": "0E", "spi2": "01",
|
||||||
|
"kic": "15", "kid": "15",
|
||||||
|
"tar": "000000",
|
||||||
|
"cntr": "0000000001",
|
||||||
|
"kicKey": "D6FCC023...",
|
||||||
|
"kidKey": "1B07E7E0..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Req | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `mode` | no | `install` (default) or `make_selectable` (GP Table 11-44; needs `instance_aid`) |
|
||||||
|
| `loadfile_aid` | cond | Package 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 |
|
||||||
|
| `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) |
|
||||||
|
|
||||||
|
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
|
||||||
|
|
||||||
### `POST /api/sp-verify`
|
### `POST /api/sp-verify`
|
||||||
|
|
||||||
Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd`
|
Cross-check a secured packet against pySim's `OtaDialectSms.encode_cmd`
|
||||||
|
|||||||
+131
-17
@@ -696,12 +696,15 @@
|
|||||||
<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">
|
<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">
|
||||||
<option value="explore" data-l10n="Explore Card (all GP data)">Explore Card (all GP data)</option>
|
<option value="explore" data-l10n="Explore Card (all GP data)">Explore Card (all GP data)</option>
|
||||||
<option value="install-cap" data-l10n="Install Package (.cap file)">Install Package (.cap file)</option>
|
<option value="install-cap" data-l10n="Install Package (.cap file)">Install Package (.cap file)</option>
|
||||||
|
<option value="install-app" data-l10n="INSTALL [for install] (loaded package)">INSTALL [for install] (loaded package)</option>
|
||||||
|
<option value="make-selectable" data-l10n="INSTALL [for make selectable]">INSTALL [for make selectable]</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button data-needs="card" data-cap-gate="ram" onclick="ramExecute()" 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>
|
<button data-needs="card" data-cap-gate="ram" onclick="ramExecute()" 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="ram-install-params" class="hidden mb-3">
|
<div id="ram-install-params" class="hidden mb-3">
|
||||||
|
<div id="ram-cap-rows">
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label>
|
||||||
<input type="file" id="ram-cap-file" accept=".cap,.zip" onchange="ramCapFileChanged()" class="w-full text-sm text-gray-600 dark:text-slate-300">
|
<input type="file" id="ram-cap-file" accept=".cap,.zip" onchange="ramCapFileChanged()" class="w-full text-sm text-gray-600 dark:text-slate-300">
|
||||||
<fieldset id="ram-cap-info" class="hidden mt-2 border border-gray-200 dark:border-slate-700 rounded p-3 text-xs text-gray-500 dark:text-slate-400">
|
<fieldset id="ram-cap-info" class="hidden mt-2 border border-gray-200 dark:border-slate-700 rounded p-3 text-xs text-gray-500 dark:text-slate-400">
|
||||||
@@ -710,6 +713,15 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300 mt-2" data-l10n="SD AID (empty = ISD)">SD AID (empty = ISD)</label>
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300 mt-2" data-l10n="SD AID (empty = ISD)">SD AID (empty = ISD)</label>
|
||||||
<input id="ram-sd-aid" 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" placeholder="A000000003000000" maxlength="32">
|
<input id="ram-sd-aid" 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" placeholder="A000000003000000" maxlength="32">
|
||||||
|
</div>
|
||||||
|
<div id="ram-app-rows" class="hidden">
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Load File AID / Package AID (hex)">Load File AID / Package AID (hex)</label>
|
||||||
|
<input id="ram-app-loadfile" 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" placeholder="F0414C46416101" maxlength="32">
|
||||||
|
<label class="block mb-1 mt-2 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Executable Module AID / Applet Class AID (hex)">Executable Module AID / Applet Class AID (hex)</label>
|
||||||
|
<input id="ram-app-module" 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" placeholder="F0414C4641610101" maxlength="32">
|
||||||
|
<label class="block mb-1 mt-2 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Instance AID (hex, empty = module AID)">Instance AID (hex, empty = module AID)</label>
|
||||||
|
<input id="ram-app-instance" 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" placeholder="(module AID)" maxlength="32">
|
||||||
|
</div>
|
||||||
<div id="rc-priv-row" class="mb-3 mt-2">
|
<div id="rc-priv-row" class="mb-3 mt-2">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<label class="text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Privileges">Privileges</label>
|
<label class="text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Privileges">Privileges</label>
|
||||||
@@ -1699,7 +1711,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.6.17';
|
const SIMPLE_VERSION = '3.6.18';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -8178,7 +8190,17 @@ function ramOpChanged() {
|
|||||||
const op = document.getElementById('ram-op').value;
|
const op = document.getElementById('ram-op').value;
|
||||||
if (_ramOpLast !== null && op !== _ramOpLast) ramClearResults();
|
if (_ramOpLast !== null && op !== _ramOpLast) ramClearResults();
|
||||||
_ramOpLast = op;
|
_ramOpLast = op;
|
||||||
document.getElementById('ram-install-params').classList.toggle('hidden', op !== 'install-cap');
|
const isCap = op === 'install-cap';
|
||||||
|
const isApp = op === 'install-app' || op === 'make-selectable';
|
||||||
|
const setHidden = (id, hidden) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.classList.toggle('hidden', hidden);
|
||||||
|
};
|
||||||
|
setHidden('ram-install-params', !(isCap || isApp));
|
||||||
|
setHidden('ram-cap-rows', !isCap);
|
||||||
|
setHidden('ram-app-rows', !isApp);
|
||||||
|
// The Execute button's .cap gate only applies to the install-cap op.
|
||||||
|
if (typeof pysimApplyAvailability === 'function') pysimApplyAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ramShowProgress(text) {
|
function ramShowProgress(text) {
|
||||||
@@ -8843,6 +8865,9 @@ function ramRenderExploreHtml(mem, isd, apps, elfs) {
|
|||||||
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
|
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
|
||||||
if (o.aid) {
|
if (o.aid) {
|
||||||
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
|
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
|
||||||
|
if ((o.lifecycle || '').toUpperCase() === '03') {
|
||||||
|
html += ' <button onclick="ramMakeSelectableFromExplorer(\'' + o.aid + '\')" data-needs="card" class="ml-1 px-2 py-0.5 text-xs bg-emerald-100 text-emerald-700 rounded hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Make selectable')) + '</button>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
|
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
|
||||||
@@ -8869,7 +8894,15 @@ function ramRenderExploreHtml(mem, isd, apps, elfs) {
|
|||||||
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + o.version + '</div>';
|
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + o.version + '</div>';
|
||||||
if (o.moduleAids && o.moduleAids.length) {
|
if (o.moduleAids && o.moduleAids.length) {
|
||||||
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
|
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
|
||||||
o.moduleAids.forEach(m => { html += '<div class="pl-4">- ' + jcAidHtml(m) + '</div>'; });
|
o.moduleAids.forEach(m => {
|
||||||
|
html += '<div class="pl-4">- ' + jcAidHtml(m);
|
||||||
|
// A loaded applet class with no instance yet: one click to
|
||||||
|
// the single-APDU INSTALL [for install].
|
||||||
|
if (!ramHasInstance(apps, m)) {
|
||||||
|
html += ' <button onclick="ramInstallFromExplorer(\'' + o.aid + '\',\'' + m + '\')" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Install')) + '</button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -9023,6 +9056,31 @@ function ramDeleteApdu(aid, withCascade) {
|
|||||||
return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data;
|
return '80E400' + (withCascade ? '80' : '00') + berLenStr(data.length / 2) + data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A loaded applet class with no instance yet (or an INSTALLED, non-selectable
|
||||||
|
// instance): jump to the matching single-APDU operation with the AIDs filled.
|
||||||
|
function ramHasInstance(apps, aid) {
|
||||||
|
const a = (aid || '').toUpperCase();
|
||||||
|
return (apps || []).some(x => (x.aid || '').toUpperCase().startsWith(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ramInstallFromExplorer(loadfileAid, moduleAid) {
|
||||||
|
document.getElementById('ram-op').value = 'install-app';
|
||||||
|
ramOpChanged();
|
||||||
|
document.getElementById('ram-app-loadfile').value = loadfileAid || '';
|
||||||
|
document.getElementById('ram-app-module').value = moduleAid || '';
|
||||||
|
document.getElementById('ram-app-instance').value = '';
|
||||||
|
const el = document.getElementById('ram-op');
|
||||||
|
if (el.scrollIntoView) el.scrollIntoView({block: 'center'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ramMakeSelectableFromExplorer(aid) {
|
||||||
|
document.getElementById('ram-op').value = 'make-selectable';
|
||||||
|
ramOpChanged();
|
||||||
|
document.getElementById('ram-app-instance').value = aid || '';
|
||||||
|
const el = document.getElementById('ram-op');
|
||||||
|
if (el.scrollIntoView) el.scrollIntoView({block: 'center'});
|
||||||
|
}
|
||||||
|
|
||||||
// A successful delete drops the object from the Explore result locally (the
|
// A successful delete drops the object from the Explore result locally (the
|
||||||
// re-explore is not re-run). The registry has no package-to-applet link, so
|
// re-explore is not re-run). The registry has no package-to-applet link, so
|
||||||
// a cascade package delete also drops the applets whose AID starts with the
|
// a cascade package delete also drops the applets whose AID starts with the
|
||||||
@@ -9165,25 +9223,71 @@ async function ramInstallCap(sp) {
|
|||||||
const data = await pysimFetch('/api/ram-install', body);
|
const data = await pysimFetch('/api/ram-install', body);
|
||||||
|
|
||||||
ramHideProgress();
|
ramHideProgress();
|
||||||
const resultEl = document.getElementById('ram-result');
|
|
||||||
const stepsEl = document.getElementById('ram-steps');
|
|
||||||
stepsEl.classList.remove('hidden');
|
|
||||||
|
|
||||||
let txt = '';
|
|
||||||
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx) + '\n'; });
|
|
||||||
stepsEl.textContent = txt;
|
|
||||||
|
|
||||||
// Persist the counter the card actually consumed (accepted packets only):
|
|
||||||
// even a failed install leaves the accepted steps behind, and replaying
|
|
||||||
// their counter would make the card reject the next attempt.
|
|
||||||
if (data.final_cntr) ramSaveCntr(data.final_cntr);
|
|
||||||
if (data.success) {
|
|
||||||
let sizeInfo = '';
|
let sizeInfo = '';
|
||||||
if (data.load_block_size) {
|
if (data.load_block_size) {
|
||||||
sizeInfo = ' — ' + t('LOAD blocks') + ': ' + data.load_block_size + ' B';
|
sizeInfo = ' — ' + t('LOAD blocks') + ': ' + data.load_block_size + ' B';
|
||||||
if (data.load_block_size_auto) sizeInfo += ' (' + t('default') + ')';
|
if (data.load_block_size_auto) sizeInfo += ' (' + t('default') + ')';
|
||||||
}
|
}
|
||||||
resultEl.textContent = t('Install OK') + ' — load_file_aid=' + data.load_file_aid + ' module_aid=' + data.module_aid + sizeInfo;
|
ramShowInstallResult(data, t('Install OK') + ' — load_file_aid=' + data.load_file_aid
|
||||||
|
+ ' module_aid=' + data.module_aid + sizeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTALL [for install] / [for make selectable] for an already loaded
|
||||||
|
// package: one APDU, so the parameters can be iterated without deleting and
|
||||||
|
// re-loading the .cap every time.
|
||||||
|
async function ramInstallApp(sp, op) {
|
||||||
|
const clean = id => (document.getElementById(id).value || '').replace(/[^0-9a-fA-F]/g, '').toUpperCase();
|
||||||
|
const mode = op === 'make-selectable' ? 'make_selectable' : 'install';
|
||||||
|
const loadfile = clean('ram-app-loadfile');
|
||||||
|
const module = clean('ram-app-module');
|
||||||
|
const instance = clean('ram-app-instance');
|
||||||
|
if (mode === 'make_selectable') {
|
||||||
|
if (!instance) { alert(t('Instance AID required')); return; }
|
||||||
|
} else if (!loadfile || !module) {
|
||||||
|
alert(t('Load File AID and Module AID required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The privileges and the STK hex are derived from the form, never sent
|
||||||
|
// stale (see ramInstallCap).
|
||||||
|
updateRcPriv();
|
||||||
|
const stkEl = document.getElementById('ram-stk-params');
|
||||||
|
if (document.getElementById('rc-toolkit-enable').checked && !(stkEl.dataset && stkEl.dataset.manual)) {
|
||||||
|
updateStkParamsHex();
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
mode: mode,
|
||||||
|
loadfile_aid: loadfile,
|
||||||
|
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, ''),
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
ramShowProgress(t('Sending to card...'));
|
||||||
|
const data = await pysimFetch('/api/ram-install-app', body);
|
||||||
|
ramHideProgress();
|
||||||
|
ramShowInstallResult(data, t('OK'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared result rendering for /api/ram-install and /api/ram-install-app: the
|
||||||
|
// per-step lines, the counter the card consumed, and the verdict.
|
||||||
|
function ramShowInstallResult(data, okText) {
|
||||||
|
const resultEl = document.getElementById('ram-result');
|
||||||
|
const stepsEl = document.getElementById('ram-steps');
|
||||||
|
stepsEl.classList.remove('hidden');
|
||||||
|
let txt = '';
|
||||||
|
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx) + '\n'; });
|
||||||
|
stepsEl.textContent = txt;
|
||||||
|
// Persist the counter the card actually consumed (accepted packets only):
|
||||||
|
// even a failed install leaves the accepted steps behind, and replaying
|
||||||
|
// their counter would make the card reject the next attempt.
|
||||||
|
if (data.final_cntr) ramSaveCntr(data.final_cntr);
|
||||||
|
if (data.success) {
|
||||||
|
resultEl.textContent = okText || t('OK');
|
||||||
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 {
|
} else {
|
||||||
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 + ')' : '');
|
||||||
@@ -9211,6 +9315,7 @@ async function ramExecute() {
|
|||||||
try {
|
try {
|
||||||
if (op === 'explore') await ramExplore(sp);
|
if (op === 'explore') await ramExplore(sp);
|
||||||
else if (op === 'install-cap') await ramInstallCap(sp);
|
else if (op === 'install-cap') await ramInstallCap(sp);
|
||||||
|
else if (op === 'install-app' || op === 'make-selectable') await ramInstallApp(sp, op);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const resultEl = document.getElementById('ram-result');
|
const resultEl = document.getElementById('ram-result');
|
||||||
resultEl.textContent = t('Error') + ': ' + e.message;
|
resultEl.textContent = t('Error') + ': ' + e.message;
|
||||||
@@ -17352,6 +17457,15 @@ const LANG_RU = {
|
|||||||
'From card': 'С карты',
|
'From card': 'С карты',
|
||||||
'Priority (0 = RFU)': 'Приоритет (0 = RFU)',
|
'Priority (0 = RFU)': 'Приоритет (0 = RFU)',
|
||||||
'Applet-specific parameters (C9, hex, optional)': 'Параметры апплета (C9, hex, опционально)',
|
'Applet-specific parameters (C9, hex, optional)': 'Параметры апплета (C9, hex, опционально)',
|
||||||
|
'INSTALL [for install] (loaded package)': 'INSTALL [for install] (загруженный пакет)',
|
||||||
|
'INSTALL [for make selectable]': 'INSTALL [for make selectable]',
|
||||||
|
'Load File AID / Package AID (hex)': 'Load File AID / AID пакета (hex)',
|
||||||
|
'Executable Module AID / Applet Class AID (hex)': 'AID модуля / класса апплета (hex)',
|
||||||
|
'Instance AID (hex, empty = module AID)': 'AID экземпляра (hex, пусто = AID модуля)',
|
||||||
|
'Install': 'Установить',
|
||||||
|
'Instance AID required': 'Требуется AID экземпляра',
|
||||||
|
'Load File AID and Module AID required': 'Требуются Load File AID и Module AID',
|
||||||
|
'Sending to card...': 'Отправка на карту...',
|
||||||
'C7 / C8 memory quotas (bytes, optional)': 'Квоты памяти C7 / C8 (байты, опционально)',
|
'C7 / C8 memory quotas (bytes, optional)': 'Квоты памяти C7 / C8 (байты, опционально)',
|
||||||
'STK parameters not generated: check the TAR (multiple of 3 bytes), menu IDs (<= 7F) and the ADF AID (5-16 bytes)': 'STK-параметры не сформированы: проверьте TAR (кратность 3 байтам), ID меню (<= 7F) и ADF AID (5-16 байт)',
|
'STK parameters not generated: check the TAR (multiple of 3 bytes), menu IDs (<= 7F) and the ADF AID (5-16 bytes)': 'STK-параметры не сформированы: проверьте TAR (кратность 3 байтам), ID меню (<= 7F) и ADF AID (5-16 байт)',
|
||||||
'SCP80 (GSM 03.48, ETSI TS 102 225)': 'SCP80 (GSM 03.48, ETSI TS 102 225)',
|
'SCP80 (GSM 03.48, ETSI TS 102 225)': 'SCP80 (GSM 03.48, ETSI TS 102 225)',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v290';
|
const CACHE = 'simple-v291';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function extractFunc(src, name) {
|
|||||||
|
|
||||||
// Extract chain builder functions and dependencies
|
// Extract chain builder functions and dependencies
|
||||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
||||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer',
|
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance',
|
||||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
||||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute',
|
||||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||||
@@ -441,6 +441,37 @@ test('ramRemoteSwOk mirrors the server success set', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ramHasInstance matches instances by AID prefix', () => {
|
||||||
|
const apps = [{ aid: 'F0414C4641610101' },
|
||||||
|
{ aid: 'A1130001180001FFFFFFFF89A1003908' }];
|
||||||
|
assert.strictEqual(ramHasInstance(apps, 'f0414c46416101'), true);
|
||||||
|
assert.strictEqual(ramHasInstance(apps, 'F0414C4641610101'), true);
|
||||||
|
assert.strictEqual(ramHasInstance(apps, 'F0414C46416001'), false);
|
||||||
|
assert.strictEqual(ramHasInstance([], 'F0414C46416101'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the Explore result offers Install / Make selectable actions', () => {
|
||||||
|
global.t = s => s;
|
||||||
|
const out = ramRenderExploreHtml(
|
||||||
|
null,
|
||||||
|
[],
|
||||||
|
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
|
||||||
|
[{ aid: 'F0414C46416101', lifecycle: '01', version: '0.0',
|
||||||
|
moduleAids: ['F0414C4641610101'] }]
|
||||||
|
);
|
||||||
|
// the module has no instance yet -> Install; the INSTALLED instance -> Make selectable
|
||||||
|
assert.ok(out.includes("ramInstallFromExplorer('F0414C46416101','F0414C4641610101')"), out);
|
||||||
|
assert.ok(out.includes("ramMakeSelectableFromExplorer('A000000151000000')"), out);
|
||||||
|
delete global.t;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the single-APDU ops are wired', () => {
|
||||||
|
assert.ok(/value="install-app"/.test(html), 'the INSTALL [for install] op is missing');
|
||||||
|
assert.ok(/value="make-selectable"/.test(html), 'the make selectable op is missing');
|
||||||
|
assert.ok(/async function ramInstallApp\(sp, op\)/.test(html), 'ramInstallApp is missing');
|
||||||
|
assert.ok(/pysimFetch\('\/api\/ram-install-app'/.test(html), 'the endpoint call is missing');
|
||||||
|
});
|
||||||
|
|
||||||
test('ramRemoveFromExplorer drops the object locally (cascade drops its applets)', () => {
|
test('ramRemoveFromExplorer drops the object locally (cascade drops its applets)', () => {
|
||||||
let renders = 0;
|
let renders = 0;
|
||||||
globalThis.ramRenderExplorer = () => { renders++; };
|
globalThis.ramRenderExplorer = () => { renders++; };
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.17"
|
version = "3.6.18"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
+176
-87
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.6.17'
|
VERSION = '3.6.18'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -721,6 +721,30 @@ def _encode_scts(dt=None):
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_install_apdu(loadfile_aid, module_aid, instance_aid='', privileges='00',
|
||||||
|
install_params='', stk_params='', make_selectable=True):
|
||||||
|
"""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.
|
||||||
|
Case 3: no trailing Le (a trailing byte becomes a phantom command on the
|
||||||
|
card's SCP80 layer)."""
|
||||||
|
instance = instance_aid or module_aid
|
||||||
|
params = install_params if install_params else 'C900'
|
||||||
|
if stk_params:
|
||||||
|
params += stk_params
|
||||||
|
p1 = 0x0C if make_selectable else 0x04
|
||||||
|
data = (_lv(loadfile_aid) + _lv(module_aid) + _lv(instance) +
|
||||||
|
_lv(privileges or '00') + _lv(params) + '00')
|
||||||
|
return '80E6%02X00%02X%s' % (p1, len(data) // 2, data)
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_make_selectable_apdu(instance_aid, privileges='00'):
|
||||||
|
"""INSTALL [for make selectable] APDU (GP Card Spec 11.5.2.3.3, Table
|
||||||
|
11-44): '00' '00' lv(AID) lv(privileges) lv(params) lv(token); the
|
||||||
|
parameters and token are empty here. Case 3 (no Le)."""
|
||||||
|
data = '00' + '00' + _lv(instance_aid) + _lv(privileges or '00') + '00' + '00'
|
||||||
|
return '80E60800%02X%s' % (len(data) // 2, data)
|
||||||
|
|
||||||
|
|
||||||
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):
|
||||||
@@ -746,14 +770,10 @@ def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
|||||||
for i, block in enumerate(blocks):
|
for i, block in enumerate(blocks):
|
||||||
p1 = 0x80 if i == len(blocks) - 1 else 0x00
|
p1 = 0x80 if i == len(blocks) - 1 else 0x00
|
||||||
apdus.append('80E8%02X%02X%02X%s' % (p1, i % 256, len(block) // 2, block))
|
apdus.append('80E8%02X%02X%02X%s' % (p1, i % 256, len(block) // 2, block))
|
||||||
instance = instance_aid or module_aid
|
apdus.append(_cap_install_apdu(
|
||||||
params = install_params if install_params else 'C900'
|
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||||
if stk_params:
|
privileges=privileges, install_params=install_params,
|
||||||
params += stk_params
|
stk_params=stk_params, make_selectable=make_selectable))
|
||||||
p1_install = 0x0C if make_selectable else 0x04
|
|
||||||
ifi_data = (_lv(loadfile_aid) + _lv(module_aid) + _lv(instance) +
|
|
||||||
_lv(privileges or '00') + _lv(params) + '00')
|
|
||||||
apdus.append('80E6%02X00%02X%s' % (p1_install, len(ifi_data) // 2, ifi_data))
|
|
||||||
return apdus
|
return apdus
|
||||||
|
|
||||||
|
|
||||||
@@ -1176,6 +1196,77 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
|||||||
return step, error
|
return step, error
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
||||||
|
"""Send one GP APDU as an SCP80 secured packet and append its step result
|
||||||
|
to `state['steps']`. `sp` carries the SCP80 parameters (spi1, spi2, kic,
|
||||||
|
kid, tar, kic_key, kid_key, include_cpi); `state` carries the mutable run
|
||||||
|
state ({'steps', 'encode_error', 'failure', 'cntr'}) and the counter is
|
||||||
|
advanced only for a packet the card accepted. Returns True on success
|
||||||
|
(the step record is the last element of state['steps'])."""
|
||||||
|
spi1, spi2 = sp['spi1'], sp['spi2']
|
||||||
|
try:
|
||||||
|
sp_hex, _ = _build_secured_packet(spi1, spi2, sp['kic'], sp['kid'], sp['tar'],
|
||||||
|
state['cntr'], apdu_hex,
|
||||||
|
sp['kic_key'], sp['kid_key'])
|
||||||
|
except ValueError as e:
|
||||||
|
state['encode_error'] = str(e)
|
||||||
|
state['steps'].append({'name': step_name, 'por_status': 'encode_error',
|
||||||
|
'sw': str(e)})
|
||||||
|
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
||||||
|
return False
|
||||||
|
submit_handler = None
|
||||||
|
old_proactive = None
|
||||||
|
if bool(int(spi2, 16) & 0x20) and hasattr(scc, '_tp'):
|
||||||
|
submit_handler = PoRSubmitHandler()
|
||||||
|
old_proactive = scc._tp.proactive_handler
|
||||||
|
scc._tp.proactive_handler = submit_handler
|
||||||
|
try:
|
||||||
|
result = _send_secured_packet(
|
||||||
|
scc, sp_hex, oa_number=server.sms_oa, sm_sc=server.sms_sc,
|
||||||
|
include_cpi=sp['include_cpi'], submit_handler=submit_handler)
|
||||||
|
if not result['success']:
|
||||||
|
state['steps'].append({'name': step_name, 'por_status': 'envelope_error',
|
||||||
|
'sw': result.get('sw') or result.get('error'),
|
||||||
|
'bytes': result.get('bytes'),
|
||||||
|
'segments': result.get('segments')})
|
||||||
|
sys.stderr.write('RAM-INSTALL: %s send failed: %s\n' % (
|
||||||
|
step_name, result.get('error')))
|
||||||
|
return False
|
||||||
|
por_hex = result['response_data']
|
||||||
|
last_sw = result['sw']
|
||||||
|
por_src = 'envelope'
|
||||||
|
submit_hex = _sms_submit_por(submit_handler)
|
||||||
|
if submit_hex:
|
||||||
|
por_hex = submit_hex
|
||||||
|
por_src = 'sms-submit'
|
||||||
|
elif submit_handler and submit_handler.submit_tpdu_hex:
|
||||||
|
# no assembled UD: fall back to a raw RPI packet
|
||||||
|
tpdu_b = bytes.fromhex(submit_handler.submit_tpdu_hex)
|
||||||
|
idx = tpdu_b.find(b'\x02\x71\x00')
|
||||||
|
if idx >= 0:
|
||||||
|
por_hex = tpdu_b[idx:].hex()
|
||||||
|
por_src = 'sms-submit'
|
||||||
|
por = _decode_por(spi1, spi2, sp['kic'], sp['kid'], state['cntr'],
|
||||||
|
sp['kic_key'], sp['kid_key'], por_hex)
|
||||||
|
step, step_error = _ram_step_result(step_name, last_sw, por, por_hex,
|
||||||
|
result['bytes'], result['segments'])
|
||||||
|
state['steps'].append(step)
|
||||||
|
sys.stderr.write('RAM-INSTALL: %s PoR[%s] status=%s remote_sw=%s%s (%d B, %d SM)\n' % (
|
||||||
|
step_name, por_src, step.get('por_status', '?'), step.get('por_sw', '-'),
|
||||||
|
(' error=%s' % step_error) if step_error else '',
|
||||||
|
result['bytes'], result['segments']))
|
||||||
|
# Advance the counter only for an accepted packet
|
||||||
|
state['cntr'] = _ram_next_cntr(
|
||||||
|
state['cntr'], step.get('por_status') in ('por_ok', 'no_por'))
|
||||||
|
if step_error:
|
||||||
|
state['failure']['error'] = '%s: %s' % (step_name, step_error)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
if submit_handler and hasattr(scc, '_tp'):
|
||||||
|
scc._tp.proactive_handler = old_proactive
|
||||||
|
|
||||||
|
|
||||||
def _parse_response_scripting(data):
|
def _parse_response_scripting(data):
|
||||||
"""Parse a Response Scripting template (TS 102 226 5.2.2, tables
|
"""Parse a Response Scripting template (TS 102 226 5.2.2, tables
|
||||||
5.10/5.10a): `AB <len>` (definite) or `AF 80 ... 00 00` (indefinite),
|
5.10/5.10a): `AB <len>` (definite) or `AF 80 ... 00 00` (indefinite),
|
||||||
@@ -4371,7 +4462,7 @@ _TEST_KIND_LABELS = {
|
|||||||
_TEST_BLOCKED_PATHS = frozenset([
|
_TEST_BLOCKED_PATHS = frozenset([
|
||||||
'/api/command', '/api/cardinfo', '/api/tree', '/api/select', '/api/read',
|
'/api/command', '/api/cardinfo', '/api/tree', '/api/select', '/api/read',
|
||||||
'/api/write', '/api/apdu', '/api/verify-adm', '/api/send-ota',
|
'/api/write', '/api/apdu', '/api/verify-adm', '/api/send-ota',
|
||||||
'/api/ram-install', '/api/sp-verify', '/api/menu-select',
|
'/api/ram-install', '/api/ram-install-app', '/api/sp-verify', '/api/menu-select',
|
||||||
'/api/menu-respond', '/api/event-send', '/api/net-sim',
|
'/api/menu-respond', '/api/event-send', '/api/net-sim',
|
||||||
'/api/net-state-refresh', '/api/status-poll', '/api/rescue',
|
'/api/net-state-refresh', '/api/status-poll', '/api/rescue',
|
||||||
'/api/terminal-profile', '/api/poll-toggle', '/api/esim/chip',
|
'/api/terminal-profile', '/api/poll-toggle', '/api/esim/chip',
|
||||||
@@ -6125,78 +6216,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % block_size)
|
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % block_size)
|
||||||
|
|
||||||
steps = []
|
steps = []
|
||||||
encode_error = None
|
state = {'steps': steps, 'encode_error': None, 'failure': {},
|
||||||
include_cpi = body.get('includeCpi', True)
|
'cntr': cntr}
|
||||||
spi2_val = int(spi2, 16)
|
sp_state = {'spi1': spi1, 'spi2': spi2, 'kic': kic, 'kid': kid,
|
||||||
por_in_submit = bool(spi2_val & 0x20)
|
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||||
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
failure = {}
|
|
||||||
|
|
||||||
def _send_gp_apdu(apdu_hex, step_name):
|
|
||||||
nonlocal cntr, encode_error
|
|
||||||
try:
|
|
||||||
sp_hex, _ = _build_secured_packet(spi1, spi2, kic, kid, tar, cntr, apdu_hex, kic_key, kid_key)
|
|
||||||
except ValueError as e:
|
|
||||||
encode_error = str(e)
|
|
||||||
steps.append({'name': step_name, 'por_status': 'encode_error',
|
|
||||||
'sw': encode_error})
|
|
||||||
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
|
||||||
return False
|
|
||||||
submit_handler = None
|
|
||||||
old_proactive = None
|
|
||||||
if por_in_submit and hasattr(scc, '_tp'):
|
|
||||||
submit_handler = PoRSubmitHandler()
|
|
||||||
old_proactive = scc._tp.proactive_handler
|
|
||||||
scc._tp.proactive_handler = submit_handler
|
|
||||||
try:
|
|
||||||
result = _send_secured_packet(
|
|
||||||
scc, sp_hex, oa_number=self.server.sms_oa,
|
|
||||||
sm_sc=self.server.sms_sc, include_cpi=include_cpi,
|
|
||||||
submit_handler=submit_handler)
|
|
||||||
if not result['success']:
|
|
||||||
steps.append({'name': step_name, 'por_status': 'envelope_error',
|
|
||||||
'sw': result.get('sw') or result.get('error'),
|
|
||||||
'bytes': result.get('bytes'),
|
|
||||||
'segments': result.get('segments')})
|
|
||||||
sys.stderr.write('RAM-INSTALL: %s send failed: %s\n' % (
|
|
||||||
step_name, result.get('error')))
|
|
||||||
return False
|
|
||||||
last_data = result['response_data']
|
|
||||||
last_sw = result['sw']
|
|
||||||
# Decode PoR
|
|
||||||
por_src = 'envelope'
|
|
||||||
por_hex = last_data
|
|
||||||
submit_hex = _sms_submit_por(submit_handler)
|
|
||||||
if submit_hex:
|
|
||||||
por_hex = submit_hex
|
|
||||||
por_src = 'sms-submit'
|
|
||||||
elif submit_handler and submit_handler.submit_tpdu_hex:
|
|
||||||
# no assembled UD: fall back to a raw RPI packet
|
|
||||||
tpdu_b = bytes.fromhex(submit_handler.submit_tpdu_hex)
|
|
||||||
idx = tpdu_b.find(b'\x02\x71\x00')
|
|
||||||
if idx >= 0:
|
|
||||||
por_hex = tpdu_b[idx:].hex()
|
|
||||||
por_src = 'sms-submit'
|
|
||||||
por = _decode_por(spi1, spi2, kic, kid, cntr, kic_key, kid_key, por_hex)
|
|
||||||
step, step_error = _ram_step_result(
|
|
||||||
step_name, last_sw, por, por_hex,
|
|
||||||
result['bytes'], result['segments'])
|
|
||||||
steps.append(step)
|
|
||||||
sys.stderr.write('RAM-INSTALL: %s PoR[%s] status=%s remote_sw=%s%s (%d B, %d SM)\n' % (
|
|
||||||
step_name, por_src, step.get('por_status', '?'),
|
|
||||||
step.get('por_sw', '-'),
|
|
||||||
(' error=%s' % step_error) if step_error else '',
|
|
||||||
result['bytes'], result['segments']))
|
|
||||||
# Advance the counter only for an accepted packet
|
|
||||||
cntr = _ram_next_cntr(
|
|
||||||
cntr, step.get('por_status') in ('por_ok', 'no_por'))
|
|
||||||
if step_error:
|
|
||||||
failure['error'] = '%s: %s' % (step_name, step_error)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
finally:
|
|
||||||
if submit_handler and hasattr(scc, '_tp'):
|
|
||||||
scc._tp.proactive_handler = old_proactive
|
|
||||||
|
|
||||||
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
||||||
seq = _cap_apdu_sequence(
|
seq = _cap_apdu_sequence(
|
||||||
@@ -6213,11 +6237,12 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
step_name = 'INSTALL [for install]'
|
step_name = 'INSTALL [for install]'
|
||||||
else:
|
else:
|
||||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||||
if not _send_gp_apdu(gp_apdu, step_name):
|
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
||||||
|
step_name, gp_apdu):
|
||||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||||
'error': (encode_error or failure.get('error')
|
'error': (state['encode_error'] or state['failure'].get('error')
|
||||||
or ('%s failed' % step_name)),
|
or ('%s failed' % step_name)),
|
||||||
'final_cntr': cntr,
|
'final_cntr': state['cntr'],
|
||||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||||
'load_block_size': block_size,
|
'load_block_size': block_size,
|
||||||
'load_block_size_requested': block_size_req,
|
'load_block_size_requested': block_size_req,
|
||||||
@@ -6227,12 +6252,12 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
||||||
'module_aid': module_aid, 'final_cntr': cntr,
|
'module_aid': module_aid, 'final_cntr': state['cntr'],
|
||||||
'load_block_size': block_size,
|
'load_block_size': block_size,
|
||||||
'load_block_size_requested': block_size_req,
|
'load_block_size_requested': block_size_req,
|
||||||
'load_block_size_auto': not block_size_req}
|
'load_block_size_auto': not block_size_req}
|
||||||
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
||||||
loadfile_aid, module_aid, cntr))
|
loadfile_aid, module_aid, state['cntr']))
|
||||||
self._send_json(resp)
|
self._send_json(resp)
|
||||||
self._log_resp(resp)
|
self._log_resp(resp)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -6240,6 +6265,70 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
err = {'success': False, 'error': str(e)}
|
err = {'success': False, 'error': str(e)}
|
||||||
self._send_json(err, 500)
|
self._send_json(err, 500)
|
||||||
self._log_resp(err)
|
self._log_resp(err)
|
||||||
|
elif self.path == '/api/ram-install-app':
|
||||||
|
# INSTALL [for install] / [for make selectable] for an already
|
||||||
|
# loaded package (iterate the final step without re-loading).
|
||||||
|
body = self._read_body()
|
||||||
|
self._log_req(body)
|
||||||
|
scc = self.server.scc
|
||||||
|
if not scc:
|
||||||
|
self._send_json({'error': _err('reader_not_init', lang)}, 503)
|
||||||
|
self._log_resp({'error': _err('reader_not_init', lang)})
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
mode = (body.get('mode') or 'install').strip()
|
||||||
|
loadfile_aid = (body.get('loadfile_aid') or '').replace(' ', '').upper()
|
||||||
|
module_aid = (body.get('module_aid') or '').replace(' ', '').upper()
|
||||||
|
instance_aid = ((body.get('instance_aid') or '').replace(' ', '').upper()
|
||||||
|
or module_aid)
|
||||||
|
privileges = (body.get('privileges') or '').replace(' ', '') or '00'
|
||||||
|
install_params_hex = (body.get('install_params') or '').replace(' ', '')
|
||||||
|
stk_params_hex = (body.get('stk_params') or '').replace(' ', '')
|
||||||
|
make_selectable = bool(body.get('make_selectable', True))
|
||||||
|
if mode == 'make_selectable':
|
||||||
|
if not instance_aid:
|
||||||
|
err = {'success': False, 'error': 'instance_aid required'}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
apdu = _cap_make_selectable_apdu(instance_aid, privileges)
|
||||||
|
step_name = 'INSTALL [for make selectable]'
|
||||||
|
else:
|
||||||
|
if not loadfile_aid or not module_aid:
|
||||||
|
err = {'success': False,
|
||||||
|
'error': 'loadfile_aid and module_aid required'}
|
||||||
|
self._send_json(err, 400)
|
||||||
|
self._log_resp(err)
|
||||||
|
return
|
||||||
|
apdu = _cap_install_apdu(
|
||||||
|
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||||
|
privileges=privileges, install_params=install_params_hex,
|
||||||
|
stk_params=stk_params_hex, make_selectable=make_selectable)
|
||||||
|
step_name = 'INSTALL [for install]'
|
||||||
|
sp_state = {
|
||||||
|
'spi1': body.get('spi1', '16'), 'spi2': body.get('spi2', '01'),
|
||||||
|
'kic': body.get('kic', '25'), 'kid': body.get('kid', '25'),
|
||||||
|
'tar': body.get('tar', '000000'), 'cntr': body.get('cntr', '00000000'),
|
||||||
|
'kic_key': body.get('kicKey', ''), 'kid_key': body.get('kidKey', ''),
|
||||||
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
|
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
|
'cntr': sp_state['cntr']}
|
||||||
|
sys.stderr.write('RAM-INSTALL-APP: %s (%s) cntr=%s\n' % (
|
||||||
|
apdu, step_name, sp_state['cntr']))
|
||||||
|
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||||
|
resp = {'success': bool(ok), 'steps': state['steps'],
|
||||||
|
'final_cntr': state['cntr']}
|
||||||
|
if not ok:
|
||||||
|
resp['error'] = (state['encode_error'] or state['failure'].get('error')
|
||||||
|
or ('%s failed' % step_name))
|
||||||
|
resp['failed_step'] = len(state['steps'])
|
||||||
|
self._send_json(resp)
|
||||||
|
self._log_resp(resp)
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write('RAM-INSTALL-APP error: %s\n' % e)
|
||||||
|
err = {'success': False, 'error': str(e)}
|
||||||
|
self._send_json(err, 500)
|
||||||
|
self._log_resp(err)
|
||||||
elif self.path == '/api/scp81/bip':
|
elif self.path == '/api/scp81/bip':
|
||||||
body = self._read_body()
|
body = self._read_body()
|
||||||
# Never log the pre-shared keys.
|
# Never log the pre-shared keys.
|
||||||
|
|||||||
@@ -1207,6 +1207,41 @@ class CapApduSequenceTest(unittest.TestCase):
|
|||||||
self.assertEqual(seq[0][:10], '80E6020012')
|
self.assertEqual(seq[0][:10], '80E6020012')
|
||||||
self.assertIn('06A00000010001' + '07A0000000040000' + '000000', seq[0])
|
self.assertIn('06A00000010001' + '07A0000000040000' + '000000', seq[0])
|
||||||
|
|
||||||
|
def test_install_apdu_matches_the_live_apdu_and_the_sequence_tail(self):
|
||||||
|
# /api/ram-install-app reuses the exact final APDU of the full
|
||||||
|
# sequence (one builder, no drift). Pinned to the live no-STK
|
||||||
|
# INSTALL [for install] (decrypted 2026-09-28).
|
||||||
|
from pysim_simple_server.server import _cap_apdu_sequence, _cap_install_apdu
|
||||||
|
self.assertEqual(
|
||||||
|
_cap_install_apdu('F0414C46416101', 'F0414C4641610101'),
|
||||||
|
'80E60C0020' + '07F0414C46416101' + '08F0414C4641610101'
|
||||||
|
+ '08F0414C4641610101' + '0100' + '02C900' + '00')
|
||||||
|
stk = 'EA0F800D000000000102011203AF4D0100'
|
||||||
|
seq = _cap_apdu_sequence('F0414C46416101', 'F0414C4641610101', 'AABBCCDD',
|
||||||
|
stk_params=stk)
|
||||||
|
self.assertEqual(seq[-1], _cap_install_apdu(
|
||||||
|
'F0414C46416101', 'F0414C4641610101', stk_params=stk))
|
||||||
|
# case 3: the length is header + Lc data, no trailing Le
|
||||||
|
lc = int(seq[-1][8:10], 16)
|
||||||
|
self.assertEqual(len(seq[-1]), 10 + 2 * lc)
|
||||||
|
# make selectable switches P1 to 0x0C
|
||||||
|
self.assertTrue(_cap_install_apdu(
|
||||||
|
'F0414C46416101', 'F0414C4641610101').startswith('80E60C00'))
|
||||||
|
self.assertTrue(_cap_install_apdu(
|
||||||
|
'F0414C46416101', 'F0414C4641610101',
|
||||||
|
make_selectable=False).startswith('80E60400'))
|
||||||
|
|
||||||
|
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.
|
||||||
|
from pysim_simple_server.server import _cap_make_selectable_apdu
|
||||||
|
self.assertEqual(
|
||||||
|
_cap_make_selectable_apdu('F0414C4641610101'),
|
||||||
|
'80E608000F' + '0000' + '08F0414C4641610101' + '0100' + '0000')
|
||||||
|
self.assertEqual(
|
||||||
|
_cap_make_selectable_apdu('F0414C4641610101', '04'),
|
||||||
|
'80E608000F' + '0000' + '08F0414C4641610101' + '0104' + '0000')
|
||||||
|
|
||||||
def test_gen_install_returns_the_apdu_list(self):
|
def test_gen_install_returns_the_apdu_list(self):
|
||||||
# /api/scp81/gen-install: build the INSTALL/LOAD/INSTALL list for a
|
# /api/scp81/gen-install: build the INSTALL/LOAD/INSTALL list for a
|
||||||
# .cap without touching any listener or script state.
|
# .cap without touching any listener or script state.
|
||||||
|
|||||||
Reference in New Issue
Block a user