From 2c49c54d2d07f538fb28377986d9be3b9378d993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 2 Oct 2026 13:16:08 +0300 Subject: [PATCH] v3.13.2: ELF versions derived through the package's module AIDs ramDeriveElfVersions matches an applet instance to its package through a shared ramElfAppMatch: the app's C4 (Load File AID) when the card reports the real one, then the package's module AIDs (the applet instance AIDs), then the shared-prefix fallback (the same tiers ramElfAppletCandidate uses for the per-AID queries). Live 2026-10-02 (test-eSIM): the card returns C4 = the applet's own AID (c4 10 ...3908 for package ...3900), so the old C4-only derivation filled nothing; the module lists map ...3900 -> ...3908 (9.3), ...0200 -> ...0208/...494D4508 (1.1) and F0414C46416001 -> F0414C4641600101 (0.1). The ELF version hint names the remaining case: a package without an applet instance has no version to derive either (the Oracle A00000006203010101 stays bare - the card refuses every ELF tag-list form, so its CE is unreachable). Docs: help EN/RU, repo AGENTS (v3.13.2 note). sw cache simple-v372. Tests: 667 Python + 796 frontend green. --- frontend/help-ru.html | 2 +- frontend/help.html | 2 +- frontend/index.html | 68 ++++++++++++++++++++++------------- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 48 ++++++++++++++++++++++++- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 7 files changed, 95 insertions(+), 31 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 5c7a691..895f8e6 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -314,7 +314,7 @@

Кнопка Проверить в pySim сверяет собранный пакет с эталонной реализацией OtaDialectSms.encode_cmd. Кнопка Отправить на карту доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется конкатенированной загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку «Разбор R-APDU» (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.

3.2 RAM

-

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Страница следующего вхождения (P2=03), отклонённая форматом, тоже повторяется в остальных форматах — сначала expanded-кодирования (первым то, что обслужило предыдущую страницу, если оно expanded), компактная командная строка последней: компактная попытка страницы может израсходовать страницу, не доставив её (карта отвечает 61xx, а сцепленный GET RESPONSE отклоняется — все последующие форматы видят вхождение уже использованным) — некоторые карты отдают expanded-страницу только в шаблоне AA — и пометка (только первая страница) ставится, только если карта обещала продолжение (6310), а ни один формат не смог его получить; в режиме Авто проводник также опрашивает по AID записи, оставшиеся без деталей (несколько дополнительных пакетов). Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии).

+

Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает ключи, таблицу TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000; SPI1 берётся из MSL этого TAR). Селектор Keyset рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор Формат фиксирует формат удалённой команды: Авто (определение) опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; Компактный отправляет голую C-APDU; Expanded (AA, определённая длина) / Expanded (AE 80, неопределённая длина) заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Страница следующего вхождения (P2=03), отклонённая форматом, тоже повторяется в остальных форматах — сначала expanded-кодирования (первым то, что обслужило предыдущую страницу, если оно expanded), компактная командная строка последней: компактная попытка страницы может израсходовать страницу, не доставив её (карта отвечает 61xx, а сцепленный GET RESPONSE отклоняется — все последующие форматы видят вхождение уже использованным) — некоторые карты отдают expanded-страницу только в шаблоне AA — и пометка (только первая страница) ставится, только если карта обещала продолжение (6310), а ни один формат не смог его получить; в режиме Авто проводник также опрашивает по AID записи, оставшиеся без деталей (несколько дополнительных пакетов). Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии). Пакет, чей собственный листинг не несёт версии, берёт её у своего экземпляра апплета, если он есть — модульные AID пакета это AID экземпляров апплета, и строка получает соответствующую пометку — так что карта, отклоняющая все формы со списком тегов, всё равно показывает версии пакетов везде, где есть экземпляр апплета.

Операции

diff --git a/frontend/help.html b/frontend/help.html index f872247..03c830e 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -313,7 +313,7 @@

