feat: RAM-install import probe for LOAD rejections (v3.6.42)

Cards that cannot satisfy the CAP's import list reject the LOAD as soon
as the Import component is complete (live 2026-09-29: 6438/6985 in the
block containing its last byte, on several cards incl. an eUICC profile
whose ISD is not the default AID and which does not answer GET DATA 0066 -
no direct platform-version readout exists).

_probe_import_versions (pure) rewrites the Import component's two version
bytes per entry in the assembled load file: same length, component sizes,
Directory and offsets untouched; unknown AIDs and bad versions are
rejected.  /api/ram-install takes probe_imports ({'all': '0.0'} or
per-AID), echoes the applied changes in the response and logs them.  The
PWA's RAM install form gains a collapsed Import probe fieldset (checkbox,
AID=version lines prefilled from the CAP analysis, 'Set all 0.0') and the
result line reports the applied changes.  Diagnostic only - never used for
a real install.

Also verified _cap_parse's component order against the reference TCA load
trace (Header, Directory, Import, Applet, Class, Method, StaticField,
ConstantPool, RefLocation, Descriptor - Import before Applet, not
ascending tag order), so the order is correct and the import list remains
the prime suspect.

sw cache simple-v315; tests: 530 Python / 676 Node.
This commit is contained in:
2026-09-29 03:41:07 +03:00
parent 56dbc3f915
commit 755470e4ba
9 changed files with 284 additions and 15 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -320,7 +320,7 @@
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Operation</th><th class="text-left py-1 px-2">Description</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Explore Card (all GP data)</td><td class="py-1 px-2">Queries GET STATUS for ISD, Applications, ELFs, and ELF Modules, plus GET DATA FF21 (Extended Card Resources: the number of installed applications and the free non-volatile/volatile memory). If a card rejects the compact listing (<code class="font-mono text-sm">P2.b2=0</code>, deprecated by GP) the queries are retried in the expanded <code class="font-mono text-sm">E3</code> form; the <strong>Query expanded registry details (E3)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields (privileges, ELF/SD associations, module lists). Results appear in an explorer view with per-item <strong>Delete</strong> buttons and the detected RAM command format.</td></tr>
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) &mdash; grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>) &mdash; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 &mdash; card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data).</td></tr>
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1&ndash;240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) &mdash; grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>) &mdash; a smaller LOAD block only helps when the block exceeds the card's buffer. The <strong>Import probe</strong> fieldset (diagnostic) rewrites the CAP's requested import versions before the load &mdash; all entries to 0.0, or one at a time: since the card checks that list while it verifies the load file, this shows whether it is the rejection cause and, by bisection, which package and version the card lacks. Only the two version bytes per entry change (the load file length, the component sizes and every offset stay untouched), the probe is never used for a real install, and the applied changes are reported in the result line and the server log. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 &mdash; card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data).</td></tr>
</tbody>
</table>
+88 -4
View File
@@ -713,6 +713,20 @@
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="CAP details">CAP details</legend>
<div id="ram-cap-body"></div>
</fieldset>
<fieldset class="mt-2 border border-gray-200 dark:border-slate-700 rounded p-3">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Import probe (diagnostic)">Import probe (diagnostic)</legend>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-slate-300 cursor-pointer">
<input id="ram-probe-enable" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="ramProbeChanged()">
<span data-l10n="Rewrite the CAP's import versions before the load">Rewrite the CAP's import versions before the load</span>
</label>
<div id="ram-probe-body" class="hidden mt-2">
<textarea id="ram-probe-versions" rows="4" spellcheck="false" oninput="this.dataset.manual='1'" class="w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1 dark:bg-slate-800" placeholder="AID=version per line"></textarea>
<div class="flex items-center gap-2 mt-1">
<button type="button" onclick="ramProbeAllZero()" class="px-2 py-0.5 text-xs rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Set all 0.0">Set all 0.0</button>
<span class="text-xs text-gray-400 dark:text-slate-500" data-l10n="Diagnostic only: the card checks the import list while loading, so lowering the versions shows whether it is the rejection cause.">Diagnostic only: the card checks the import list while loading, so lowering the versions shows whether it is the rejection cause.</span>
</div>
</div>
</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>
<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>
@@ -1732,7 +1746,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.6.41';
const SIMPLE_VERSION = '3.6.42';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8602,7 +8616,7 @@ async function capAnalyzeFile(file, kind) {
if (resp && resp.ok) {
st.status = 'ok';
st.memory = resp.memory || {};
if (kind === 'ram') ramApplyCapToolkitMode();
if (kind === 'ram') { ramApplyCapToolkitMode(); ramProbeFill(st.memory); }
} else {
st.status = 'error';
st.error = (resp && resp.error) || t('Analysis failed');
@@ -9443,12 +9457,64 @@ function ramInstallFailHint(data, mem) {
return hint;
}
// ===== Import probe (diagnostic, v3.6.42) =====
// The card checks the CAP's import list while it verifies the load file, so
// rewriting the requested versions shows whether that list is what it refuses
// - and, by lowering one entry at a time, which package and version it lacks.
// 'AID=version' per line (or 'all=x.y'); blank/comment lines are ignored.
// Pure: returns {map} or {error}.
function ramProbeParse(text) {
const out = {};
for (const rawLine of String(text || '').split('\n')) {
const line = rawLine.split('#')[0].trim();
if (!line) continue;
const m = /^([0-9a-fA-F]{2,32}|all)\s*=\s*(\d{1,3})\.(\d{1,3})$/.exec(line);
if (!m) return { error: t('Invalid line:') + ' ' + line };
const aid = m[1].toLowerCase() === 'all' ? 'all' : m[1].toUpperCase();
out[aid] = m[2] + '.' + m[3];
}
if (!Object.keys(out).length) return { error: t('No import versions given') };
return { map: out };
}
function ramProbeChanged() {
const on = document.getElementById('ram-probe-enable').checked;
document.getElementById('ram-probe-body').classList.toggle('hidden', !on);
}
// Prefill from the analysed CAP's import list unless the user edited it.
function ramProbeFill(mem) {
const el = document.getElementById('ram-probe-versions');
if (!el || (el.dataset && el.dataset.manual)) return;
const imports = (mem && mem.imports) || [];
el.value = imports.map(im => im.aid + '=' + (Number(im.major) || 0) + '.' + (Number(im.minor) || 0)).join('\n');
}
function ramProbeAllZero() {
const el = document.getElementById('ram-probe-versions');
if (!el) return;
el.value = String(el.value || '').split('\n').map(line => {
const m = /^([0-9a-fA-F]{2,32}|all)\s*=/.exec(line.trim());
return m ? m[1] + '=0.0' : line;
}).join('\n');
el.dataset.manual = '1';
}
async function ramInstallCap(sp) {
const fileInput = document.getElementById('ram-cap-file');
const file = fileInput.files[0];
if (!file) { alert(t('Select a .cap file')); return; }
if (file.size > 48 * 1024) { alert(t('CAP file exceeds 48 kB limit')); return; }
// Import probe (diagnostic): validated before the modal opens
let probe = null;
if (document.getElementById('ram-probe-enable').checked) {
const parsed = ramProbeParse(document.getElementById('ram-probe-versions').value);
if (parsed.error) { alert(parsed.error); return; }
probe = parsed.map;
}
const capPkg = (_capAnalysis.ram.memory && _capAnalysis.ram.memory.package_aid) || '';
ramOpModalBegin(t('Installing package') + (capPkg ? ' \u2014 ' + capPkg : ''));
ramOpModalStatus(t('Reading CAP file...'));
@@ -9484,6 +9550,7 @@ async function ramInstallCap(sp) {
if (!(n >= 1 && n <= 240)) { alert(t('LOAD block size must be 1..240')); return; }
body.load_block_size = n;
}
if (probe) body.probe_imports = probe;
const data = await pysimFetch('/api/ram-install', body);
@@ -9554,13 +9621,21 @@ function ramShowInstallResult(data, okText, capMem) {
// 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);
// The import probe's applied changes (diagnostic runs only)
let probeNote = '';
const pa = data.probe_imports;
if (pa && pa.length) {
const targets = Array.from(new Set(pa.map(p => p.to)));
probeNote = ' \u2014 ' + t('probe') + ': ' + pa.length + ' ' + t('imports') +
' \u2192 ' + (targets.length === 1 ? targets[0] : t('mixed'));
}
if (data.success) {
resultEl.textContent = okText || t('OK');
resultEl.textContent = (okText || t('OK')) + probeNote;
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
} else {
const hint = ramInstallFailHint(data, capMem || null);
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '') +
(hint ? ' \u2014 ' + t('Hint:') + ' ' + hint : '');
probeNote + (hint ? ' \u2014 ' + t('Hint:') + ' ' + hint : '');
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
}
ramOpModalFinish(resultEl.textContent, !!data.success, txt);
@@ -17367,6 +17442,15 @@ const LANG_RU = {
'Install OK': 'Установка OK',
'Install FAILED at step:': 'Установка не удалась на шаге:',
'Hint:': 'Подсказка:',
'Import probe (diagnostic)': 'Проба импортов (диагностика)',
"Rewrite the CAP's import versions before the load": 'Переписать версии импортов CAP перед загрузкой',
'Set all 0.0': 'Все 0.0',
'Diagnostic only: the card checks the import list while loading, so lowering the versions shows whether it is the rejection cause.': 'Только диагностика: карта проверяет список импортов при загрузке, поэтому понижение версий показывает, является ли он причиной отказа.',
'Invalid line:': 'Некорректная строка:',
'No import versions given': 'Версии импортов не заданы',
'probe': 'проба',
'imports': 'импортов',
'mixed': 'разные',
'completes': 'завершает',
'inside': 'внутри',
'the failed step completes': 'неудавшийся шаг завершает',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v314';
const CACHE = 'simple-v315';
const URLS = [
'index.html',
'help.html',
+4 -2
View File
@@ -42,9 +42,11 @@ eval(extractFunc(html, 'capMemBytes'));
eval(extractFunc(html, 'capQuotaText'));
eval(extractFunc(html, 'capMemHtml'));
eval(extractFunc(html, 'capAnalyzeFile', true));
// The RAM CAP analysis also applies the detected toolkit mode (DOM-bound; its
// own logic is covered in ram.test.js) - stub it for these state tests.
// The RAM CAP analysis also applies the detected toolkit mode and prefills
// the import-probe field (DOM-bound; their own logic is covered in
// ram.test.js) - stub them for these state tests.
globalThis.ramApplyCapToolkitMode = () => {};
globalThis.ramProbeFill = () => {};
globalThis.esc = s => s;
globalThis.t = s => s;
+17 -1
View File
@@ -22,7 +22,7 @@ function extractFunc(src, name) {
}
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
@@ -772,3 +772,19 @@ test('ramDeleteFromExplorer advances but does not re-explore on a failed remote
assert.strictEqual(calls.explored, null, 'a failed DELETE must not re-explore');
unstubDeleteEnv();
});
test('ramProbeParse reads AID=version lines', () => {
globalThis.t = s => s;
let r = ramProbeParse('A0000000620101=0.0\n\n# comment\n0102030405 = 1.2');
assert.deepStrictEqual(r.map, { 'A0000000620101': '0.0', '0102030405': '1.2' });
// 'all' and lower-case AIDs are accepted, normalized to upper case
r = ramProbeParse('all=0.0');
assert.deepStrictEqual(r.map, { all: '0.0' });
r = ramProbeParse('a0000000620101=1.3');
assert.deepStrictEqual(r.map, { 'A0000000620101': '1.3' });
// malformed lines and an empty field are errors
assert.ok(ramProbeParse('A0000000620101').error);
assert.ok(ramProbeParse('A0000000620101=x.y').error);
assert.ok(ramProbeParse('').error);
assert.ok(ramProbeParse(' \n# only comments\n').error);
});