v3.12.0: the card preset carries the card's CAT (USAT) release
The release selector lives in the preset rather than a global chip: presets already travel with the card (ICCID auto-selection, export/import), and the operations that will consume the release are preset-aware. - presets.py: the optional `release` field (4-18; empty = unspecified) joins PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store schema version is 5. - catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective release is the preset's own value, else Rel-12 (the safe level for cards whose release is not known; an unset release never filters anything away). The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded. - Cards tab: the editor's header row gains a compact CAT release select (unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option names the Rel-12 default and the tooltip explains the guard; the field is collected, cleared and restored with the other form fields. - Nothing filters yet: the Simulator's event parameters (stage 3) will use the effective release. - docs/api.md + help EN/RU updated; sw cache simple-v368.
This commit is contained in:
+7
-1
@@ -245,6 +245,12 @@ stored for later use (nothing consumes them yet). PIN/PUK must be **4–8
|
||||
decimal digits** when set (empty is fine; anything else is refused with `400`
|
||||
naming the field); the ADM key is 4–16 hex digits (a decimal PIN is fine).
|
||||
|
||||
**CAT (USAT) release.** The optional `release` field (4–18) names the release
|
||||
the card implements; the release guard hides CAT items introduced after it
|
||||
(the Simulator's event parameters, see `catrelease`). Empty means unspecified
|
||||
and resolves to Rel-12 (`catrelease.DEFAULT_RELEASE`) — an unset release never
|
||||
filters anything away.
|
||||
|
||||
**Counters.** The store is the source of truth for the SCP80 counters, one per
|
||||
keyset ("a dedicated counter shall be associated to each key version",
|
||||
Annex A.1 — the spec's wording). The operations below accept a `preset_id` and **persist the
|
||||
@@ -256,7 +262,7 @@ is not defined in preset '…' - add it in the Cards tab"}`), and so is a
|
||||
KIc/KID number mismatch. The Cards tab's export/import uses this API; the
|
||||
import also accepts presets exported by the older localStorage-based builds.
|
||||
|
||||
- `GET /api/presets` — `{"path": "…", "count": 2, "version": 4, "presets": [{…}]}`
|
||||
- `GET /api/presets` — `{"path": "…", "count": 2, "version": 5, "presets": [{…}]}`
|
||||
(card-free: it answers while a long card operation runs).
|
||||
- `POST /api/presets` — body = the preset fields (`keysets` included); returns
|
||||
`{"ok": true, "preset": {…}}`. A duplicate ICCID (digits, spaced or raw EF
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
<p class="mb-2">Каждый принятый картой пакет <strong>расходует счётчик</strong>, который сохраняется в keyset пресета — проба отклоняется, если в разрешённом SPI1 нет проверки счётчика или в пресете нет такого номера keyset. На живой карте: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> и <code class="font-mono text-sm">B00001</code> зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка «Карты»</h2>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка: по умолчанию показывается <strong>список пресетов</strong>, а <strong>Добавить пресет</strong> / <strong>Редактировать</strong> открывают <strong>редактор</strong> (отдельное представление) — <strong>Сохранить</strong> сохраняет пресет и возвращает к списку, <strong>Отмена</strong> отменяет правки. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля редактора сгруппированы в блоки с рамкой: <strong>PIN/ADM</strong> (необязательные коды карты — PIN1/PUK1/PIN2/PUK2 по 4–8 десятичных цифр и ключ ADM — сохраняются для будущего использования), <strong>TAR</strong> (таблица TAR), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (keysets и их счётчики) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка: по умолчанию показывается <strong>список пресетов</strong>, а <strong>Добавить пресет</strong> / <strong>Редактировать</strong> открывают <strong>редактор</strong> (отдельное представление) — <strong>Сохранить</strong> сохраняет пресет и возвращает к списку, <strong>Отмена</strong> отменяет правки. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля редактора сгруппированы в блоки с рамкой: <strong>PIN/ADM</strong> (необязательные коды карты — PIN1/PUK1/PIN2/PUK2 по 4–8 десятичных цифр и ключ ADM — сохраняются для будущего использования), <strong>TAR</strong> (таблица TAR), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (keysets и их счётчики) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). В шапке редактора также выбирается <strong>релиз CAT (USAT)</strong> карты — по нему механизм релизов скрывает элементы CAT, появившиеся позже (параметры событий в Симуляторе); «не указан» означает Rel-12, и неуказанный релиз ничего не фильтрует. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<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>
|
||||
|
||||
+1
-1
@@ -345,7 +345,7 @@
|
||||
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when the resolved SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) — <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), <strong>TAR</strong> (the TAR table), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab: it shows the <strong>preset list</strong> by default, and <strong>Add preset</strong> / <strong>Edit</strong> open the <strong>editor</strong> (a separate view) — <strong>Save</strong> stores the preset and returns to the list, <strong>Cancel</strong> discards the edits. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The editor groups the fields into bordered blocks: <strong>PIN/ADM</strong> (the optional card codes — PIN1/PUK1/PIN2/PUK2 as 4–8 decimal digits and the ADM key — stored for later use), <strong>TAR</strong> (the TAR table), <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (the keysets and their counters) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The editor's header row also carries the card's <strong>CAT (USAT) release</strong> — the release guard hides CAT items introduced after it (the Simulator's event parameters); unspecified means Rel-12, and an unset release never filters anything away. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<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">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
+42
-2
@@ -1636,6 +1636,26 @@
|
||||
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-36 shrink-0">
|
||||
<label for="cards-release" class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAT release">CAT release</label>
|
||||
<select id="cards-release" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800" data-l10n-title="The card's CAT (USAT) release. The Simulator hides event parameters introduced after it. Unspecified = Rel-12, safe for most modern cards.">
|
||||
<option value="" data-l10n="(unspecified — Rel-12)">(unspecified — Rel-12)</option>
|
||||
<option value="5" data-l10n="pre-Rel-6">pre-Rel-6</option>
|
||||
<option value="6">Rel-6</option>
|
||||
<option value="7">Rel-7</option>
|
||||
<option value="8">Rel-8</option>
|
||||
<option value="9">Rel-9</option>
|
||||
<option value="10">Rel-10</option>
|
||||
<option value="11">Rel-11</option>
|
||||
<option value="12">Rel-12</option>
|
||||
<option value="13">Rel-13</option>
|
||||
<option value="14">Rel-14</option>
|
||||
<option value="15">Rel-15</option>
|
||||
<option value="16">Rel-16</option>
|
||||
<option value="17">Rel-17</option>
|
||||
<option value="18">Rel-18</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row gap-3 mb-3">
|
||||
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 lg:w-56 shrink-0">
|
||||
@@ -1857,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.11.0';
|
||||
const SIMPLE_VERSION = '3.12.0';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -9154,6 +9174,20 @@ function catReleaseLabel(rel) {
|
||||
return rel < 6 ? 'pre-Rel-6' : 'Rel-' + rel;
|
||||
}
|
||||
|
||||
// The release a preset's empty `release` resolves to: Rel-12 is the safe
|
||||
// level for cards whose release is not known (the CAT items the tool sends
|
||||
// stay within what a modern card implements). Mirrors
|
||||
// catrelease.DEFAULT_RELEASE, drift-guarded by tests/test_catrelease.py.
|
||||
const CAT_RELEASE_DEFAULT = 12;
|
||||
|
||||
// The effective CAT release of a card preset (pure): its own release when it
|
||||
// names one (4-18), else the default. Empty or invalid values resolve to the
|
||||
// default - an unset release must never filter anything away.
|
||||
function presetRelease(preset) {
|
||||
const rel = parseInt((preset || {}).release, 10);
|
||||
return (rel >= 4 && rel <= 18) ? rel : CAT_RELEASE_DEFAULT;
|
||||
}
|
||||
|
||||
// Normalise an AID (strip spacing, uppercase) and reject malformed input;
|
||||
// '' when it cannot be an AID (5-16 bytes).
|
||||
function jcAidNorm(aid) {
|
||||
@@ -12157,6 +12191,7 @@ function cardsFormValues() {
|
||||
return {
|
||||
name: val('name'),
|
||||
iccid: val('iccid'),
|
||||
release: val('release'),
|
||||
pin1: val('pin1').replace(/\s/g, ''),
|
||||
puk1: val('puk1').replace(/\s/g, ''),
|
||||
pin2: val('pin2').replace(/\s/g, ''),
|
||||
@@ -12207,7 +12242,7 @@ function cardsNew() {
|
||||
}
|
||||
|
||||
function cardsClearForm() {
|
||||
['name','iccid','pin1','puk1','pin2','puk2','adm','psk-id','psk-key'].forEach(id => {
|
||||
['name','iccid','release','pin1','puk1','pin2','puk2','adm','psk-id','psk-key'].forEach(id => {
|
||||
const el = document.getElementById('cards-' + id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
@@ -12300,6 +12335,7 @@ function cardsEdit(i) {
|
||||
if (el) el.value = value == null ? '' : value;
|
||||
};
|
||||
set('name', c.name); set('iccid', c.iccid);
|
||||
set('release', c.release);
|
||||
set('pin1', c.pin1); set('puk1', c.puk1);
|
||||
set('pin2', c.pin2); set('puk2', c.puk2);
|
||||
set('adm', c.adm);
|
||||
@@ -20925,6 +20961,10 @@ const LANG_RU = {
|
||||
'Description': 'Описание',
|
||||
'Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.': 'Пресеты карт хранятся на сервере. Пресет содержит коды карты (PIN/PUK/ADM), таблицу TAR (у каждого TAR свой MSL) и несколько keysets — у каждого свой номер и счётчик.',
|
||||
'All codes are optional': 'Все коды необязательны',
|
||||
'CAT release': 'Релиз CAT',
|
||||
'(unspecified — Rel-12)': '(не указан — Rel-12)',
|
||||
'pre-Rel-6': 'до Rel-6',
|
||||
'The card\'s CAT (USAT) release. The Simulator hides event parameters introduced after it. Unspecified = Rel-12, safe for most modern cards.': 'Релиз CAT (USAT) карты. Симулятор скрывает параметры событий, появившиеся после него. Не указан = Rel-12, безопасно для большинства современных карт.',
|
||||
'The first three rows are the fixed role TARs; add further TARs as needed.': 'Первые три строки — фиксированные роли TAR; дополнительные TAR добавляются по необходимости.',
|
||||
'PIN/PUK codes must be 4-8 decimal digits': 'Коды PIN/PUK должны состоять из 4–8 десятичных цифр',
|
||||
'KIc/KID — counter': 'KIc/KID — счётчик',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v367';
|
||||
const CACHE = 'simple-v368';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -58,7 +58,7 @@ code = extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_
|
||||
eval(code);
|
||||
globalThis.t = s => s;
|
||||
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-pin1','cards-puk1','cards-pin2','cards-puk2','cards-adm',
|
||||
const CARD_IDS = ['cards-name','cards-iccid','cards-release','cards-pin1','cards-puk1','cards-pin2','cards-puk2','cards-adm',
|
||||
'cards-tars','cards-list-view','cards-editor-view','cards-editor-title','cards-new-btn',
|
||||
'cards-psk-id','cards-psk-key','cards-keysets',
|
||||
'cards-add-btn','cards-cancel-btn',
|
||||
@@ -319,6 +319,21 @@ test('the form values carry the codes and the TAR table', () => {
|
||||
assert.strictEqual(v.adm, '00112233');
|
||||
});
|
||||
|
||||
test('the release field is collected, cleared and restored', () => {
|
||||
const els = setup();
|
||||
els['cards-release'].value = '14';
|
||||
assert.strictEqual(cardsFormValues().release, '14');
|
||||
cardsClearForm();
|
||||
assert.strictEqual(els['cards-release'].value, '');
|
||||
globalThis.cards = [presetFixture({ name: 'Rel', release: '17' })];
|
||||
cardsEdit(0);
|
||||
assert.strictEqual(els['cards-release'].value, '17');
|
||||
// a preset without a release reads as unspecified
|
||||
globalThis.cards = [presetFixture({ name: 'Plain' })];
|
||||
cardsEdit(0);
|
||||
assert.strictEqual(els['cards-release'].value, '');
|
||||
});
|
||||
|
||||
test('saving a preset posts the form with the pre-settled TAR table', async () => {
|
||||
const els = setup();
|
||||
els['cards-name'].value = 'New card';
|
||||
|
||||
@@ -32,6 +32,8 @@ function extractFunc(src, name) {
|
||||
eval(extractBlock('const CAT_RELEASE = {', '// The display label of an introduction release')
|
||||
.replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'catReleaseLabel'));
|
||||
eval(extractBlock('const CAT_RELEASE_DEFAULT', 'function presetRelease').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'presetRelease'));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
|
||||
test('CAT_RELEASE carries the introduction releases of the event list', () => {
|
||||
@@ -77,3 +79,15 @@ test('catReleaseLabel collapses everything below Rel-6', () => {
|
||||
assert.strictEqual(catReleaseLabel(6), 'Rel-6');
|
||||
assert.strictEqual(catReleaseLabel(18), 'Rel-18');
|
||||
});
|
||||
|
||||
test('presetRelease resolves unspecified presets to the default', () => {
|
||||
assert.strictEqual(CAT_RELEASE_DEFAULT, 12);
|
||||
assert.strictEqual(presetRelease({ release: '14' }), 14);
|
||||
assert.strictEqual(presetRelease({ release: 4 }), 4);
|
||||
assert.strictEqual(presetRelease({ release: '18' }), 18);
|
||||
// an unset or invalid release must never filter anything away
|
||||
for (const p of [{}, { release: '' }, { release: '0' }, { release: '3' },
|
||||
{ release: '99' }, { release: 'x' }, null, undefined]) {
|
||||
assert.strictEqual(presetRelease(p), CAT_RELEASE_DEFAULT, JSON.stringify(p));
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.11.0"
|
||||
version = "3.12.0"
|
||||
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.
|
||||
|
||||
@@ -16,7 +16,9 @@ histories do not pin down - never filter on it.
|
||||
|
||||
The PWA mirrors this table (``CAT_RELEASE`` in frontend/index.html); the drift
|
||||
guard is tests/test_catrelease.py. Releases below 6 display as "pre-Rel-6"
|
||||
(``release_label``).
|
||||
(``release_label``). ``DEFAULT_RELEASE`` (Rel-12) is the release a card
|
||||
preset's empty ``release`` resolves to (``preset_release``) - the safe level
|
||||
for cards whose release is not known.
|
||||
"""
|
||||
|
||||
# Keyed by: events - the event code (TS 102 223 8.25 / TS 31.111 7.5);
|
||||
@@ -117,3 +119,24 @@ def release_label(rel):
|
||||
def item_release(kind, key, default=None):
|
||||
"""The introduction release of an item, or `default` when unknown."""
|
||||
return CAT_RELEASE.get(kind, {}).get(key, default)
|
||||
|
||||
|
||||
# The release a preset's empty `release` resolves to: Rel-12 is the safe level
|
||||
# for cards whose release is not known (the CAT items the tool sends stay
|
||||
# within what a modern card implements). Mirrored in the PWA
|
||||
# (`CAT_RELEASE_DEFAULT`), drift-guarded by tests/test_catrelease.py.
|
||||
DEFAULT_RELEASE = 12
|
||||
|
||||
|
||||
def preset_release(preset):
|
||||
"""The effective CAT release of a card preset.
|
||||
|
||||
The preset's own `release` when it names one (4-18), else the default.
|
||||
Empty or invalid values resolve to the default - an unset release must
|
||||
never filter anything away.
|
||||
"""
|
||||
try:
|
||||
rel = int((preset or {}).get('release') or 0)
|
||||
except (TypeError, ValueError):
|
||||
rel = 0
|
||||
return rel if 4 <= rel <= 18 else DEFAULT_RELEASE
|
||||
|
||||
@@ -24,6 +24,12 @@ The optional **card codes** (`pin1`/`puk1`/`pin2`/`puk2`/`adm`) are stored for
|
||||
later use by the card operations (nothing consumes them yet); the PIN/PUK codes
|
||||
are 4-8 decimal digits, the ADM key is hex or ASCII digits.
|
||||
|
||||
The optional **CAT (USAT) release** (`release`, 4-18) names the release the
|
||||
card implements: the release guard hides CAT items introduced after it (the
|
||||
Simulator's event parameters, see `catrelease`). Empty means unspecified and
|
||||
resolves to Rel-12 (`catrelease.DEFAULT_RELEASE`) - an unset release never
|
||||
filters anything away.
|
||||
|
||||
A preset holds **several keysets**; the b8..b5 nibble of KIc/KID numbers them
|
||||
(TS 102 225 5.1.2/A.2). Each keyset has its own KIc/KID keys and its own
|
||||
counter - "a dedicated counter shall be associated to each key version"
|
||||
@@ -63,14 +69,14 @@ from pathlib import Path
|
||||
|
||||
DEFAULT_DIR = '.pysim-simple-server'
|
||||
DEFAULT_FILENAME = 'card_presets.json'
|
||||
SCHEMA_VERSION = 4
|
||||
SCHEMA_VERSION = 5
|
||||
|
||||
# The fields a preset carries: the localStorage shape plus the id. The OTA key
|
||||
# material lives in `keysets` - the b8..b5 nibble of KIc/KID numbers them,
|
||||
# each with its own counter (TS 102 225 Annex A.1: "a dedicated counter shall
|
||||
# be associated to each key version"); the TAR table lives in `tars` (see
|
||||
# TAR_ROLES). The optional card codes (PIN/PUK/ADM) are stored for later use.
|
||||
PRESET_FIELDS = ('name', 'iccid', 'pin1', 'puk1', 'pin2', 'puk2', 'adm',
|
||||
PRESET_FIELDS = ('name', 'iccid', 'release', 'pin1', 'puk1', 'pin2', 'puk2', 'adm',
|
||||
'pskIdentity', 'pskKey')
|
||||
# The v3.8.0 flat key fields: converted into a single keyset on load/import.
|
||||
LEGACY_KEY_FIELDS = ('kic', 'kid', 'kicKey', 'kidKey', 'cntr')
|
||||
@@ -371,6 +377,9 @@ class PresetStore:
|
||||
raise PresetError('PSK identity and PSK key must be set together')
|
||||
if p['pskKey'] and not re.fullmatch(r'[0-9A-F]{32}', p['pskKey']):
|
||||
raise PresetError('PSK key must be 32 hex characters')
|
||||
if p['release'] and (not re.fullmatch(r'[0-9]{1,2}', p['release'])
|
||||
or not 4 <= int(p['release']) <= 18):
|
||||
raise PresetError('release must be 4-18 (or empty for the Rel-12 default)')
|
||||
for k in DIGIT_FIELDS:
|
||||
if p[k] and not re.fullmatch(r'[0-9]{4,8}', p[k]):
|
||||
raise PresetError('%s must be 4-8 decimal digits (or empty)'
|
||||
|
||||
@@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.11.0'
|
||||
VERSION = '3.12.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestCatReleaseTable(unittest.TestCase):
|
||||
def test_pwa_mirror_matches_the_module(self):
|
||||
# the PWA's release guard uses the same table (drift guard, like
|
||||
# ETSI_TOOLKIT_RELEASE in test_cap_memory)
|
||||
block, _ = _pwa_block()
|
||||
block, html = _pwa_block()
|
||||
ev = re.search(r"events:\s*\{(.*?)\}", block, re.S)
|
||||
self.assertIsNotNone(ev, 'events section not found')
|
||||
self.assertEqual(
|
||||
@@ -34,6 +34,9 @@ class TestCatReleaseTable(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
{k: int(v) for k, v in re.findall(r"'([^']+)':\s*(\d+)", m.group(1))},
|
||||
catrelease.CAT_RELEASE[section], section)
|
||||
d = re.search(r"const CAT_RELEASE_DEFAULT = (\d+);", html)
|
||||
self.assertIsNotNone(d, 'CAT_RELEASE_DEFAULT not found in index.html')
|
||||
self.assertEqual(int(d.group(1)), catrelease.DEFAULT_RELEASE)
|
||||
|
||||
def test_events_cover_the_pwa_event_list(self):
|
||||
# every event the UI names through 0x1F except the Void 0x1A carries
|
||||
@@ -60,6 +63,17 @@ class TestCatReleaseTable(unittest.TestCase):
|
||||
self.assertIsNone(catrelease.item_release('events', 0x1A))
|
||||
self.assertIsNone(catrelease.item_release('nope', 'x'))
|
||||
|
||||
def test_preset_release_resolves_the_default(self):
|
||||
self.assertEqual(catrelease.DEFAULT_RELEASE, 12)
|
||||
self.assertEqual(catrelease.preset_release({'release': '14'}), 14)
|
||||
self.assertEqual(catrelease.preset_release({'release': 4}), 4)
|
||||
self.assertEqual(catrelease.preset_release({'release': '18'}), 18)
|
||||
# an unset or invalid release must never filter anything away
|
||||
for p in ({}, {'release': ''}, {'release': '0'}, {'release': '3'},
|
||||
{'release': '99'}, {'release': 'x'}, None):
|
||||
self.assertEqual(catrelease.preset_release(p),
|
||||
catrelease.DEFAULT_RELEASE, p)
|
||||
|
||||
def test_table_sanity(self):
|
||||
for section, table in catrelease.CAT_RELEASE.items():
|
||||
self.assertTrue(table, section)
|
||||
|
||||
@@ -234,6 +234,21 @@ class StoreCrudTests(unittest.TestCase):
|
||||
self.assertEqual(p['pin1'], '1234')
|
||||
self.assertEqual(p['puk1'], '12345678')
|
||||
|
||||
def test_release_is_optional_and_must_be_4_to_18(self):
|
||||
p = self.store.add(preset())
|
||||
self.assertEqual(p['release'], '')
|
||||
p = self.store.add(preset(name='Rel', release='14'))
|
||||
self.assertEqual(p['release'], '14')
|
||||
# numbers arrive as strings from the form and as ints from the API
|
||||
p = self.store.add(preset(name='Int', release=4))
|
||||
self.assertEqual(p['release'], '4')
|
||||
p = self.store.add(preset(name='Top', release=' 18 '))
|
||||
self.assertEqual(p['release'], '18')
|
||||
for bad in ('3', '19', '0', '20', 'abc', '1x', '012'):
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
self.store.add(preset(name='X', release=bad))
|
||||
self.assertIn('release', str(ctx.exception))
|
||||
|
||||
def test_duplicate_iccid_is_refused_across_stored_forms(self):
|
||||
self.store.add(preset(iccid=DIGITS))
|
||||
with self.assertRaises(presets.PresetError) as ctx:
|
||||
|
||||
Reference in New Issue
Block a user