A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference OtaDialectSms.encode_cmd. A “Send to Card” button delivers it via SMS-PP-DOWNLOAD ENVELOPE (when connected to the server). The 2-byte CPL prefix is transmitted whenever the packet is ciphered or carries RC/CC/DS (it is part of their input) and whenever the packet is concatenated; a single unprotected SMS keeps pySim’s CHL-first form (TS 31.115 Table 1 NOTE). Under the packet field the app reports its size and how many SMS it takes; a packet that does not fit one SMS is sent as a concatenated download (TS 31.115 §4.3) — the packet is split into SMS user-data parts (first SM 132 octets, following ones 134; the first one additionally carries the concatenation and CPI information elements) and the segments are sent in order. More than 5 segments is refused, that being the practical limit of the card’s concatenation buffer. The returned Proof of Receipt is decoded and shown as a PoR status line (status, TAR, counter, raw PoR); the last command’s status word and response data are filled into the R-APDU parser panel, and a successful PoR advances the replay counter (when SPI1 carries a counter check — b5 b4 ≠ 00; without one no counter is advanced or saved) and clears the packet. A low-counter verdict (cntr_low, or a warning ENVELOPE answer that still carries such a PoR) is not recoverable by retrying: every SCP80/RAM operation stops immediately. The card never reports its counter (the PoR’s CNTR field is a copy of the packet’s), so the notice offers a bounded Sync counter probe — a read-only command with increasing counter values (doubling steps, a 32-bit ceiling, at most 40 packets) until the card accepts one, then accepted+1 is stored — and a Go to preset button for raising the counter by hand; nothing else is written automatically.

3.2 RAM

-

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A next-occurrence page (P2=03) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers 61xx and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the AA template — and a listing is marked (first page only) only when the card promised more (6310) and no format could fetch the follow-up; in Auto the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions).

+

Delivers Remote Application Management operations as SCP80 secured packets via SMS-PP-DOWNLOAD ENVELOPE. The card must support SCP03 (AES or 3DES). A saved card preset from the Cards tab provides the keys, the TAR table and the counter (RAM uses the preset's ISD TAR, 000000 by default; the SPI1 is derived from that TAR's MSL). The Keyset selector next to the preset selector picks the keyset whose keys and counter the operation uses. It is disabled until a card preset is selected. The start button stays disabled until a card preset and one of its keysets are selected. The Format selector pins the remote command format: Auto (detect) probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, Compact sends the bare C-APDU, and Expanded (AA, definite length) / Expanded (AE 80, indefinite length) wrap it in the TS 102 226 §5.2.1 scripting template (both execute on the live ISD; a template whose declared lengths do not match its body draws a Bad format TLV, which the operation shows as the step failure). The listing and registry queries follow that format: compact listings keep the chained GET RESPONSE, while an expanded run sends the same P2=00 queries wrapped and unchained (the reference TCA loader’s form, Le=’00’ returns the listing directly), and a registry query the card refuses in the current format is retried in the other formats — the report names the one that served it. A next-occurrence page (P2=03) that a format refuses is retried in the other formats too — the expanded codings first (the serving one first when it is expanded), the compact command string last, because a compact page attempt can consume the page without delivering it (the card answers 61xx and the chained GET RESPONSE fails; every later format then sees the occurrence already gone) — some cards serve the expanded next page only wrapped in the AA template — and a listing is marked (first page only) only when the card promised more (6310) and no format could fetch the follow-up; in Auto the explorer also queries entries the bulk listing left without details individually by AID (a few extra packets). The card-resources block also names the card’s inferred ETSI release (uicc.toolkit version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions). A package whose own listing carries no version derives it from its applet instance when one exists — the package’s module AIDs are the applet instance AIDs, and the row is annotated as coming from the app instance — so a card that refuses every tag-list form still shows package versions wherever an applet instance exists.

Operations

