feat: CAP platform requirement + LOAD rejection hint (v3.6.36)

capmem derives the minimum platform from the CAP's Import component
(javacard.framework -> Java Card level via the SDK-kit corpus, int flag)
and /api/cap-info exposes it; the CAP details show it as a Platform row
('Java Card >= 2.2.2 - CAP format 2.1 - int support').

A failed RAM install at a LOAD / INSTALL [for load] step now appends a
hint naming the import requirement and the smaller-block fallback
(ramInstallFailHint, pure): JC VM spec 4.5.2 makes every import a
'>= version' demand on the card's own packages checked while the load
file is verified - live 2026-09-29, a CAP requiring Java Card 2.2.2 was
rejected exactly at the block containing the end of Import (6438 at
240 B blocks, 6985 at 64 B), not at a step count.

sw cache simple-v309; tests: 507 Python / 670 Node.
This commit is contained in:
2026-09-29 00:59:17 +03:00
parent bd5dc69c4e
commit f007d5c9be
10 changed files with 169 additions and 16 deletions
+1 -1
View File
@@ -321,7 +321,7 @@
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Операция</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Если карта отвергает компактный формат листинга (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>; флажок <strong>Запрашивать расширенные данные реестра (E3)</strong> дополнительно запрашивает их и объединяет расширенные поля (привилегии, связи ELF/SD, списки модулей). Результаты отображаются в обзоре с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM.</td></tr>
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Строка <strong>Platform</strong> показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта <code class="font-mono text-sm">javacard.framework</code>; флаг <code class="font-mono text-sm">int</code> в заголовке дополнительно требует поддержки int — Java Card 2.2+): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
</tbody>
</table>
+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. 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) &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>
</tbody>
</table>
+43 -8
View File
@@ -1732,7 +1732,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.35';
const SIMPLE_VERSION = '3.6.36';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -8472,11 +8472,21 @@ function capMemHtml(mem) {
return esc(a) + (n ? ' <span class="text-gray-400">(' + esc(n) + ')</span>' : '');
}).join('<br>'), 'pl-4 font-mono break-all'));
}
// ── minimum platform: the card must provide every import version (JC VM
// spec 4.5.2 - same major, minor >= the export version) and the JCRE
// checks that while the load file is verified, so the framework import
// names the minimum Java Card level; the header int flag needs int
// support (Java Card 2.2+). capmem computes the level
// (requires_java_card); the local table is the fallback for an older
// server response.
const fw = imports.find(im => (im.aid || '').toUpperCase() === 'A0000000620101');
const sdk = fw ? JC_FRAMEWORK_SDK[(Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0)] : '';
let against = sdk ? 'Java Card ' + sdk : '';
const sdk = mem.requires_java_card ||
(fw ? JC_FRAMEWORK_SDK[(Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0)] : '');
let against = sdk ? 'Java Card \u2265 ' + sdk
: (fw ? 'javacard.framework \u2265 ' + (Number(fw.major) || 0) + '.' + (Number(fw.minor) || 0) : '');
if (mem.cap_version) against += (against ? ' \u00b7 ' : '') + t('CAP format') + ' ' + mem.cap_version;
if (against) pkgRows += row(label(esc(t('Compiled'))) + cell(esc(against), 'pl-4'));
if (mem.needs_int) against += (against ? ' \u00b7 ' : '') + t('int support');
if (against) pkgRows += row(label(esc(t('Platform'))) + cell(esc(against), 'pl-4'));
if (pkgRows) html += section(t('Package')) + table(pkgRows);
// ── the libraries the CAP is linked against (JC VM spec 6.6), shown as
@@ -9375,6 +9385,25 @@ function ramStepLine(s, idx) {
return line;
}
// Hint after a failed RAM install (pure). A rejected LOAD / INSTALL [for
// load] step has two known causes: the card's platform is older than the
// CAP requires (the JCRE checks every import while it verifies the load file,
// JC VM spec 4.5.2, and refuses the step as soon as the Import component is
// complete - observed remote SW 6438/6985 exactly at that block boundary),
// or the LOAD block exceeds the card's buffer. `mem` is the CAP analysis
// (capmem memory_json) when the install came from a .cap.
function ramInstallFailHint(data, mem) {
if (!data || data.success) return '';
const steps = data.steps || [];
const step = steps[(Number(data.failed_step) || 1) - 1] || null;
const name = step ? String(step.name || '') : '';
if (!/^(LOAD |INSTALL \[for load\])/.test(name)) return '';
let hint = t('a LOAD rejected here means the card refused the CAP: every import version under Requires must be present on the card');
if (mem && mem.requires_java_card) hint += ' (Java Card \u2265 ' + mem.requires_java_card + ')';
hint += t('. If the CAP needs only versions this card has, retry with a smaller LOAD block (e.g. 64 B)');
return hint;
}
async function ramInstallCap(sp) {
const fileInput = document.getElementById('ram-cap-file');
const file = fileInput.files[0];
@@ -9426,7 +9455,7 @@ async function ramInstallCap(sp) {
if (data.load_block_size_auto) sizeInfo += ' (' + t('default') + ')';
}
ramShowInstallResult(data, t('Install OK') + ' — load_file_aid=' + data.load_file_aid
+ ' module_aid=' + data.module_aid + sizeInfo);
+ ' module_aid=' + data.module_aid + sizeInfo, _capAnalysis.ram.memory);
}
// INSTALL [for install] / [for make selectable] for an already loaded
@@ -9474,7 +9503,7 @@ async function ramInstallApp(sp, op) {
// 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) {
function ramShowInstallResult(data, okText, capMem) {
const resultEl = document.getElementById('ram-result');
const stepsEl = document.getElementById('ram-steps');
stepsEl.classList.remove('hidden');
@@ -9489,7 +9518,9 @@ function ramShowInstallResult(data, okText) {
resultEl.textContent = okText || t('OK');
resultEl.classList.remove('hidden', 'text-red-600'); resultEl.classList.add('text-green-600');
} else {
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '');
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 : '');
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
}
ramOpModalFinish(resultEl.textContent, !!data.success, txt);
@@ -17116,7 +17147,8 @@ const LANG_RU = {
'Version': 'Версия',
'Type': 'Тип',
'Applets': 'Апплеты',
'Compiled': 'Собрано',
'Platform': 'Платформа',
'int support': 'поддержка int',
'Requires': 'Требуются',
'Family': 'Семейство',
'AID': 'AID',
@@ -17143,6 +17175,9 @@ const LANG_RU = {
'Step': 'Шаг',
'Install OK': 'Установка OK',
'Install FAILED at step:': 'Установка не удалась на шаге:',
'Hint:': 'Подсказка:',
'a LOAD rejected here means the card refused the CAP: every import version under Requires must be present on the card': 'отклонение LOAD здесь означает, что карта отклонила CAP: все версии импортов из «Требуются» должны присутствовать на карте',
'. If the CAP needs only versions this card has, retry with a smaller LOAD block (e.g. 64 B)': '. Если CAP требует только доступные этой карте версии, повторите с меньшим размером блока LOAD (например, 64 Б)',
'Profile from snapshot': 'Профиль из снимка',
'Profile verification results for:': 'Результаты проверки профиля:',
'Snapshot comparison results:': 'Результаты сравнения снимков:',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v308';
const CACHE = 'simple-v309';
const URLS = [
'index.html',
'help.html',
+22 -2
View File
@@ -119,11 +119,12 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen
assert.ok(req.includes('>\u2265 1.0</td>'), req);
assert.ok(req.includes('>Oracle JavaCard API</td>'), req);
assert.ok(req.includes('>6</td>'), req);
// package identity + compiled-against hint derived from the framework version
// package identity + platform requirement derived from the framework version
assert.ok(out.includes('A0000000620101') && out.includes('>v1.0</td>'), out);
assert.ok(out.includes('applet package'), out);
assert.ok(out.includes('(javacard.framework)</span>'), out);
assert.ok(out.includes('Java Card 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out);
assert.ok(out.includes('>Platform</td>'), out);
assert.ok(out.includes('Java Card \u2265 2.1.1/2.1.2 \u00b7 CAP format 2.1'), out);
// components in load-file order with sizes, shares and the total row
assert.ok(out.includes('>Header</td>') && out.includes('>20 B</td>') && out.includes('>1%</td>'), out);
assert.ok(out.includes('>Method</td>') && out.includes('>2.7 kB</td>') && out.includes('>93%</td>'), out);
@@ -134,6 +135,25 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen
assert.strictEqual(capMemHtml(null), '');
});
test('capMemHtml shows the server platform requirement and int support', () => {
const mem = memFixture();
mem.requires_java_card = '2.2.2';
mem.requires_framework = '1.3';
mem.needs_int = true;
const out = capMemHtml(mem);
assert.ok(out.includes('Java Card \u2265 2.2.2 \u00b7 CAP format 2.1 \u00b7 int support'), out);
// the server value wins over the local fallback table (fixture import 1.0)
assert.ok(!out.includes('2.1.1/2.1.2'), out);
});
test('capMemHtml falls back to the raw framework version outside the corpus', () => {
const mem = memFixture();
mem.imports = [{ aid: 'A0000000620101', minor: 0, major: 7, refs: 6 }];
const out = capMemHtml(mem);
assert.ok(out.includes('javacard.framework \u2265 7.0 \u00b7 CAP format 2.1'), out);
assert.ok(!out.includes('Java Card'), out);
});
test('capMemHtml skips the import section on a response without imports', () => {
const mem = memFixture();
delete mem.imports;
+21 -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', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramInstallFailHint', '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',
@@ -390,6 +390,26 @@ test('ramStepLine shows the PoR verdict and the remote status word', () => {
assert.ok(noPor.startsWith('\u2705') && noPor.includes('no PoR'), noPor);
});
test('ramInstallFailHint names the CAP import requirement for a rejected LOAD', () => {
globalThis.t = s => s;
const failedLoad = {
success: false, failed_step: 3,
steps: [{ name: 'INSTALL [for load]' }, { name: 'LOAD (1/240)' }, { name: 'LOAD (2/240)', por_sw: '6985' }],
};
const hint = ramInstallFailHint(failedLoad, { requires_java_card: '2.2.2' });
assert.ok(hint.includes('every import version under Requires'), hint);
assert.ok(hint.includes('(Java Card \u2265 2.2.2)'), hint);
assert.ok(hint.includes('smaller LOAD block'), hint);
// without a CAP analysis (INSTALL [for install] ops) the level is omitted
const noMem = ramInstallFailHint(failedLoad, null);
assert.ok(noMem.includes('every import version under Requires'), noMem);
assert.ok(!noMem.includes('Java Card'), noMem);
// only a load-related step gets the hint
assert.strictEqual(ramInstallFailHint(
{ success: false, failed_step: 2, steps: [{ name: 'INSTALL [for load]' }, { name: 'INSTALL [for install]' }] }, null), '');
assert.strictEqual(ramInstallFailHint({ success: true, failed_step: 1, steps: [] }, null), '');
});
test('ramOpChanged clears the executed status only on a real op change', () => {
const els = fakeRamDocument(['ram-op', 'ram-install-params', 'ram-result', 'ram-explorer', 'ram-steps', 'ram-progress']);
_ramOpLast = null;
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.35"
version = "3.6.36"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+44
View File
@@ -1444,6 +1444,21 @@ def _external_ref_counts(cap):
return counts
# javacard.framework export version -> Java Card release family whose kit
# ships exactly that version. Derived from the local Oracle SDK kit corpus
# (jc211_kit .. jc305u4_kit exports, 2026-09-27; same table as the PWA's
# JC_FRAMEWORK_SDK). A card provides the framework export of its platform,
# and JC VM spec 4.5.2 makes the CAP's import version a ">=" demand on it
# (same major, minor >= the export version), checked by the JCRE while the
# load file is verified - so the framework import names the minimum Java Card
# level a CAP can load on.
JC_FRAMEWORK_AID = 'A0000000620101'
JC_FRAMEWORK_JDK = {
'1.0': '2.1.1/2.1.2', '1.2': '2.2.1', '1.3': '2.2.2',
'1.4': '3.0.3', '1.5': '3.0.4', '1.6': '3.0.5',
}
def analyze_bytes(cap_bytes, verbose=False):
"""Analyze a CAP archive (bytes) and return (report, memory)."""
cap = CAP(cap_bytes)
@@ -1472,6 +1487,21 @@ def analyze_bytes(cap_bytes, verbose=False):
h = cap.components['header']
report['flags'] = h.flags
report['package_name'] = h.package_name
# flags bit 0 ('int', set by the JC 2.1.2 converter's -i): the loader
# must support int (Java Card 2.2+)
report['needs_int'] = bool(h.flags & 0x01)
# Minimum platform (JC VM spec 4.5.2): the card must export every
# imported package at >= the version recorded in the CAP, and the JCRE
# performs that check while the load file is verified. The framework
# import names the minimum Java Card level (a card with an older platform
# rejects the LOAD as soon as the Import component is complete).
if cap.has('import'):
fw = next((p for p in cap.components['import'].packages
if p.aid_hex == JC_FRAMEWORK_AID), None)
if fw:
report['requires_framework'] = f'{fw.major}.{fw.minor}'
report['requires_java_card'] = JC_FRAMEWORK_JDK.get(report['requires_framework'])
# Component sizes (the load file order) for the breakdown display
report['components'] = [{'name': name, 'size': size} for name, size in cap.files]
@@ -1710,6 +1740,12 @@ def memory_json(report, memory, load_file_bytes=None):
'applet': bool(report.get('flags', 0) & 0x04),
},
'package_name': report.get('package_name'),
# minimum platform the card must provide for this load file (JC VM
# spec 4.5.2, derived from the imports): the Java Card level named by
# the framework import, and the header int flag's loader requirement
'requires_framework': report.get('requires_framework'),
'requires_java_card': report.get('requires_java_card'),
'needs_int': bool(report.get('needs_int')),
'components': report.get('components', []),
'class_count': report.get('class_count', 0),
'method_count': report.get('method_count', 0),
@@ -1756,6 +1792,14 @@ def format_report(report, memory):
if 'cap_version' in report:
lines.append(f'CAP format: {report["cap_version"]}')
if report.get('requires_java_card') or report.get('requires_framework'):
req = 'Requires Java Card >= %s' % (report.get('requires_java_card')
or report.get('requires_framework'))
if report.get('requires_java_card') and report.get('requires_framework'):
req += ' (javacard.framework %s)' % report['requires_framework']
if report.get('needs_int'):
req += ', int support'
lines.append(req)
if 'package_version' in report:
lines.append(f'Package version: {report["package_version"]}')
if 'package_aid' in report:
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.35'
VERSION = '3.6.36'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
+34
View File
@@ -1,6 +1,8 @@
"""Tests for the bundled CAP memory analyzer (capmem) and /api/cap-info helper."""
import io
import pathlib
import re
import struct
import unittest
import zipfile
@@ -228,6 +230,38 @@ class TestCapAnalyzer(unittest.TestCase):
report, _ = capmem.analyze_bytes(build_cap())
self.assertIsNone(report['package_name'])
def test_framework_sdk_table_matches_the_pwa(self):
# capmem.JC_FRAMEWORK_JDK mirrors the PWA's fallback JC_FRAMEWORK_SDK
# (drift guard: the PWA shows the server value when it is present)
html = pathlib.Path(__file__).resolve().parents[1].joinpath(
'frontend', 'index.html').read_text(encoding='utf-8')
m = re.search(r"const JC_FRAMEWORK_SDK = \{(.*?)\};", html, re.S)
self.assertIsNotNone(m, 'JC_FRAMEWORK_SDK not found in index.html')
pwa = dict(re.findall(r"'([\d.]+)':\s*'([^']+)'", m.group(1)))
self.assertEqual(pwa, capmem.JC_FRAMEWORK_JDK)
def test_platform_requirement_from_the_framework_import(self):
# java.lang only: no Java Card level named, but the int flag shows
report, _ = capmem.analyze_bytes(
build_cap(imports=[(0, 1, bytes.fromhex('A0000000620001'))], header_flags=0x01))
info = capmem.memory_json(report, capmem.compute_memory(report))
self.assertIsNone(info['requires_framework'])
self.assertIsNone(info['requires_java_card'])
self.assertTrue(info['needs_int'])
# javacard.framework 1.3 -> Java Card 2.2.2 (the kit corpus mapping)
report, _ = capmem.analyze_bytes(
build_cap(imports=[(3, 1, JAVACARD_FRAMEWORK)], header_flags=0x05))
info = capmem.memory_json(report, capmem.compute_memory(report))
self.assertEqual(info['requires_framework'], '1.3')
self.assertEqual(info['requires_java_card'], '2.2.2')
self.assertTrue(info['needs_int'])
# a version outside the corpus keeps the raw framework version
report, _ = capmem.analyze_bytes(
build_cap(imports=[(0, 2, JAVACARD_FRAMEWORK)]))
info = capmem.memory_json(report, capmem.compute_memory(report))
self.assertEqual(info['requires_framework'], '2.0')
self.assertIsNone(info['requires_java_card'])
def test_memory_json_nvram_requirement_uses_the_load_file(self):
report, memory = capmem.analyze_bytes(rich_cap())
info = capmem.memory_json(report, memory, load_file_bytes=1000)