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.
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
@@ -313,7 +313,7 @@
|
||||
<p class="text-sm mb-3">A “Verify vs pySim” button cross-checks the assembled packet against pySim’s reference <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. 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 <strong>concatenated</strong> 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 <strong>R-APDU parser</strong> 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 <strong>low-counter</strong> verdict (<code class="font-mono text-sm">cntr_low</code>, or a warning ENVELOPE answer that still carries such a PoR) is <em>not</em> 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 <strong>Sync counter</strong> 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 <strong>Go to preset</strong> button for raising the counter by hand; nothing else is written automatically.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">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 <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). The <strong>Keyset</strong> 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 <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> 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 <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) 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 <code class="font-mono text-sm">61xx</code> 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 <code class="font-mono text-sm">AA</code> template — and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> 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 <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, the same mapping as the CAP analysis; shown only when the expanded registry returned the versions).</p>
|
||||
<p class="mb-2">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 <strong>Cards</strong> tab provides the keys, the TAR table and the counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default; the SPI1 is derived from that TAR's MSL). The <strong>Keyset</strong> 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 <strong>Format</strong> selector pins the remote command format: <strong>Auto (detect)</strong> probes the read-only GET STATUS command compact and in both expanded codings and keeps the first that the card executes, <strong>Compact</strong> sends the bare C-APDU, and <strong>Expanded (AA, definite length)</strong> / <strong>Expanded (AE 80, indefinite length)</strong> 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 <strong>next-occurrence page</strong> (<code class="font-mono text-sm">P2=03</code>) 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 <code class="font-mono text-sm">61xx</code> 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 <code class="font-mono text-sm">AA</code> template — and a listing is marked <code class="font-mono text-sm">(first page only)</code> only when the card promised more (<code class="font-mono text-sm">6310</code>) and no format could fetch the follow-up; in <strong>Auto</strong> 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 <strong>ETSI release</strong> (<code class="font-mono text-sm">uicc.toolkit</code> 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.</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+43
-25
@@ -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 += '<div class="mb-4">';
|
||||
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Executable Load Files (ELFs) / Packages')) + ' (' + elfs.length + ')</div>';
|
||||
if (elfs.some(o => !o.version)) {
|
||||
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(ramElfVersionHint(elfForm)) + '</div>';
|
||||
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(ramElfVersionHint(elfForm, elfs, apps)) + '</div>';
|
||||
}
|
||||
elfs.forEach(o => {
|
||||
html += '<div class="mb-1 pl-2 pr-2 py-1 rounded bg-gray-50 dark:bg-slate-800/60 hover:bg-gray-100 dark:hover:bg-slate-700/50 border-l-2 border-purple-400 flex items-start gap-3">';
|
||||
@@ -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': 'карта отклонила пакет',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v371';
|
||||
const CACHE = 'simple-v372';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user