diff --git a/frontend/index.html b/frontend/index.html index de164a9..e6e39e8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1877,7 +1877,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.13.1'; +const SIMPLE_VERSION = '3.13.2'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -9675,11 +9675,17 @@ function ramExpandedElfForm(report) { // Why the ELF rows show no version (pure). The version exists only in the // tag-list forms of the expanded query: without a tag list the card builds the // default E3 (AID + life cycle + modules, GP 11.4.3.1). -function ramElfVersionHint(elfForm) { - if (elfForm === 'notags') { - return t('Package versions are only in the tag-list forms of the expanded registry query \u2014 this card accepted only the no-tag-list form.'); +function ramElfVersionHint(elfForm, elfs, apps) { + let hint = elfForm === 'notags' + ? t('Package versions are only in the tag-list forms of the expanded registry query \u2014 this card accepted only the no-tag-list form.') + : t('Package versions come from the expanded registry query \u2014 this card did not return them.'); + // A package with no applet instance has no version to derive either (the + // derivation maps the package's module AIDs to the app entries). + const versioned = (apps || []).filter(a => a && a.aid && a.version); + if ((elfs || []).some(e => e && !e.version && !ramElfAppMatch(e, versioned))) { + hint += ' ' + t('A package without an applet instance has no version to derive either.'); } - return t('Package versions come from the expanded registry query \u2014 this card did not return them.'); + return hint; } // The card's ETSI release from its installed uicc.toolkit package (pure): @@ -9699,33 +9705,44 @@ function cardEtsiRelease(elfs) { return { release: ETSI_TOOLKIT_RELEASE[v] || '', basis: 'uicc.toolkit ' + v }; } -// The applet instance that belongs to a package (pure). The package's module -// AIDs are the applet instance AIDs - they can differ from the package AID in -// the last byte (live 2026-09-30: package A113...0200 with modules ...0208 / -// ...494D4508, all present as applet instances); a shared AID prefix is the -// fallback rule. Applets that already carry a version are skipped. -function ramElfAppletCandidate(elf, apps) { +// Match a package to one of its applet instances (pure). Three rules, in +// order: the app's C4 (Load File AID) names the package when the card reports +// the real one; the package's module AIDs are the applet instance AIDs (they +// can differ from the package AID in the last byte - live 2026-09-30: package +// A113...0200 with modules ...0208 / ...494D4508, all present as applet +// instances; live 2026-10-02: the test-eSIM returns C4 = the applet's own AID, +// so the module list is the mapping that works there); a shared AID prefix is +// the last fallback. The caller picks the candidate set (versionless app +// entries for the per-AID queries, versioned ones for the version derivation). +function ramElfAppMatch(elf, apps) { if (!elf || !elf.aid) return null; - const cands = (elf.moduleAids || []).concat([elf.aid]) - .map(c => String(c).toUpperCase()); - const list = (apps || []).filter(a => a && a.aid && !a.version); - const exact = list.find(a => cands.indexOf(String(a.aid).toUpperCase()) >= 0); - if (exact) return exact; const pkg = String(elf.aid).toUpperCase(); - return list.find(a => String(a.aid).toUpperCase().indexOf(pkg) === 0) || null; + const cands = (elf.moduleAids || []).concat([pkg]).map(c => String(c).toUpperCase()); + const list = (apps || []).filter(a => a && a.aid); + return list.find(a => String(a.elfAid || '').toUpperCase() === pkg) || + list.find(a => cands.indexOf(String(a.aid).toUpperCase()) >= 0) || + list.find(a => String(a.aid).toUpperCase().indexOf(pkg) === 0) || null; +} + +// The versionless applet instance to query for a package's version (pure): +// applets that already carry a version are skipped. +function ramElfAppletCandidate(elf, apps) { + return ramElfAppMatch(elf, (apps || []).filter(a => a && a.aid && !a.version)); } // Fill a missing ELF version from an application entry: cards commonly return -// the load file version (CE) on the app entry together with its package AID -// (C4), so the package's version can be read from its applet instance (live -// 2026-09-30: the ELF listing refused every tag list while the apps listing -// carried CE). The row is annotated (`versionFrom`) so the inference stays -// visible. Packages without an applet instance get no version this way. +// the load file version (CE) on the app entry (live 2026-09-30: the ELF +// listing refused every tag list while the apps listing carried CE), so the +// package's version can be read from its applet instance - via the app's C4 +// or, when the card returns the applet's own AID there (live 2026-10-02), via +// the package's module AIDs (ramElfAppMatch). The row is annotated +// (`versionFrom`) so the inference stays visible. Packages without an applet +// instance get no version this way. function ramDeriveElfVersions(elfs, apps) { + const list = (apps || []).filter(a => a && a.aid && a.version); (elfs || []).forEach(e => { if (e.version || !e.aid) return; - const app = (apps || []).find(a => a.version && a.elfAid && - String(a.elfAid).toUpperCase() === String(e.aid).toUpperCase()); + const app = ramElfAppMatch(e, list); if (app) { e.version = app.version; e.versionFrom = 'app'; @@ -9815,7 +9832,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) { html += '
'; html += '
' + esc(t('Executable Load Files (ELFs) / Packages')) + ' (' + elfs.length + ')
'; if (elfs.some(o => !o.version)) { - html += '
' + esc(ramElfVersionHint(elfForm)) + '
'; + html += '
' + esc(ramElfVersionHint(elfForm, elfs, apps)) + '
'; } elfs.forEach(o => { html += '
'; @@ -21027,6 +21044,7 @@ const LANG_RU = { 'Package versions come from the expanded registry query \u2014 this card did not return them.': 'Версии пакетов приходят из расширенного запроса реестра \u2014 эта карта их не вернула.', 'Package versions are only in the tag-list forms of the expanded registry query \u2014 this card accepted only the no-tag-list form.': 'Версии пакетов есть только в формах расширенного запроса со списком тегов \u2014 эта карта приняла только форму без списка тегов.', 'from the app instance': 'от экземпляра апплета', + 'A package without an applet instance has no version to derive either.': 'У пакета без экземпляра апплета версию тоже неоткуда вывести.', 'request error': 'ошибка запроса', 'Low counter': 'Низкий счётчик', 'the card rejected the packet': 'карта отклонила пакет', diff --git a/frontend/sw.js b/frontend/sw.js index 935f9cc..e2d7f8b 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v371'; +const CACHE = 'simple-v372'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index a26ffdc..cde793d 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -26,7 +26,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB 'ramFormatIsExpanded', 'ramFmtLadder', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', - 'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', + 'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'ramElfAppMatch', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx', @@ -636,6 +636,47 @@ test('ramDeriveElfVersions reads a package version from its applet instance', () assert.strictEqual(elfs[2].version, undefined); }); +test('ramDeriveElfVersions falls back to the package module AIDs', () => { + // live 2026-10-02 (test-eSIM): the card returns C4 = the applet's own AID, + // so the C4 rule matches nothing - the package's module AIDs (the applet + // instance AIDs) are the mapping that works there + const elfs = [ + { aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }, + { aid: 'A1130001180002FFF7100E8904000200', moduleAids: ['A1130001180002FFF7100E8904000208', 'A1130001180002FFF7100E89494D4508'] }, + { aid: 'F0414C46416001', moduleAids: ['F0414C4641600101'] }, + { aid: 'A00000006203010101', moduleAids: [] }, + ]; + const apps = [ + { aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003908' }, + { aid: 'A1130001180002FFF7100E8904000208', version: '0101', elfAid: 'A1130001180002FFF7100E8904000208' }, + { aid: 'A1130001180002FFF7100E89494D4508', version: '0101', elfAid: 'A1130001180002FFF7100E89494D4508' }, + { aid: 'F0414C4641600101', version: '0001', elfAid: 'F0414C4641600101' }, + ]; + ramDeriveElfVersions(elfs, apps); + assert.strictEqual(elfs[0].version, '0903'); + assert.strictEqual(elfs[0].versionFrom, 'app'); + assert.strictEqual(elfs[1].version, '0101'); + assert.strictEqual(elfs[2].version, '0001'); + // no applet instance: nothing to derive from + assert.strictEqual(elfs[3].version, undefined); +}); + +test('ramElfVersionHint names a package with no applet instance', () => { + globalThis.t = s => s; + const elfs = [{ aid: 'A1130001180001FFFFFFFF89A1003900', moduleAids: ['A1130001180001FFFFFFFF89A1003908'] }]; + const apps = [{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903' }]; + // a versionless package with a matching applet instance needs no clause + const ok = ramElfVersionHint('notags', elfs, apps); + // ...but a package with no applet instance is called out + const orphan = ramElfVersionHint('notags', [{ aid: 'A00000006203010101', moduleAids: [] }], apps); + // the base hints stay form-aware + const base = ramElfVersionHint('tags', [], []); + delete globalThis.t; + assert.match(ok, /no-tag-list form\.$/); + assert.match(orphan, /no version to derive either\.$/); + assert.match(base, /did not return them\.$/); +}); + test('ramElfAppletCandidate maps a package to its applet instance', () => { // live 2026-09-30: the applet instance AIDs are the package's module AIDs // (they differ from the package AID in the last byte) @@ -659,6 +700,11 @@ test('ramElfAppletCandidate maps a package to its applet instance', () => { assert.strictEqual(ramElfAppletCandidate(null, apps), null); // the shared-prefix rule is the fallback assert.strictEqual(ramElfAppletCandidate({ aid: 'ABCD' }, [{ aid: 'ABCDEF' }]).aid, 'ABCDEF'); + // the app's C4 (Load File AID) is the authoritative mapping when the card + // reports the real package AID + assert.strictEqual(ramElfAppletCandidate({ aid: 'A0000005591010FFFFFFFF8900000E00' }, + [{ aid: 'A0000005591010FFFFFFFF8900001200', elfAid: 'A0000005591010FFFFFFFF8900000E00' }]).aid, + 'A0000005591010FFFFFFFF8900001200'); }); test('the explorer annotates a derived package version', () => { diff --git a/pyproject.toml b/pyproject.toml index 7042c72..ad36f7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.13.1" +version = "3.13.2" 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. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 31b1d96..79b1f64 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.13.1' +VERSION = '3.13.2' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE