feat: Explore exposes the rest of the registry data + install NV footprint (v3.6.51)

The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files).  The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).

New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
  applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
  'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
  concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
  queries add 84 to the tag list (with a tag list the card returns only the
  listed tags - the fallback path would otherwise lose the module lists and
  their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
  app, SD - detected via the Security Domain privilege - and ELF), with
  multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
  surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
  aligned with the same tables

/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.

sw cache simple-v324; tests: 548 Python / 698 Node.
This commit is contained in:
2026-09-30 09:00:33 +03:00
parent be411a7b4b
commit 599d84fd66
11 changed files with 637 additions and 76 deletions
+13 -2
View File
@@ -501,7 +501,7 @@ Clears a finished run report (409 while a run is active).
### `POST /api/ram-install`
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The **NV footprint** is measured around the chain: a best-effort `GET DATA FF21` (Extended Card Resources, TS 102 226 §8.2.1.7.2) read before the first step and again after the last one (also on failure) reports the free non-volatile memory and its delta — the read is silent (no step record, no failure) and a card without FF21 simply yields no fields. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
**Request body:**
```json
@@ -545,7 +545,10 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
"application_aid": "A000000003000000",
"load_block_size": 240,
"load_block_size_requested": null,
"load_block_size_auto": true}
"load_block_size_auto": true,
"nv_before": 50646,
"nv_after": 23400,
"nv_delta": 27246}
```
`load_block_size` is the effective size used for the LOAD blocks (240 by
@@ -558,6 +561,14 @@ word; the RAM results are in `por_status` (the PoR verdict: `por_ok`,
`por_sw` (the remote command's own status word from the compact/expanded
response, with `por_type`/`por_cntr`/`por_data`/`por_raw` for context).
`nv_before`/`nv_after` are the free non-volatile memory (bytes) read via
`GET DATA FF21` before and after the chain, `nv_delta` their difference (what
the operation consumed; negative = memory was released, e.g. a discarded
partial load). The three fields are omitted when the card has no FF21
readout; on a failure the values still show what the failed attempt consumed.
The two FF21 reads consume counters like any accepted packet, so
`final_cntr` already includes them.
A step fails when the PoR is not `por_ok`, when `por_sw` is outside the
success set (`9000`, `61xx` more data, `62xx`/`63xx` warnings, `CAFE` GP
"more data"), or when response data arrived but could not be decoded. The
+1 -1
View File
@@ -320,7 +320,7 @@
<table class="w-full text-sm mb-3 border-collapse">
<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>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля (AID load-файла, AID Security Domain, списки модулей) &mdash; он расходует дополнительные защищённые пакеты, и не все карты поддерживают расширенный запрос, поэтому по умолчанию выключен и запоминается в браузере. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
<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 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>, а флажок <strong>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он расходует дополнительные защищённые пакеты, и не все карты поддерживают расширенный запрос, поэтому по умолчанию выключен и запоминается в браузере. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</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+), и <strong>релиз ETSI</strong>, из которого пришли импорты UICC API (версия <code class="font-mono text-sm">uicc.toolkit</code> &rarr; REL-N по корпусу <code class="font-mono text-sm">sim-tools</code> в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты и компонент, который завершил блок, например <em>завершает Import</em>) — меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install].</td></tr>
</tbody>
+1 -1
View File
@@ -319,7 +319,7 @@
<table class="w-full text-sm mb-3 border-collapse">
<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>Fetch expanded registry details (load-file / SD associations)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields (Load File AID, Security Domain AID, module lists) &mdash; it costs extra secured packets and some cards reject the expanded query, so it is off by default and remembered per browser. Results appear in an explorer view (the success line stays minimal; the counts are in the section headings) with per-item <strong>Delete</strong> buttons and the detected RAM command format (hover for what compact vs expanded means).</td></tr>
<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). The compact listing carries the AID, life cycle and privileges (the legacy format has no version); if a card rejects it (<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, and the <strong>Fetch expanded registry details (load-file / SD associations)</strong> checkbox also requests it as an extra pass and merges the expanded-only fields: the Executable Load File version, Load File AID, Security Domain AID, module lists, the applet STK menu entries (SCP Registry Data, TS 102 226 8.2.1.6) and any other registry data objects the card returns (shown raw, never dropped) &mdash; it costs extra secured packets and some cards reject the expanded query, so it is off by default and remembered per browser. Results appear in an explorer view (the success line stays minimal; the counts are in the section headings) with per-item <strong>Delete</strong> buttons and the detected RAM command format (hover for what compact vs expanded means).</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+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version &rarr; REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): 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 class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component &mdash; the point where the JCRE verifies the import list &mdash; then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at &ge; that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 &mdash; if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install].</td></tr>
</tbody>
+199 -46
View File
@@ -703,7 +703,7 @@
<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>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA). The compact listing carries AID/lifecycle/privileges/version only; the expanded pass adds the Load File AID, Security Domain AID and module lists. It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<label class="flex items-center gap-1 mb-3 text-sm text-gray-700 dark:text-slate-300" data-l10n-title="Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing."><input type="checkbox" id="ram-expanded-details" class="rounded border-gray-300 dark:border-slate-600" onchange="ramExpandedDetailsChanged()"> <span data-l10n="Fetch expanded registry details (load-file / SD associations)">Fetch expanded registry details (load-file / SD associations)</span></label>
<div id="ram-cap-rows" class="hidden mb-3">
<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>
@@ -1749,7 +1749,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.50';
const SIMPLE_VERSION = '3.6.51';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -6480,16 +6480,16 @@ const SW_CMD_NAMES = {
};
const LIFECYCLE_MAP = {
'01': 'OP_READY (card) / LOADED (ELF)',
'03': 'INSTALLED',
'07': 'SELECTABLE',
'0F': 'SECURED (card)',
'1F': 'PERSONALIZED (SD)',
'01': 'OP_READY (card/ISD) / LOADED (ELF)',
'03': 'INSTALLED (app/SD)',
'07': 'SELECTABLE (app/SD) / INITIALIZED (card/ISD)',
'0F': 'SECURED (card/ISD) / PERSONALIZED (SD)',
'1F': 'App-specific (app)',
'7F': 'CARD_LOCKED',
'FF': 'TERMINATED',
'83': 'LOCKED (SD)',
'87': 'LOCKED (SD)',
'8F': 'LOCKED (SD)',
'83': 'LOCKED',
'87': 'LOCKED',
'8F': 'LOCKED',
};
function getSwCategory(cmd) {
@@ -8837,22 +8837,59 @@ function jcAidHtml(aid) {
// ===== TLV parsers for GET STATUS responses (P2=02 format) =====
// P2=02 wraps each entry in an 'E3' GP Registry Data template containing TLV tags.
// Fallback: if the card only supports P2=00 (raw format), raw parsers are used.
// Known tags per GP Card Spec v2.3.1 Tables 11-36/11-37 and ETSI TS 102 226
// 8.2.1.6 (EA SCP Registry Data -> 80 Menu Parameters); unknown registry data
// objects are preserved in rawTlvs instead of being dropped.
function _parseE3Entry(hex) {
const children = parseTLV(hex || '');
const r = {};
const raw = [];
for (const c of children) {
if (c.tag === '4F') r.aid = c.value.toUpperCase();
else if (c.tag === '9F70') r.lifecycle = c.value.toUpperCase();
else if (c.tag === 'C5') r.privileges = c.value.toUpperCase();
else if (c.tag === 'CE') r.version = c.value.toUpperCase();
else if (c.tag === 'CC') r.sdAid = c.value.toUpperCase();
else if (c.tag === 'C4') r.elfAid = c.value.toUpperCase();
else if (c.tag === 'CF') r.implicitSel = c.value.toUpperCase();
else if (c.tag === '84') r.moduleAids = ramParseModuleAids(c.value);
const v = c.value.toUpperCase();
if (c.tag === '4F') r.aid = v;
else if (c.tag === '9F70') r.lifecycle = v;
else if (c.tag === 'C5') r.privileges = v;
else if (c.tag === 'CE') r.version = v;
else if (c.tag === 'CC') r.sdAid = v;
else if (c.tag === 'C4') r.elfAid = v;
else if (c.tag === 'CF') r.implicitSel = v;
else if (c.tag === '84') {
// GP Table 11-37: one Executable Module AID per '84' TLV, repeated
// per module. Some cards concatenate 4F<len><aid> TLVs inside one
// '84' - accept both codings.
const aids = ramParseModuleAids(v);
const list = aids.length ? aids : (v.length >= 10 && v.length <= 32 ? [v] : []);
r.moduleAids = (r.moduleAids || []).concat(list);
}
else if (c.tag === 'EA') {
// SCP Registry Data (TS 102 226 8.2.1.6 Table 8.2): a Menu
// Parameters TLV ('80') with (position, identifier, state)
// triples (Table 8.3). An empty EA is valid (no menu entries).
const entries = _parseMenuEntries(v);
if (entries.length) r.menuEntries = entries;
else if (v) raw.push({ tag: 'EA', value: v });
}
else raw.push({ tag: c.tag, value: v });
}
if (raw.length) r.rawTlvs = raw;
return r;
}
// EA SCP Registry Data -> Menu Parameters ('80'): the value is a list of
// 3-byte menu entry triples (position, identifier, state; '00' disabled /
// '01' enabled, TS 102 226 Table 8.3).
function _parseMenuEntries(hex) {
const out = [];
for (const c of parseTLV(hex || '')) {
if (c.tag !== '80') continue;
const v = c.value.toUpperCase();
for (let i = 0; i + 6 <= v.length; i += 6) {
out.push({ pos: v.substr(i, 2), id: v.substr(i + 2, 2), state: v.substr(i + 4, 2) });
}
}
return out;
}
// Raw format (P2=00): ISD uses pure AID length, Apps use combined length (AID+lifecycle).
// Heuristic: length > 8 means combined (AID = length-1 bytes, lifecycle inside length).
// Raw format (P2=00): consecutive <aid_len><AID><lifecycle><privileges>.
@@ -8868,8 +8905,12 @@ function _parseRawAppEntry(hex, i) {
return { aid, lifecycle, privileges, next: j };
}
// Raw format (P2=00) ELF entry: <aid_len><AID><lifecycle><version>; the P1=10
// (ELF + modules) listing appends module entries (<len><AID>[, lifecycle]).
// Raw format (P2=00) ELF entry: <aid_len><AID><lifecycle><privileges>. The
// 4th byte is the privileges byte - the legacy GET STATUS response shape,
// cross-checked against GlobalPlatformPro's populate_legacy (for load files it
// must be 0x00); there is no version in the compact listing, it comes from the
// expanded 'CE' tag. The P1=10 (ELF + modules) listing appends
// <module_count> (<module_len><module_AID>)*.
// The walk is deterministic on the AID length - the old "scan for 0x10"
// heuristic derailed whenever a length byte or an AID byte happened to be
// 0x10 (which silently dropped entries like F0414C46416101).
@@ -8879,10 +8920,10 @@ function _parseRawElfEntry(hex, i, withModules) {
const aid = hex.substr(i + 2, aidLen * 2).toUpperCase();
let j = i + 2 + aidLen * 2;
const lifecycle = hex.substr(j, 2).toUpperCase(); j += 2;
const version = hex.substr(j + 2, 2).toUpperCase(); j += 2;
const out = { aid, lifecycle, version, next: j };
const privileges = hex.substr(j, 2).toUpperCase(); j += 2;
const out = { aid, lifecycle, privileges, next: j };
if (!withModules) return out;
// P1=10 (ELF + modules) entry: ...<lifecycle><version><module_count>
// P1=10 (ELF + modules) entry: ...<lifecycle><privileges><module_count>
// followed by <module_len><module_AID> entries. Verified against two
// live traces (a F0414C46416101 ELF with its F0414C4641610101 module).
if (j + 2 > hex.length) return out;
@@ -8982,13 +9023,64 @@ function ramParseGetMemory(hex) {
}
// ===== Lifecycle / privilege formatting =====
const RAM_LIFECYCLE = {
'01': 'OP_READY', '07': 'SELECTABLE', '0F': 'PERSONALIZED',
'03': 'INSTALLED', '1F': 'SD_PERSONALIZED', '7F': 'LOCKED',
'FF': 'TERMINATED',
};
function ramFmtLifecycle(hex) {
return RAM_LIFECYCLE[hex] || ('0x' + hex);
// GP Card Spec v2.3.1 11.1.1: the life cycle coding depends on the registry
// object kind (Tables 11-3..11-6; the ISD inherits the card life cycle).
// `kind`: 'isd' (card life cycle) | 'app' | 'sd' | 'elf'. Cards may return
// extra bytes after the state (e.g. 9F70 02 0700): decode the first byte and
// surface non-zero extras instead of guessing what they are.
function ramFmtLifecycle(hex, kind) {
const s = (hex || '').toUpperCase();
if (!s) return '';
const b = parseInt(s.substr(0, 2), 16);
const extra = s.length > 2 && !/^0+$/.test(s.substr(2)) ? ' (0x' + s.substr(2) + ')' : '';
let name = '';
if (kind === 'elf') {
// GP Table 11-3: LOADED only.
if (b === 0x01) name = 'LOADED';
} else if (kind === 'isd') {
// GP Table 11-6: OP_READY / INITIALIZED / SECURED / CARD_LOCKED /
// TERMINATED (the ISD inherits the card life cycle).
name = { 1: 'OP_READY', 7: 'INITIALIZED', 15: 'SECURED', 127: 'CARD_LOCKED', 255: 'TERMINATED' }[b] || '';
} else if (kind === 'sd') {
// GP Table 11-5: INSTALLED / SELECTABLE / PERSONALIZED / LOCKED.
name = { 3: 'INSTALLED', 7: 'SELECTABLE', 15: 'PERSONALIZED' }[b] || '';
if (!name && (b & 0x83) === 0x83) name = 'LOCKED';
} else {
// GP Table 11-4 (default: an application): INSTALLED / SELECTABLE /
// application-specific bits b4-b7 (still selectable) / LOCKED.
if (b === 0x03) name = 'INSTALLED';
else if (b === 0x07) name = 'SELECTABLE';
else if ((b & 0x87) === 0x07) name = 'SELECTABLE (0x' + s.substr(0, 2) + ')';
else if ((b & 0x83) === 0x83) name = 'LOCKED';
}
return name ? name + extra : '0x' + s + extra;
}
// GP Table 11-37 Note 1: on a Java Card the Executable Load File Version
// Number is a 2-byte major/minor pair (in this order); other lengths stay raw.
function ramFmtVersion(hex) {
const s = (hex || '').toUpperCase();
if (/^[0-9A-F]{4}$/.test(s)) return parseInt(s.substr(0, 2), 16) + '.' + parseInt(s.substr(2, 2), 16);
return s;
}
// TS 102 226 Table 8.3 menu entry state: '00' disabled, '01' enabled.
function ramMenuState(state) {
const s = (state || '').toUpperCase();
if (s === '00') return t('Disabled');
if (s === '01') return t('Enabled');
return '0x' + s;
}
// Life cycle coding kind of an explorer entry (GP 11.1.1): an application
// carrying the Security Domain privilege (GP Table 11-7 byte 1 b8) is decoded
// as a Security Domain; the ISD section and the ELF section pass their kind
// explicitly.
function ramEntryKind(o) {
if (!o) return 'app';
if (o.type === 'elf') return 'elf';
const b1 = parseInt(((o.privileges || '').substr(0, 2)) || '0', 16);
return (b1 & 0x80) ? 'sd' : 'app';
}
// Decode the GP privileges value (tag C5 / compact listing) into
@@ -9015,6 +9107,44 @@ function ramActionBtn(label, action, tone) {
+ ' disabled:opacity-40 disabled:cursor-not-allowed">' + esc(label) + '</button>';
}
// One line per STK menu entry the card reports for an applet (EA SCP Registry
// Data, TS 102 226 8.2.1.6): identifier, position and state.
function ramMenuHtml(entries) {
let html = '';
(entries || []).forEach(m => {
html += '<div>' + esc(t('STK menu entry:')) + ' 0x' + esc(m.id) +
' (' + esc(t('position')) + ' ' + parseInt(m.pos, 16) + ', ' + esc(ramMenuState(m.state)) + ')</div>';
});
return html;
}
// Registry data objects the card returned but the Explore does not decode
// (GP Tables 11-36/11-37 and TS 102 226 extensions): preserved and shown, so
// nothing from the card's answer is silently dropped.
function ramRawTlvsHtml(rawTlvs) {
if (!rawTlvs || !rawTlvs.length) return '';
return '<div class="text-gray-500 dark:text-slate-400">' + esc(t('Other registry data:')) + ' ' +
rawTlvs.map(x => esc(x.tag) + '=' + esc(x.value)).join(' \u00b7 ') + '</div>';
}
// Merge the expanded (E3) pass into the compact entries: fill only the fields
// the compact listing does not carry (it has AID, life cycle and privileges,
// plus the module list for P1=10; the version, Load File AID, SD AID, menu
// entries and unknown registry data come from the expanded pass). A field
// already present in the compact entry is never overwritten.
function ramMergeExpanded(collector, parsed) {
const byAid = {};
collector.forEach(e => { if (e.aid) byAid[e.aid.toUpperCase()] = e; });
parsed.forEach(p => {
const cur = byAid[(p.aid || '').toUpperCase()];
if (!cur) return;
['privileges', 'lifecycle', 'version', 'elfAid', 'sdAid', 'implicitSel', 'menuEntries', 'rawTlvs'].forEach(k => {
if (p[k] && !cur[k]) cur[k] = p[k];
});
if (p.moduleAids && p.moduleAids.length) cur.moduleAids = p.moduleAids;
});
}
// Render the full explorer result as structured HTML
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
let html = '';
@@ -9036,9 +9166,12 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
isd.forEach(o => {
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-blue-400">';
html += '<div>' + esc(t('AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'isd') + '</div>';
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
html += ramMenuHtml(o.menuEntries);
html += ramRawTlvsHtml(o.rawTlvs);
html += '</div>';
});
html += '</div>';
@@ -9050,11 +9183,13 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-green-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', ramEntryKind(o)) + '</div>';
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
if (o.implicitSel) html += '<div>' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '</div>';
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
html += ramMenuHtml(o.menuEntries);
html += ramRawTlvsHtml(o.rawTlvs);
html += '</div>';
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
if (o.aid) {
@@ -9074,12 +9209,14 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-purple-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + o.version + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'elf') + '</div>';
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '</div>';
if (o.privileges && o.privileges !== '00') html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
if (o.moduleAids && o.moduleAids.length) {
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
}
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
html += ramRawTlvsHtml(o.rawTlvs);
html += '</div>';
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
if (o.aid) {
@@ -9210,16 +9347,7 @@ async function ramExplore(sp) {
collector.push(...parsed);
} else if (parsed.length) {
// Keep the compact entries, add the expanded-only fields
const byAid = {};
collector.forEach(e => { if (e.aid) byAid[e.aid.toUpperCase()] = e; });
parsed.forEach(p => {
const cur = byAid[(p.aid || '').toUpperCase()];
if (!cur) return;
['privileges', 'lifecycle', 'version', 'elfAid', 'sdAid', 'implicitSel'].forEach(k => {
if (p[k] && !cur[k]) cur[k] = p[k];
});
if (p.moduleAids && p.moduleAids.length) cur.moduleAids = p.moduleAids;
});
ramMergeExpanded(collector, parsed);
}
}
// The compact error only surfaces when the expanded pass did not rescue it
@@ -9726,6 +9854,17 @@ async function ramInstallApp(sp, op) {
ramShowInstallResult(data, t('OK'));
}
// Measured NV footprint of an install operation: the server reads GET DATA
// FF21 (free non-volatile memory) before and after the chain; the delta is
// what the operation consumed (negative = memory was freed). Cards without
// FF21 have no values and the line disappears.
function ramNvLine(data) {
if (!data || data.nv_before == null || data.nv_after == null) return '';
const used = data.nv_before - data.nv_after;
const label = used < 0 ? t('NV freed:') : t('NV used:');
return label + ' ~' + Math.abs(used) + ' B (' + t('free') + ' ' + data.nv_before + ' \u2192 ' + data.nv_after + ')';
}
// 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, capMem) {
@@ -9735,6 +9874,8 @@ function ramShowInstallResult(data, okText, capMem) {
const comps = capMem && capMem.components;
let txt = '';
(data.steps || []).forEach((s, idx) => { txt += ramStepLine(s, idx, comps, data.load_block_size) + '\n'; });
const nv = ramNvLine(data);
if (nv) txt += nv + '\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
@@ -10489,8 +10630,14 @@ function ramGetStatusApdu(p1, p2) {
// (GP Card Spec 11.4.2.3/11.4.3.1). Used as the fallback when a card rejects
// the deprecated compact format and for the optional details pass; the bytes
// are the reference trace form (`80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`).
// The ELF queries add '84' (Executable Module AIDs, GP Table 11-37): with a
// tag list present the card returns only the listed tags (11.4.3.1), so
// without it the expanded fallback would list packages without their modules
// (no module rows / Install buttons on cards that reject the compact form).
function ramExpandedQueryApdu(p1) {
return '80F2' + p1 + '02' + '0C' + '4F005C084F9F70C5C4CCCEEA' + '00';
const elf = p1 === '10' || p1 === '20';
const tags = '4F9F70C5C4CCCEEA' + (elf ? '84' : '');
return '80F2' + p1 + '02' + (elf ? '0D' : '0C') + '4F005C' + (elf ? '09' : '08') + tags + '00';
}
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
@@ -18189,7 +18336,13 @@ const LANG_RU = {
'KIc key and KID key are required': 'Укажите ключ KIc и ключ KID',
'RAM command format:': 'Формат команд RAM:',
'Fetch expanded registry details (load-file / SD associations)': 'Запрашивать расширенные данные реестра (связи load-файла и SD)',
'Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA). The compact listing carries AID/lifecycle/privileges/version only; the expanded pass adds the Load File AID, Security Domain AID and module lists. It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02, список тегов 4F 9F70 C5 C4 CC CE EA). Компактный список содержит только AID/состояние/привилегии/версию; расширенный проход добавляет AID load-файла, AID Security Domain и списки модулей. Он расходует дополнительные защищённые пакеты, и не все карты его поддерживают — Explore и так применяет его автоматически, когда компактный список ничего не вернул.',
'Runs one extra expanded query per listing (P2=02, tag list 4F 9F70 C5 C4 CC CE EA, plus 84 for the ELF queries). The compact listing carries AID, life cycle and privileges only - it has no version; the expanded pass adds the Executable Load File version, the Load File AID, the Security Domain AID, the module lists and the applet STK menu entries (SCP Registry Data). It costs extra secured packets and some cards reject it - the Explore already uses it automatically when the compact listing returns nothing.': 'Запускает дополнительный расширенный запрос для каждого списка (P2=02, список тегов 4F 9F70 C5 C4 CC CE EA, плюс 84 для запросов ELF). Компактный список содержит только AID, состояние и привилегии — версии в нём нет; расширенный проход добавляет версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей и пункты меню STK апплета (SCP Registry Data). Он расходует дополнительные защищённые пакеты, и не все карты его поддерживают — Explore и так применяет его автоматически, когда компактный список ничего не вернул.',
'STK menu entry:': 'Пункт меню STK:',
'position': 'позиция',
'Other registry data:': 'Прочие данные реестра:',
'NV used:': 'Занято NV:',
'NV freed:': 'Освобождено NV:',
'free': 'свободно',
'compact = the TS 102 226 5.2.1 command string (one C-APDU per secured packet, e.g. GET STATUS + GET RESPONSE); expanded = the AA/22 Command Scripting template (5.2.1.0). Detected per operation with a read-only GET STATUS probe; a card that rejects the deprecated compact P2.b2=0 form answers the expanded one.': 'compact = командная строка по TS 102 226 5.2.1 (один C-APDU на защищённый пакет, например GET STATUS + GET RESPONSE); expanded = шаблон Command Scripting AA/22 (5.2.1.0). Определяется для каждой операции пробным чтением GET STATUS; карта, отклоняющая устаревшую компактную форму P2.b2=0, отвечает в расширенной.',
'Card resources (GET DATA FF21)': 'Ресурсы карты (GET DATA FF21)',
'Extended Card Resources (GP tag FF21, ETSI TS 102 226 8.2.1.7.2): installed applications, free non-volatile and free volatile memory': 'Расширенные ресурсы карты (тег GP FF21, ETSI TS 102 226 8.2.1.7.2): число установленных приложений, свободная энергонезависимая и свободная энергозависимая память',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v323';
const CACHE = 'simple-v324';
const URLS = [
'index.html',
'help.html',
+163 -6
View File
@@ -22,9 +22,9 @@ function extractFunc(src, name) {
}
// Extract chain builder functions and dependencies
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = '';
@@ -33,8 +33,6 @@ for (const f of FNS) {
}
const m = html.match(/const _chains = \{\};/);
if (m) code += m[0].replace(/^const /, 'var ') + '\n';
const lc = html.match(/const RAM_LIFECYCLE = \{[\s\S]*?\n\};/);
if (lc) code += lc[0].replace(/^const /, 'var ') + '\n';
const pn = html.match(/const PRIVILEGE_NAMES = \[[\s\S]*?\n\];/);
if (pn) code += pn[0].replace(/^const /, 'var ') + '\n';
const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
@@ -222,7 +220,8 @@ test('ramRenderExploreHtml localizes every label and button', () => {
const out = ramRenderExploreHtml(
{ appCount: 5, freeNV: 100, freeV: 50 },
[{ aid: 'A000000151000000', lifecycle: '07', privileges: '', sdAid: 'A000000151000000' }],
[{ aid: 'A1130001180001', lifecycle: '07', privileges: '80', implicitSel: '00', elfAid: 'ELF1' }],
[{ aid: 'A1130001180001', lifecycle: '07', privileges: '80', implicitSel: '00', elfAid: 'ELF1',
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'], sdAid: null }]
);
delete global.t;
@@ -240,6 +239,9 @@ test('ramRenderExploreHtml localizes every label and button', () => {
assert.ok(seen.includes('Application / Instance AID:'));
assert.ok(seen.includes('Load File AID / Package AID:'));
assert.ok(seen.includes('Executable Module AIDs / Applet Class AIDs:'));
assert.ok(seen.includes('STK menu entry:'));
assert.ok(seen.includes('position'));
assert.ok(seen.includes('Other registry data:'));
assert.ok(!out.includes('data-l10n'), out);
// the well-known ISD AID is annotated (GP Card Spec v2.3.1 H.1.3)
assert.ok(out.includes('A000000151000000 <span class="text-gray-400 dark:text-slate-500">(GlobalPlatform Issuer Security Domain)</span>'), out);
@@ -544,10 +546,17 @@ test('ramCardIdxAfterRemove keeps the remembered index aligned', () => {
test('ramExpandedQueryApdu matches the reference trace bytes', () => {
// TCA Loader: `AA14 2212 80F28002 0C 4F00 5C08 4F9F70C5C4CCCEEA 00`
for (const p1 of ['80', '40', '20', '10']) {
for (const p1 of ['80', '40']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020C4F005C084F9F70C5C4CCCEEA00');
}
// The ELF queries add '84' (Executable Module AIDs): with a tag list the
// card returns only the listed tags, so the expanded fallback would
// otherwise list packages without their modules (v3.6.51).
for (const p1 of ['20', '10']) {
assert.strictEqual(ramExpandedQueryApdu(p1),
'80F2' + p1 + '020D4F005C094F9F70C5C4CCCEEA8400');
}
});
test('ramParseAppStatus parses the expanded E3 listing (vendor traces)', () => {
@@ -586,6 +595,10 @@ test('ramParseElfStatus lists the compact ELF and module listings (F0414C4641610
const f041 = elfs.find(r => r.aid === 'F0414C46416101');
assert.ok(f041, JSON.stringify(elfs.map(r => r.aid)));
assert.strictEqual(f041.lifecycle, '01');
// the 4th byte is the privileges byte (the legacy GET STATUS shape);
// the compact listing has no version - it comes from the expanded CE tag
assert.strictEqual(f041.privileges, '00');
assert.strictEqual(f041.version, undefined);
assert.ok(elfs.some(r => r.aid === 'A1130001180002FFF7100E8904000200'), 'A113 ELF missing');
assert.ok(elfs.some(r => r.aid === 'A0000000090005FFFFFFFF8912000000'), 'uicc.toolkit ELF missing');
@@ -596,6 +609,150 @@ test('ramParseElfStatus lists the compact ELF and module listings (F0414C4641610
assert.deepStrictEqual(f041Row.moduleAids, ['F0414C4641610101']);
});
test('_parseE3Entry collects Executable Module AIDs from every 84 TLV', () => {
// GP Table 11-37: one Executable Module AID per '84' TLV (the reference
// GPPro coding), repeated per module.
const one = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039008410A1130001180001FFFFFFFF89A1003908');
assert.deepStrictEqual(one.moduleAids, ['A1130001180001FFFFFFFF89A1003908']);
const two = _parseE3Entry('4F10A1130001180002FFF7100E89040002008410A1130001180002FFF7100E89040002088410A1130001180002FFF7100E89494D4508');
assert.deepStrictEqual(two.moduleAids,
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
// a card concatenating 4F<len><aid> TLVs inside one 84 still parses
const concat = _parseE3Entry('4F10A1130001180002FFF7100E890400020084244F10A1130001180002FFF7100E89040002084F10A1130001180002FFF7100E89494D4508');
assert.deepStrictEqual(concat.moduleAids,
['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508']);
});
test('_parseE3Entry reads the SCP Registry Data menu entries (NP trace)', () => {
// Explore_NP.log app entry: ... EA 05 80 03 01 80 01 = menu entry
// position 1, identifier 0x80, enabled (TS 102 226 Tables 8.2/8.3).
const r = _parseE3Entry('4F10A1130001180001FFFFFFFF89A10039089F70020700C503000000C410A1130001180001FFFFFFFF89A1003900CE020903CC07A0000001510000EA058003018001');
assert.strictEqual(r.aid, 'A1130001180001FFFFFFFF89A1003908');
assert.strictEqual(r.lifecycle, '0700');
assert.strictEqual(r.version, '0903');
assert.strictEqual(r.sdAid, 'A0000001510000');
assert.strictEqual(r.elfAid, 'A1130001180001FFFFFFFF89A1003900');
assert.deepStrictEqual(r.menuEntries, [{ pos: '01', id: '80', state: '01' }]);
assert.strictEqual(r.rawTlvs, undefined);
// an empty EA (an ISD with no menu entries) is not an error and adds nothing
const empty = _parseE3Entry('4F08A000000151000000EA00');
assert.strictEqual(empty.menuEntries, undefined);
assert.strictEqual(empty.rawTlvs, undefined);
});
test('_parseE3Entry preserves registry data objects it does not decode', () => {
// Explore_NP.log ISD entry: the card returned 8F/90/91/92 beyond the
// requested GP tags - shown raw, never dropped.
const r = _parseE3Entry('4F07A00000015100009F70020F00C5039AFF80C407A0000001515350CE020001CC07A00000015100008F02752B9002012D910265D592020BD3');
assert.deepStrictEqual(r.rawTlvs, [
{ tag: '8F', value: '752B' }, { tag: '90', value: '012D' },
{ tag: '91', value: '65D5' }, { tag: '92', value: '0BD3' },
]);
assert.strictEqual(r.privileges, '9AFF80');
});
test('ramFmtVersion formats the 2-byte CE version as major.minor', () => {
// GP Table 11-37 Note 1: Java Card CAP major/minor attributes, in order.
assert.strictEqual(ramFmtVersion('0903'), '9.3');
assert.strictEqual(ramFmtVersion('0100'), '1.0');
assert.strictEqual(ramFmtVersion('0A0B'), '10.11');
// other lengths stay raw (the format depends on the load file format)
assert.strictEqual(ramFmtVersion('09'), '09');
assert.strictEqual(ramFmtVersion(''), '');
});
test('ramFmtLifecycle decodes per GP object kind (Tables 11-3..11-6)', () => {
// ISD inherits the card life cycle (Table 11-6)
assert.strictEqual(ramFmtLifecycle('01', 'isd'), 'OP_READY');
assert.strictEqual(ramFmtLifecycle('07', 'isd'), 'INITIALIZED');
assert.strictEqual(ramFmtLifecycle('0F', 'isd'), 'SECURED');
assert.strictEqual(ramFmtLifecycle('7F', 'isd'), 'CARD_LOCKED');
assert.strictEqual(ramFmtLifecycle('FF', 'isd'), 'TERMINATED');
// Executable Load File (Table 11-3)
assert.strictEqual(ramFmtLifecycle('01', 'elf'), 'LOADED');
// Application (Table 11-4)
assert.strictEqual(ramFmtLifecycle('03', 'app'), 'INSTALLED');
assert.strictEqual(ramFmtLifecycle('07', 'app'), 'SELECTABLE');
assert.strictEqual(ramFmtLifecycle('0F', 'app'), 'SELECTABLE (0x0F)');
assert.strictEqual(ramFmtLifecycle('83', 'app'), 'LOCKED');
// Security Domain (Table 11-5)
assert.strictEqual(ramFmtLifecycle('0F', 'sd'), 'PERSONALIZED');
assert.strictEqual(ramFmtLifecycle('8F', 'sd'), 'LOCKED');
// a card may append extra bytes (NP trace: 9F70 02 0700)
assert.strictEqual(ramFmtLifecycle('0700', 'app'), 'SELECTABLE');
assert.strictEqual(ramFmtLifecycle('0701', 'app'), 'SELECTABLE (0x01)');
// unknown values stay visible
assert.strictEqual(ramFmtLifecycle('42', 'app'), '0x42');
assert.strictEqual(ramFmtLifecycle('', 'app'), '');
});
test('ramMenuState and ramEntryKind decode menu state and SD detection', () => {
global.t = s => s;
assert.strictEqual(ramMenuState('00'), 'Disabled');
assert.strictEqual(ramMenuState('01'), 'Enabled');
assert.strictEqual(ramMenuState('02'), '0x02');
delete global.t;
// GP Table 11-7 byte 1 b8 = Security Domain privilege
assert.strictEqual(ramEntryKind({ privileges: '9AFE80' }), 'sd');
assert.strictEqual(ramEntryKind({ privileges: '04' }), 'app');
assert.strictEqual(ramEntryKind({ type: 'elf', privileges: '00' }), 'elf');
assert.strictEqual(ramEntryKind(null), 'app');
});
test('the explorer shows versions, menu entries and unknown registry data', () => {
global.t = s => s;
const out = ramRenderExploreHtml(
{ appCount: 1, freeNV: 10, freeV: 5 },
[],
[{ aid: 'A1130001180001FFFFFFFF89A1003908', lifecycle: '0700', privileges: '000000',
elfAid: 'A1130001180001FFFFFFFF89A1003900', sdAid: 'A0000001510000',
menuEntries: [{ pos: '01', id: '80', state: '01' }], rawTlvs: [{ tag: '8F', value: '752B' }] }],
[{ aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00', version: '0309' }]
);
delete global.t;
// the app row: SELECTABLE from the 2-byte 0700, menu entry, raw TLV
assert.ok(out.includes('SELECTABLE'), out);
assert.ok(out.includes('STK menu entry: 0x80 (position 1, Enabled)'), out);
assert.ok(out.includes('Other registry data: 8F=752B'), out);
// the ELF row: LOADED, formatted version, no privileges noise for '00'
assert.ok(out.includes('LOADED'), out);
assert.ok(out.includes('Version: 3.9'), out);
const elfSection = out.slice(out.indexOf('Executable Load Files'));
assert.ok(!elfSection.includes('Privileges:'), elfSection);
});
test('ramMergeExpanded fills only what the compact listing lacks', () => {
// the compact ELF entry has no version (v3.6.51): the expanded CE version
// and SD AID merge in - this was blocked while the bogus compact "version"
// was still set
const compact = [{ type: 'elf', aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' }];
const expanded = [{ aid: 'A1130001180001FFFFFFFF89A1003900', version: '0309', sdAid: 'A0000001510000', lifecycle: '01' }];
ramMergeExpanded(compact, expanded);
assert.strictEqual(compact[0].version, '0309');
assert.strictEqual(compact[0].sdAid, 'A0000001510000');
// a field the compact entry already carries is never overwritten
const apps = [{ aid: 'A1', privileges: '04' }];
ramMergeExpanded(apps, [{ aid: 'A1', privileges: '9AFE80', menuEntries: [{ pos: '01', id: '80', state: '01' }] }]);
assert.strictEqual(apps[0].privileges, '04');
assert.deepStrictEqual(apps[0].menuEntries, [{ pos: '01', id: '80', state: '01' }]);
// module lists come from the expanded listing when it has them
const elfs = [{ aid: 'E1' }];
ramMergeExpanded(elfs, [{ aid: 'E1', moduleAids: ['M1'] }]);
assert.deepStrictEqual(elfs[0].moduleAids, ['M1']);
});
test('ramNvLine reports the measured NV footprint', () => {
global.t = s => s;
assert.strictEqual(ramNvLine({ nv_before: 50646, nv_after: 23400 }),
'NV used: ~27246 B (free 50646 \u2192 23400)');
assert.strictEqual(ramNvLine({ nv_before: 100, nv_after: 150 }),
'NV freed: ~50 B (free 100 \u2192 150)');
assert.strictEqual(ramNvLine({ nv_before: 100 }), '');
assert.strictEqual(ramNvLine({}), '');
assert.strictEqual(ramNvLine(null), '');
delete global.t;
});
test('ramParseAppStatus keeps 16-byte AIDs (no rawLen-1 truncation)', () => {
const page = '08D276000005AA3F010704'
+ '0FD276000005AA060200000000B000000700'
+11 -6
View File
@@ -38,14 +38,19 @@ test('gp 6310 label does not reference removed P2=42 option', () => {
});
test('LIFECYCLE_MAP per GPC v2.3 life cycles', () => {
assert.strictEqual(LIFECYCLE_MAP['01'], 'OP_READY (card) / LOADED (ELF)');
assert.strictEqual(LIFECYCLE_MAP['03'], 'INSTALLED');
assert.strictEqual(LIFECYCLE_MAP['07'], 'SELECTABLE');
assert.strictEqual(LIFECYCLE_MAP['0F'], 'SECURED (card)');
assert.strictEqual(LIFECYCLE_MAP['1F'], 'PERSONALIZED (SD)');
// GP Card Spec v2.3.1 11.1.1 Tables 11-3..11-6: the coding depends on the
// object kind (the ISD inherits the card life cycle), so the shared map
// names every interpretation. The Explore decodes per kind instead
// (ramFmtLifecycle).
assert.strictEqual(LIFECYCLE_MAP['01'], 'OP_READY (card/ISD) / LOADED (ELF)');
assert.strictEqual(LIFECYCLE_MAP['03'], 'INSTALLED (app/SD)');
assert.strictEqual(LIFECYCLE_MAP['07'], 'SELECTABLE (app/SD) / INITIALIZED (card/ISD)');
assert.strictEqual(LIFECYCLE_MAP['0F'], 'SECURED (card/ISD) / PERSONALIZED (SD)');
assert.strictEqual(LIFECYCLE_MAP['1F'], 'App-specific (app)');
assert.strictEqual(LIFECYCLE_MAP['7F'], 'CARD_LOCKED');
assert.strictEqual(LIFECYCLE_MAP['FF'], 'TERMINATED');
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED (SD)');
assert.strictEqual(LIFECYCLE_MAP['83'], 'LOCKED');
assert.strictEqual(LIFECYCLE_MAP['8F'], 'LOCKED');
});
test('TS 51.011 SIM families resolve (94xx/98xx/92xx/9Exx/9Fxx)', () => {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.50"
version = "3.6.51"
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.
+73 -12
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.50'
VERSION = '3.6.51'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1479,13 +1479,18 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
return step, error
def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex, silent=False):
"""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'])."""
(the step record is the last element of state['steps']).
`silent=True` (the NV footprint reads) leaves no step record and no
failure note behind: the card's answer is only used by the caller, and a
rejected read must never fail the operation. The decoded step record is
still available as `state['last_step']`."""
spi1, spi2 = sp['spi1'], sp['spi2']
try:
sp_hex, _ = _build_secured_packet(spi1, spi2, sp['kic'], sp['kid'], sp['tar'],
@@ -1496,9 +1501,10 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
sys.stderr.write('RAM C-APDU (%s): %s\n' % (step_name, apdu_hex))
sys.stderr.write('RAM SECURED-PACKET (%s): %s\n' % (step_name, sp_hex))
except ValueError as e:
state['encode_error'] = str(e)
state['steps'].append({'name': step_name, 'por_status': 'encode_error',
'sw': str(e)})
if not silent:
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
# Capture the PoR from either transport: inline in the ENVELOPE response
@@ -1516,10 +1522,11 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
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')})
if not silent:
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
@@ -1541,7 +1548,9 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
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)
state['last_step'] = step
if not silent:
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 '',
@@ -1550,7 +1559,8 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
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)
if not silent:
state['failure']['error'] = '%s: %s' % (step_name, step_error)
return False
return True
finally:
@@ -1558,6 +1568,41 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
scc._tp.proactive_handler = old_proactive
def _ram_free_nv(por_data_hex):
"""Free non-volatile memory (bytes) from a GET DATA FF21 R-APDU, or None.
Reuses the FF21 decoder (TS 102 226 8.2.1.7.2: '81' applet count /
'82' free NV / '83' free volatile)."""
try:
rapdu = bytes.fromhex(por_data_hex or '')
except ValueError:
return None
mem = _scp81_decode_memory(rapdu)
return mem.get('free_nv') if mem else None
def _ram_read_ff21(server, scc, sp, state, ram_format, label):
"""Best-effort GET DATA FF21 (Extended Card Resources) read around an
install: returns the free non-volatile memory in bytes, or None (a card
without FF21 or a rejected read must never fail the operation). The read
is sent through the normal step sender with `silent=True`, so it consumes
and advances the counter correctly but leaves no step record or failure
note behind."""
apdu = _ram_format_apdu('80CAFF2100', ram_format)
if not _ram_send_gp_apdu(server, scc, sp, state, label, apdu, silent=True):
return None
step = state.get('last_step') or {}
return _ram_free_nv(step.get('por_data'))
def _ram_nv_fields(nv_before, nv_after):
"""NV footprint fields for the install responses: the free-NV values and
their delta (None when the card has no FF21 readout)."""
if nv_before is None and nv_after is None:
return {}
delta = (nv_before - nv_after) if (nv_before is not None and nv_after is not None) else None
return {'nv_before': nv_before, 'nv_after': nv_after, 'nv_delta': delta}
def _parse_response_scripting(data):
"""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),
@@ -6765,6 +6810,8 @@ class PysimHandler(BaseHTTPRequestHandler):
# Live progress for the PWA's operation modal (the request holds
# _CARD_LOCK until the chain is done; /api/status carries this)
_ram_progress_begin('install-cap', len(seq) + 1)
nv_before = None
nv_after = None
try:
# RAM command format: detected per operation (a read-only
# probe step) unless the caller pinned compact/expanded.
@@ -6774,6 +6821,10 @@ class PysimHandler(BaseHTTPRequestHandler):
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
len(seq), len(seq) - 2, ram_format, loadfile_aid))
# NV footprint: free non-volatile memory before the chain
# (best effort - a card without FF21 skips it).
nv_before = _ram_read_ff21(self.server, scc, sp_state, state,
ram_format, 'NV before')
for apdu_idx, gp_apdu in enumerate(seq):
if apdu_idx == 0:
@@ -6786,6 +6837,11 @@ class PysimHandler(BaseHTTPRequestHandler):
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
step_name,
_ram_format_apdu(gp_apdu, ram_format)):
# What the failed attempt consumed (a rejected load
# may keep or discard its partial file - the delta
# tells which).
nv_after = _ram_read_ff21(self.server, scc, sp_state, state,
ram_format, 'NV after')
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
'error': (state['encode_error'] or state['failure'].get('error')
or ('%s failed' % step_name)),
@@ -6794,9 +6850,13 @@ class PysimHandler(BaseHTTPRequestHandler):
'load_block_size': block_size,
'load_block_size_requested': block_size_req,
'load_block_size_auto': not block_size_req}
resp.update(_ram_nv_fields(nv_before, nv_after))
self._send_json(resp)
self._log_resp(resp)
return
# NV footprint: what the successful chain consumed.
nv_after = _ram_read_ff21(self.server, scc, sp_state, state,
ram_format, 'NV after')
finally:
_ram_progress_end()
@@ -6805,6 +6865,7 @@ class PysimHandler(BaseHTTPRequestHandler):
'ram_format': ram_format, 'load_block_size': block_size,
'load_block_size_requested': block_size_req,
'load_block_size_auto': not block_size_req}
resp.update(_ram_nv_fields(nv_before, nv_after))
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
loadfile_aid, module_aid, state['cntr']))
self._send_json(resp)
+87
View File
@@ -16,6 +16,7 @@ import threading
import unittest
import urllib.error
import urllib.request
from unittest import mock
from pysim_simple_server.__main__ import _build_http_server
from pysim_simple_server.server import PysimHandler, _CARD_FREE_GET, _CARD_LOCK
@@ -222,5 +223,91 @@ class CapCompatHttpTests(unittest.TestCase):
self.assertIn('import probe', resp['error'])
class RamInstallNvFootprintHttpTests(unittest.TestCase):
"""The RAM install response carries the measured NV footprint (v3.6.51):
the server reads GET DATA FF21 before and after the chain (best effort)
and reports the free-NV values and their delta."""
def setUp(self):
from pysim_simple_server import server as srv
self.srv = srv
self.server = _build_http_server('127.0.0.1', 0, PysimHandler)
self.server.log_requests = False
self.server.app = None
self.server.sl = None
self.server.scc = object()
self.server.sms_oa = '12345'
self.server.sms_sc = '12345678912'
threading.Thread(target=self.server.serve_forever, daemon=True).start()
self.port = self.server.server_address[1]
def tearDown(self):
self.server.shutdown()
self.server.server_close()
def _post(self, path, body):
req = urllib.request.Request(
'http://127.0.0.1:%d%s' % (self.port, path),
data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json'}, method='POST')
try:
with urllib.request.urlopen(req, timeout=5) as res:
return res.status, json.loads(res.read() or b'{}')
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or b'{}')
def _install(self, fail_at=None, nv_before=50000, nv_after=20000):
def fake_send(server, scc, sp, state, name, apdu, silent=False):
if fail_at == name:
state['steps'].append({'name': name, 'por_status': 'por_error', 'por_sw': '6A88'})
return False
state['steps'].append({'name': name, 'por_status': 'por_ok', 'por_sw': '9000'})
return True
def fake_read(server, scc, sp, state, ram_format, label):
return nv_before if label == 'NV before' else nv_after
patches = [
mock.patch.object(self.srv, '_cap_parse',
lambda cap_hex: ('F0414C46416101', 'F0414C4641610101', b'\x01\x02')),
mock.patch.object(self.srv, '_cap_apdu_sequence',
lambda *a, **k: ['80E60200', '80E80000', '80E60000']),
mock.patch.object(self.srv, '_ram_detect_format', lambda *a, **k: 'compact'),
mock.patch.object(self.srv, '_ram_send_gp_apdu', fake_send),
mock.patch.object(self.srv, '_ram_read_ff21', fake_read),
]
for p in patches:
p.start()
try:
return self._post('/api/ram-install', {'cap_hex': 'AA'})
finally:
for p in reversed(patches):
p.stop()
def test_success_reports_the_nv_delta(self):
status, resp = self._install()
self.assertEqual(status, 200, resp)
self.assertTrue(resp['success'], resp)
self.assertEqual(resp['nv_before'], 50000)
self.assertEqual(resp['nv_after'], 20000)
self.assertEqual(resp['nv_delta'], 30000)
def test_failed_install_still_reports_the_nv_delta(self):
status, resp = self._install(fail_at='LOAD (1/1)')
self.assertEqual(status, 200, resp)
self.assertFalse(resp['success'], resp)
self.assertEqual(resp['failed_step'], 2)
self.assertEqual(resp['nv_before'], 50000)
self.assertEqual(resp['nv_after'], 20000)
self.assertEqual(resp['nv_delta'], 30000)
def test_a_card_without_ff21_has_no_nv_fields(self):
status, resp = self._install(nv_before=None, nv_after=None)
self.assertEqual(status, 200, resp)
self.assertTrue(resp['success'], resp)
for key in ('nv_before', 'nv_after', 'nv_delta'):
self.assertNotIn(key, resp)
if __name__ == '__main__':
unittest.main()
+87
View File
@@ -37,6 +37,9 @@ from pysim_simple_server.server import (
_por_remote_sw,
_ram_next_cntr,
_ram_format_apdu,
_ram_free_nv,
_ram_read_ff21,
_ram_nv_fields,
_wrap_expanded_apdu,
_ram_remote_sw_ok,
_sms_submit_por,
@@ -1585,6 +1588,90 @@ class RamSendGpApduLoggingTest(unittest.TestCase):
self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)
class RamNvFootprintTests(unittest.TestCase):
"""GET DATA FF21 reads around an install (v3.6.51): the free-NV values and
their delta. The reads are best-effort - a card without FF21 (or a
rejected read) must never fail the operation."""
def test_free_nv_from_ff21_response(self):
# TS 102 226 8.2.1.7.2: '81' applet count, '82' free NV, '83' free
# volatile (the length byte follows FF21).
data = 'FF210C' + '810102' + '820300C5D6' + '83020064'
self.assertEqual(_ram_free_nv(data), 0xC5D6)
# CR-set tag variants (0x01/0x02/0x03) decode the same
self.assertEqual(_ram_free_nv('FF210C' + '010102' + '020300C5D6' + '03020064'), 0xC5D6)
# not an FF21 response / empty / garbage
self.assertIsNone(_ram_free_nv('6A88'))
self.assertIsNone(_ram_free_nv(''))
self.assertIsNone(_ram_free_nv('not-hex'))
def test_nv_fields(self):
self.assertEqual(_ram_nv_fields(None, None), {})
self.assertEqual(_ram_nv_fields(50000, 20000),
{'nv_before': 50000, 'nv_after': 20000, 'nv_delta': 30000})
# a freed delta is negative (memory released by the operation)
self.assertEqual(_ram_nv_fields(100, 150),
{'nv_before': 100, 'nv_after': 150, 'nv_delta': -50})
# only one read available: the delta stays None
self.assertEqual(_ram_nv_fields(100, None),
{'nv_before': 100, 'nv_after': None, 'nv_delta': None})
def test_read_ff21_uses_the_silent_sender(self):
from pysim_simple_server import server as srv
calls = []
def fake_send(server, scc, sp, state, name, apdu, silent=False):
calls.append((name, apdu, silent))
if not silent:
state['steps'].append({'name': name})
state['last_step'] = {'por_data': 'FF210C810102820300C5D683020064'}
return True
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
with mock.patch.object(srv, '_ram_send_gp_apdu', fake_send):
nv = _ram_read_ff21(None, None, {}, state, 'expanded', 'NV before')
self.assertEqual(nv, 0xC5D6)
self.assertEqual(calls, [('NV before', _wrap_expanded_apdu('80CAFF2100'), True)])
self.assertEqual(state['steps'], [])
self.assertEqual(state['failure'], {})
def test_read_ff21_returns_none_when_the_read_fails(self):
from pysim_simple_server import server as srv
def fake_send(server, scc, sp, state, name, apdu, silent=False):
return False
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
with mock.patch.object(srv, '_ram_send_gp_apdu', fake_send):
self.assertIsNone(_ram_read_ff21(None, None, {}, state, 'compact', 'NV after'))
def test_silent_send_leaves_no_step_or_failure(self):
import contextlib
import io
from pysim_simple_server import server as srv
class FakeServer:
sms_oa = '12345'
sms_sc = '12345678912'
sp = {'spi1': '16', 'spi2': '01', 'kic': '25', 'kid': '25', 'tar': '000000',
'kic_key': 'AA', 'kid_key': 'BB', 'include_cpi': True}
state = {'steps': [], 'encode_error': None, 'failure': {}, 'cntr': '0000000001'}
with mock.patch.object(srv, '_build_secured_packet', lambda *a, **k: ('AABB', {})), \
mock.patch.object(srv, '_send_secured_packet',
lambda *a, **k: {'success': False, 'error': 'stub'}):
buf = io.StringIO()
with contextlib.redirect_stderr(buf):
ok = srv._ram_send_gp_apdu(FakeServer(), object(), sp, state,
'NV before', '80CAFF2100', silent=True)
self.assertFalse(ok)
self.assertEqual(state['steps'], [])
self.assertEqual(state['failure'], {})
self.assertIsNone(state['encode_error'])
self.assertIn('RAM C-APDU (NV before): 80CAFF2100', buf.getvalue())
class RamCommandFormatTests(unittest.TestCase):
"""RAM command format detection helpers (TS 102 226 5.2.1, v3.6.24)."""