v3.25.0 review: the DF-tree ARR resolution walk-up

The live card references 6F06 from MF-tree files (MF/7F20/6F07 ->
8B 03 6F06 14), but the resolver compared only the path's first
segment: the reference fell back to the ADF.USIM copy instead of the
file's own DF copy (ISO 7816-4 5.4.3.3 / TS 102 221 9.2.7 walk-up) -
masked on this card only because all four copies are byte-identical.

- The server reads the DF.TELECOM and DF.GSM copies too (MF/7F10/6F06,
  MF/7F20/6F06 - TS 51.011), best effort.
- fcpArrRules resolves by the file's DF chain (own DF first, then
  walking up to MF); a reference with no copy in the chain resolves
  only when a single candidate exists, otherwise it stays silent.
- The implicit form prefers the file's DF copy (6F06), walking up to
  the MF copy (2F06).

Tests: the DF copy wins over the ADF copy; two candidates outside the
chain stay unresolved; a single one resolves; the implicit walk-up;
the server reads the DF-tree paths.  879 frontend + 858 Python;
sw cache simple-v424.
This commit is contained in:
2026-10-10 23:26:16 +03:00
parent 3bf005619e
commit a37266f865
7 changed files with 82 additions and 17 deletions
+1 -1
View File
@@ -397,7 +397,7 @@
<p class="mb-3">Подключение к локальному <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> для работы с картой: введите URL сервера (по умолчанию <code class="font-mono text-sm">http://127.0.0.1:8080</code>) и нажмите <strong>Подключиться</strong>. Область статуса показывает состояние ридера/карты, а <strong>Подключить карту</strong> (пере)инициализирует карту после вставки. Подвкладки: <strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong> и <strong>Отправка APDU</strong>. <strong>&laquo;Профайлер&raquo;</strong> и <strong>&laquo;Симулятор&raquo;</strong> — отдельные вкладки верхнего уровня.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 Файловый менеджер</h3>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись <code class="font-mono text-sm">EF.ARR</code> (атрибут безопасности <code class="font-mono text-sm">8B</code>), правила указанной записи показываются с отступом под этой строкой: они берутся из копий <code class="font-mono text-sm">EF.ARR</code> карты &mdash; копии MF (<code class="font-mono text-sm">2F06</code>) и копии ADF.USIM (<code class="font-mono text-sm">6F06</code>): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи <code class="font-mono text-sm">EF.ARR</code>; снимок разрешает те же ссылки из собственной захваченной копии. Ссылка на копию ARR другого DF не разрешается (строка остаётся как есть).</p>
<p class="text-sm mb-2">Дерево файловой системы отображается слева; выбор файла открывает панель деталей справа. Элементы сгруппированы: DF выше EF, сортировка по <strong>FID</strong> или символьному <strong>имени</strong> (пиллы и кнопка <strong>«Проверить все файлы»</strong> закреплены над прокручиваемым деревом; выбор сохраняется в <code class="font-mono text-sm">localStorage</code>). При выборе файла над содержимым также показываются FID, тип файла, размер / структура записей и декодированный FCI — в рамке с символьным именем файла. Если FCI ссылается на свои правила доступа через запись <code class="font-mono text-sm">EF.ARR</code> (атрибут безопасности <code class="font-mono text-sm">8B</code>), правила указанной записи показываются с отступом под этой строкой: они берутся из копий <code class="font-mono text-sm">EF.ARR</code> карты &mdash; копии MF (<code class="font-mono text-sm">2F06</code>), копий DF.TELECOM/DF.GSM (<code class="font-mono text-sm">6F06</code>) и копии ADF.USIM (<code class="font-mono text-sm">6F06</code>): читаются один раз за сессию при equip, обновляются при сбросе/equip и после записи <code class="font-mono text-sm">EF.ARR</code>; снимок разрешает те же ссылки из собственной захваченной копии. Побеждает копия собственного DF файла, с подъёмом до MF; ссылка, для которой в этой цепочке копии нет, остаётся неразрешённой, если только кандидат не единственный.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Читать сырые данные / Декодировать</strong> — чтение выбранного файла в виде hex-дампа или таблицы декодированных полей (клиентские декодеры EF: IMSI, ICCID, SPN, списки PLMN, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, таблицы сервисов, SUME, …); серверный pySim JSON того же чтения остаётся доступен в свёрнутом блоке <em>pySim JSON (сервер)</em>. Подсвеченная кнопка — текущий вид; нажатие любой из них (пере)читывает файл. «Декодировать» недоступна, если для файла нет клиентского декодера (проверка по имени/FID до чтения).</li>
<li><strong>Редактировать сырые данные</strong> — изменение hex-данных, <strong>Сохранить</strong> для записи (или <strong>Отмена</strong>); transparent-файлы редактируются в textarea, record-файлы — по одному полю на запись с флажками выбора. Декодированный вид доступен только для чтения — «Редактировать сырые данные» сначала переключает на hex-вид и при необходимости читает файл.</li>
+1 -1
View File
@@ -396,7 +396,7 @@
<p class="mb-3">Connects to a local <a href="https://github.com/anttro/simple" class="text-blue-600 dark:text-blue-400 hover:underline">pysim-simple-server</a> for live card operations: enter the server URL (default <code class="font-mono text-sm">http://127.0.0.1:8080</code>) and press <strong>Connect</strong>. The status area shows the reader/card state, and <strong>Equip card</strong> (re)initializes the card after insertion. Sub-tabs: <strong>File manager</strong>, <strong>pySim command line</strong>, and <strong>Raw APDU</strong>. The <strong>Profiler</strong> and <strong>Simulator</strong> are separate top-level tabs.</p>
<h3 id="file-manager" class="text-lg font-medium mb-2">6.1 File manager</h3>
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file&rsquo;s symbolic name — above the content pane. When the FCI references its access rules through an <code class="font-mono text-sm">EF.ARR</code> record (security attribute <code class="font-mono text-sm">8B</code>), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card&rsquo;s <code class="font-mono text-sm">EF.ARR</code> copies &mdash; the MF copy (<code class="font-mono text-sm">2F06</code>) and the ADF.USIM copy (<code class="font-mono text-sm">6F06</code>): read once per card session at equip, refreshed on reset/equip and after an <code class="font-mono text-sm">EF.ARR</code> write; a snapshot resolves the same references from its own captured copy. A reference to another DF&rsquo;s ARR copy is not resolved (the row stays as it is).</p>
<p class="text-sm mb-2">The file system tree is displayed on the left; selecting a file opens its detail pane on the right. Entries are grouped with DFs above EFs and sorted by <strong>FID</strong> or symbolic <strong>Name</strong> (pills pinned above the scrolling tree together with <strong>Probe all files</strong>; the choice is remembered in <code class="font-mono text-sm">localStorage</code>). Selecting a file also shows its FID, file type, size / record layout and the decoded FCI — in a bordered block labelled with the file&rsquo;s symbolic name — above the content pane. When the FCI references its access rules through an <code class="font-mono text-sm">EF.ARR</code> record (security attribute <code class="font-mono text-sm">8B</code>), the rules of the referenced record are shown as indented lines under that row: they are resolved from the card&rsquo;s <code class="font-mono text-sm">EF.ARR</code> copies &mdash; the MF copy (<code class="font-mono text-sm">2F06</code>), the DF.TELECOM/DF.GSM copies (<code class="font-mono text-sm">6F06</code>) and the ADF.USIM copy (<code class="font-mono text-sm">6F06</code>): read once per card session at equip, refreshed on reset/equip and after an <code class="font-mono text-sm">EF.ARR</code> write; a snapshot resolves the same references from its own captured copy. The copy of the file&rsquo;s own DF wins, walking up to MF; a reference with no copy in that chain stays unresolved unless a single candidate exists.</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Read raw / Read decoded</strong> — read the selected file as a hex dump or as a decoded field table (client-side EF decoders: IMSI, ICCID, SPN, PLMN lists, LOCI/PSLOCI/EPSLOCI, ADN/MSISDN, service tables, SUME, …); the server-side pySim JSON of the same read stays available in the collapsed <em>pySim JSON (server)</em> disclosure. The highlighted pill is the current view; clicking either pill (re-)reads the file. <strong>Read decoded</strong> is disabled when the client-side decoders do not cover the file (name/FID check, before reading).</li>
<li><strong>Edit raw</strong> — edit the raw hex data, <strong>Save</strong> to write back (or <strong>Cancel</strong>); transparent files use a textarea, record files one input per record with selection checkboxes. The decoded view is read-only — <strong>Edit raw</strong> switches to the hex view first and reads the file if needed.</li>
+29 -9
View File
@@ -20545,33 +20545,53 @@ function fcpWithoutArrRef(hex) {
// Access rules of the record(s) an FCP '8B' reference points at, resolved
// against a set of EF.ARR copies ({fid, path, records[]} - the server's
// /api/arr cache or a snapshot's own EF.ARR files). `path` is the tree path
// of the file whose FCI is shown: it picks the ARR copy of that DF (the
// ISO 7816-4 5.4.3.3 lookup order - ADF files use the ADF's 6F06 copy).
// of the file whose FCI is shown: the ARR copy is looked up per the ISO
// 7816-4 5.4.3.3 / TS 102 221 9.2.7 order - the file's own DF first, then
// walking up to the ADF/MF (a DF/ADF level holds 6F06, MF holds 2F06).
// Returns {found, rules}: found = at least one referenced record exists;
// rules = the decoded rule lines (empty when nothing resolves - the caller
// stays silent, v3.24.0).
// stays silent, v3.24.0; v3.25.0 walk-up fix).
function fcpArrRules(ref, arrs, path) {
if (!ref || !arrs || !arrs.length) return { found: false, rules: [] };
const df = (((path || '').split('/')[0]) || 'MF').toUpperCase();
// the file's DF chain, own DF first: MF/7F20/6F07 -> ['MF/7F20', 'MF'];
// ADF.USIM/6F07 -> ['ADF.USIM', 'MF']
const segs = ((path || '').toUpperCase().split('/')).filter(s => s);
const fileDf = segs.slice(0, -1);
const chain = [];
for (let i = fileDf.length; i > 0; i--) chain.push(fileDf.slice(0, i).join('/'));
if (!chain.includes('MF')) chain.push('MF');
const dfOf = a => String(a.path || '').toUpperCase().split('/').slice(0, -1).join('/');
const pickAt = (df, fid) => arrs.find(a =>
String(a.fid || '').toUpperCase() === fid && dfOf(a) === df) || null;
const pick = fid => {
const hits = arrs.filter(a => String(a.fid || '').toUpperCase() === fid);
if (!hits.length) return null;
return hits.find(a => String(a.path || '').split('/')[0].toUpperCase() === df) || hits[0];
for (const df of chain) {
const hit = hits.find(a => dfOf(a) === df);
if (hit) return hit;
}
// no copy in the file's DF chain: only a single candidate is safe
return hits.length === 1 ? hits[0] : null;
};
const out = [];
let found = false;
const add = (label, fid, record) => {
const arr = pick(fid);
const add = (label, fid, record, arrOverride) => {
const arr = arrOverride !== undefined ? arrOverride : pick(fid);
const recs = (arr && arr.records) || [];
if (!(record >= 1) || record > recs.length) return;
if (!(record >= 1) || record > recs.length) return false;
found = true;
const dec = efDecArr(efBytes(recs[record - 1] || ''));
for (const rule of ((dec && dec.rules) || [])) {
out.push((label ? label + ': ' : '') + rule);
}
return true;
};
if (ref.implicit) {
add('', df.startsWith('ADF.') ? '6F06' : '2F06', ref.record);
// the implicitly known EF.ARR of the file's DF, walking up
for (const df of chain) {
const fid = df === 'MF' ? '2F06' : '6F06';
if (add('', fid, ref.record, pickAt(df, fid))) break;
}
} else if (ref.pairs) {
for (const p of ref.pairs) add(fcpSeidName(p.seid), ref.fid, p.record);
} else {
+37
View File
@@ -1021,6 +1021,43 @@ test('ignoreFciRef compares the FCI without the EF.ARR reference', async () => {
assert.strictEqual(strict.checks.find(c => c.label === 'fci').ok, false);
});
test('a 6F06 reference resolves to the file DF\'s own ARR copy', () => {
global.t = s => s;
// 8B 03 6F06 01: record 1 of the DF-tree copy
const hex = '621A82054221000F0583026F4F8A01058B036F06018802004B8801B0';
const dfCopy = { fid: '6F06', path: 'MF/7F20/6F06', records: ['8001019000'] };
const adfCopy = { fid: '6F06', path: 'ADF.USIM/6F06', records: ['800101A40683010A950108'] };
// the file's own DF copy wins (ISO 7816-4 5.4.3.3 walk-up)
const df = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'MF/7F20/6F07' });
assert.ok(df.includes(': always'), df);
assert.ok(!df.includes('ADM1'), df);
// an ADF.USIM file uses the ADF's copy
const adf = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'ADF.USIM/6F07' });
assert.ok(adf.includes(': ADM1'), adf);
// two candidates outside the file's chain stay unresolved (silent)
const none = profilerFciPreviewItems(hex, { arr: [dfCopy, adfCopy], path: 'MF/7F10/6F07' });
assert.ok(!none.includes('ADM1') && !none.includes(': always'), none);
// a single candidate outside the chain still resolves
const uniq = profilerFciPreviewItems(hex, { arr: [adfCopy], path: 'MF/7F10/6F07' });
assert.ok(uniq.includes(': ADM1'), uniq);
delete global.t;
});
test('the implicit form prefers the file DF\'s own ARR copy', () => {
global.t = s => s;
// 8B 01 01: implicit EF_ARR, record 1
const hex = '621882054221000F0583026F4F8A01058B01018002004B8801B0';
const dfCopy = { fid: '6F06', path: 'MF/7F20/6F06', records: ['8001019000'] };
const mfCopy = { fid: '2F06', path: 'MF/2F06', records: ['800101A40683010A950108'] };
// the DF's own copy wins
const df = profilerFciPreviewItems(hex, { arr: [dfCopy, mfCopy], path: 'MF/7F20/6F07' });
assert.ok(df.includes(': always'), df);
// without it the walk-up reaches the MF copy
const up = profilerFciPreviewItems(hex, { arr: [mfCopy], path: 'MF/7F20/6F07' });
assert.ok(up.includes(': ADM1'), up);
delete global.t;
});
test('profilerResultAspects and the summary include Access rules', () => {
const res = { checks: [
{ label: 'exists', ok: true },