From 00181ba3ad4ceb543c7122510c1d403d67605e3e 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 00:48:45 +0300
Subject: [PATCH] Infer the card's ETSI release from its installed uicc.toolkit
version
The explorer's card-resources block now names the card's ETSI release, using
the same mapping as the CAP analysis (capmem.ETSI_TOOLKIT_RELEASE: uicc.toolkit
version -> REL-N, basis in brackets; the live card's 1.12 -> REL-15/16/17).
The table is mirrored in the PWA (drift-guarded by tests/test_cap_memory.py,
like JC_FRAMEWORK_SDK); the version comes from the expanded registry's CE tag
and degrades to the version hint when the listing has none; SIM-only cards (no
uicc.toolkit) get no label, the same rule as SIM-only CAPs.
- cardEtsiRelease(elfs) (pure) + UICC_TOOLKIT_AID / ETSI_TOOLKIT_RELEASE in the
PWA; rendered after the free-memory lines with a tooltip naming the basis.
- Tests: helper vectors (1.12/1.11/1.5, unknown version, versionless,
SIM-only), the rendering guard, and the table/AID drift guard.
- Help EN/RU; sw cache simple-v354.
---
frontend/help-ru.html | 2 +-
frontend/help.html | 2 +-
frontend/index.html | 39 ++++++++++++++++++++++++++++++++++++++
frontend/sw.js | 2 +-
frontend/tests/ram.test.js | 36 ++++++++++++++++++++++++++++++++++-
tests/test_cap_memory.py | 14 ++++++++++++++
6 files changed, 91 insertions(+), 4 deletions(-)
diff --git a/frontend/help-ru.html b/frontend/help-ru.html
index f8772dc..96d6f33 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). Пресет карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000). Селектор 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’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал.
+ Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки Карты обеспечивает SPI, ключи, TAR и счётчик (RAM использует ISD TAR пресета, по умолчанию 000000). Селектор 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’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал. Блок ресурсов карты также называет предполагаемый релиз ETSI карты (версия uicc.toolkit → REL-N, то же сопоставление, что и в анализе CAP; показывается, только если расширенный реестр вернул версии).
Операции
diff --git a/frontend/help.html b/frontend/help.html
index 31f52d0..0a239fe 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 SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default). 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.
+ 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 SPI, keys, TAR, and counter (RAM uses the preset's ISD TAR, 000000 by default). 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. 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).
Operations
diff --git a/frontend/index.html b/frontend/index.html
index c345b62..27c16df 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8965,6 +8965,19 @@ const JC_AID_RIDS = {
'A000000151': 'GlobalPlatform RID',
};
+// The card's ETSI release, inferred from its installed uicc.toolkit package
+// version - the same table the CAP analysis uses (capmem.ETSI_TOOLKIT_RELEASE,
+// drift-guarded by tests/test_cap_memory.py; the basis is the workspace
+// sim-tools corpus: the REL-7/REL-12 prepared trees and the TS 102 241
+// package-version table). Only uicc.toolkit has a documented mapping -
+// SIM-only cards get no label, the same rule as SIM-only CAPs.
+const UICC_TOOLKIT_AID = 'A0000000090005FFFFFFFF8912000000';
+const ETSI_TOOLKIT_RELEASE = {
+ '1.0': 'REL-6/7', '1.1': 'REL-7', '1.2': 'REL-7', '1.3': 'REL-8',
+ '1.4': 'REL-7', '1.5': 'REL-9', '1.11': 'REL-12',
+ '1.12': 'REL-15/16/17', '1.13': 'REL-17', '1.14': 'REL-17/18',
+};
+
// Normalise an AID (strip spacing, uppercase) and reject malformed input;
// '' when it cannot be an AID (5-16 bytes).
function jcAidNorm(aid) {
@@ -9436,6 +9449,20 @@ function ramElfVersionHint(elfForm) {
return t('Package versions come from the expanded registry query \u2014 this card did not return them.');
}
+// The card's ETSI release from its installed uicc.toolkit package (pure):
+// {release, basis} when the version maps, {release: '', basis} when the
+// version is outside the table, {release: '', basis: ''} when the package
+// carries no version (the compact listing has none - the expanded registry's
+// CE tag provides it), and null when the card has no uicc.toolkit package at
+// all (SIM-only cards get no label, the same rule as SIM-only CAPs).
+function cardEtsiRelease(elfs) {
+ const tk = (elfs || []).find(e => (e.aid || '').toUpperCase() === UICC_TOOLKIT_AID);
+ if (!tk) return null;
+ const v = String(tk.version || '').trim();
+ if (!v) return { release: '', basis: '' };
+ 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 /
@@ -9479,6 +9506,15 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
html += '' + esc(t('Applications:')) + ' ' + (mem.appCount != null ? mem.appCount : '?') + '
';
html += '' + esc(t('Free NV:')) + ' ' + (mem.freeNV != null ? mem.freeNV + ' B' : '?') + '
';
html += '' + esc(t('Free Volatile:')) + ' ' + (mem.freeV != null ? mem.freeV + ' B' : '?') + '
';
+ const etsi = cardEtsiRelease(elfs);
+ if (etsi) {
+ let val;
+ if (etsi.release) val = etsi.release + (etsi.basis ? ' (' + etsi.basis + ')' : '');
+ else if (etsi.basis) val = t('unknown') + ' (' + etsi.basis + ')';
+ else val = t('unknown') + ' \u2014 ' + ramElfVersionHint(elfForm);
+ html += '' +
+ esc(t('ETSI release:')) + ' ' + esc(val) + '
';
+ }
if (fmt) {
html += '' +
esc(t('RAM command format:')) + ' ' + esc(fmt) + '
';
@@ -19603,6 +19639,9 @@ const LANG_RU = {
'Executable Module AIDs / Applet Class AIDs:': 'AID исполняемых модулей / классов апплетов:',
'Applications:': 'Приложения:',
'Free NV:': 'Свободно NV:',
+ 'ETSI release:': 'Релиз ETSI:',
+ 'unknown': 'неизвестно',
+ 'Inferred from the card\'s installed uicc.toolkit package version (the same table as the CAP analysis); the expanded registry query provides the versions.': 'Определяется по версии установленного на карте пакета uicc.toolkit (та же таблица, что и в анализе CAP); версии даёт расширенный запрос реестра.',
'Free Volatile:': 'Свободно volatile:',
'AID:': 'AID:',
'Lifecycle:': 'Жизненный цикл:',
diff --git a/frontend/sw.js b/frontend/sw.js
index 6ec1617..45429a4 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v353';
+const CACHE = 'simple-v354';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js
index af04e9d..dffb599 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', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate',
+ 'ramExpandedReport', 'ramExpandedMore', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate',
'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx',
@@ -50,6 +50,10 @@ const an = html.match(/const JC_AID_NAMES = \{[\s\S]*?\n\};/);
if (an) code += an[0].replace(/^const /, 'var ') + '\n';
const ar = html.match(/const JC_AID_RIDS = \{[\s\S]*?\n\};/);
if (ar) code += ar[0].replace(/^const /, 'var ') + '\n';
+const et = html.match(/const ETSI_TOOLKIT_RELEASE = \{[\s\S]*?\n\};/);
+if (et) code += et[0].replace(/^const /, 'var ') + '\n';
+const ua = html.match(/const UICC_TOOLKIT_AID = '[^']*';/);
+if (ua) code += ua[0].replace(/^const /, 'var ') + '\n';
globalThis.spPresetWarningRender = () => {};
globalThis.pysimApplyAvailability = () => {};
eval(code);
@@ -701,6 +705,36 @@ test('ramFmtLadder orders the registry command formats cautiously', () => {
assert.deepStrictEqual(ramFmtLadder('expanded-ae', 'expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
});
+test('cardEtsiRelease infers the card ETSI release from uicc.toolkit', () => {
+ const tk = v => ({ aid: 'A0000000090005FFFFFFFF8912000000', version: v });
+ assert.deepStrictEqual(cardEtsiRelease([tk('1.12')]),
+ { release: 'REL-15/16/17', basis: 'uicc.toolkit 1.12' });
+ assert.deepStrictEqual(cardEtsiRelease([tk('1.11')]),
+ { release: 'REL-12', basis: 'uicc.toolkit 1.11' });
+ assert.deepStrictEqual(cardEtsiRelease([tk('1.5')]),
+ { release: 'REL-9', basis: 'uicc.toolkit 1.5' });
+ // a version outside the table: the basis is still named
+ assert.deepStrictEqual(cardEtsiRelease([tk('9.9')]),
+ { release: '', basis: 'uicc.toolkit 9.9' });
+ // the compact listing carries no versions (the expanded CE tag does)
+ assert.deepStrictEqual(cardEtsiRelease([tk('')]), { release: '', basis: '' });
+ // no uicc.toolkit (SIM-only card): no label, like SIM-only CAPs
+ assert.strictEqual(cardEtsiRelease([{ aid: 'A0000000090003FFFFFFFF8910710002', version: '2.6' }]), null);
+ assert.strictEqual(cardEtsiRelease(null), null);
+});
+
+test('the explorer shows the inferred ETSI release', () => {
+ globalThis.t = s => s;
+ const out = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [],
+ [{ aid: 'A0000000090005FFFFFFFF8912000000', version: '1.12' }], 'compact', '', '');
+ delete globalThis.t;
+ assert.ok(out.includes('ETSI release: REL-15/16/17 (uicc.toolkit 1.12)'), out);
+ globalThis.t = s => s;
+ const none = ramRenderExploreHtml({ appCount: 1, freeNV: 2, freeV: 3 }, [], [], [], 'compact', '', '');
+ delete globalThis.t;
+ assert.ok(!none.includes('ETSI release:'), none);
+});
+
test('ramExpandedReport names the expanded serving format', () => {
globalThis.t = s => s;
const out = ramExpandedReport([
diff --git a/tests/test_cap_memory.py b/tests/test_cap_memory.py
index a501732..ebe1b91 100644
--- a/tests/test_cap_memory.py
+++ b/tests/test_cap_memory.py
@@ -407,6 +407,20 @@ class TestCapAnalyzer(unittest.TestCase):
pwa = dict(re.findall(r"'([\d.]+)':\s*'([^']+)'", m.group(1)))
self.assertEqual(pwa, capmem.JC_FRAMEWORK_JDK)
+ def test_etsi_table_matches_the_pwa(self):
+ # the PWA infers the card's ETSI release from its installed
+ # uicc.toolkit version with the same table as the CAP analysis
+ # (drift guard, like JC_FRAMEWORK_SDK above)
+ html = pathlib.Path(__file__).resolve().parents[1].joinpath(
+ 'frontend', 'index.html').read_text(encoding='utf-8')
+ m = re.search(r"const ETSI_TOOLKIT_RELEASE = \{(.*?)\};", html, re.S)
+ self.assertIsNotNone(m, 'ETSI_TOOLKIT_RELEASE not found in index.html')
+ pwa = dict(re.findall(r"'([\d.]+)':\s*'([^']+)'", m.group(1)))
+ self.assertEqual(pwa, capmem.ETSI_TOOLKIT_RELEASE)
+ a = re.search(r"const UICC_TOOLKIT_AID = '([0-9A-F]+)';", html)
+ self.assertIsNotNone(a, 'UICC_TOOLKIT_AID not found in index.html')
+ self.assertEqual(a.group(1), capmem.UICC_TOOLKIT_AID)
+
def test_platform_requirement_from_the_framework_import(self):
# java.lang only: no Java Card level named, but the int flag shows
report, _ = capmem.analyze_bytes(