feat: chained GET RESPONSE + ELF versions from app instances (v3.6.56)
The live log (2026-09-30) showed our standalone GET RESPONSE was wrong: the card answered 6700 (wrong length) to 00C00000<xx> for the 61xx answers of the expanded ISD query and the ELF notags query. The compact path proves how this card wants it: GET STATUS and GET RESPONSE chained in one compact command string (80F2..C0000000). - a 61xx answer now retries the same query with ramChainGetResponse (+ C0000000, compact format only; the expanded AA/22 form has no command string, so no retry there); - a 61xx page still continues to the next occurrence (the GET RESPONSE's own SW replaces the 6310 signal); the ISD stays a single occurrence; - the standalone 00C00000<xx> attempt is gone (it only ever answered 6700). This card also refuses every tag list for the ELF queries (tags/noea/lean all 6A88), so CE cannot come from the ELF listing. Its apps listing does carry the load file version (CE) together with the package AID (C4), so ramDeriveElfVersions fills a missing ELF version from its applet instance and the row annotates it: 'Version: 9.3 (from the app instance)'. The version hint now shows whenever any ELF row lacks a version. sw cache simple-v329; tests: 550 Python / 711 Node.
This commit is contained in:
+53
-17
@@ -1749,7 +1749,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.6.55';
|
||||
const SIMPLE_VERSION = '3.6.56';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -9238,6 +9238,24 @@ function ramElfVersionHint(elfForm) {
|
||||
return t('Package versions come from the expanded registry query \u2014 this card did not return them.');
|
||||
}
|
||||
|
||||
// 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.
|
||||
function ramDeriveElfVersions(elfs, apps) {
|
||||
(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());
|
||||
if (app) {
|
||||
e.version = app.version;
|
||||
e.versionFrom = 'app';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render the full explorer result as structured HTML
|
||||
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
||||
let html = '';
|
||||
@@ -9301,7 +9319,7 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
||||
if (elfs && elfs.length) {
|
||||
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)) {
|
||||
if (elfs.some(o => !o.version)) {
|
||||
html += '<div class="mb-1 text-xs text-gray-500 dark:text-slate-400">' + esc(ramElfVersionHint(elfForm)) + '</div>';
|
||||
}
|
||||
elfs.forEach(o => {
|
||||
@@ -9309,7 +9327,11 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt, expanded, elfForm) {
|
||||
html += '<div class="flex-1 min-w-0">';
|
||||
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '', 'elf') + '</div>';
|
||||
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) + '</div>';
|
||||
if (o.version) {
|
||||
html += '<div>' + esc(t('Version:')) + ' ' + esc(ramFmtVersion(o.version)) +
|
||||
(o.versionFrom === 'app' ? ' <span class="text-gray-500 dark:text-slate-400">(' + esc(t('from the app instance')) + ')</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
if (o.privileges && o.privileges !== '00') html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
||||
if (o.moduleAids && o.moduleAids.length) {
|
||||
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
|
||||
@@ -9407,15 +9429,24 @@ async function ramExplore(sp) {
|
||||
};
|
||||
let page = 0;
|
||||
while (accepted && page++ < 8) {
|
||||
data += await sendOne(ramExpandedQueryApdu(p1, form, page > 1),
|
||||
page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')');
|
||||
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
|
||||
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')';
|
||||
data += await sendOne(apdu, what);
|
||||
if (!accepted) break;
|
||||
let rounds = 0;
|
||||
while (/^61[0-9A-F]{2}$/.test(sw) && rounds++ < 4) {
|
||||
data += await sendOne(ramGetResponseApdu(sw), 'GET RESPONSE');
|
||||
// '61xx': the card holds the response data - the GET RESPONSE
|
||||
// must ride in the same compact command string (a standalone
|
||||
// one answers 6700 on the live card); the expanded AA/22
|
||||
// format has no command string, so there is no retry there.
|
||||
const pending = /^61[0-9A-F]{2}$/.test(sw);
|
||||
if (pending && sp.ram_format !== 'expanded') {
|
||||
data += await sendOne(ramChainGetResponse(apdu), what + ' + GET RESPONSE');
|
||||
if (!accepted) break;
|
||||
}
|
||||
if (!accepted || sw !== '6310') break;
|
||||
// the ISD is a single occurrence (GP 11.4.2.2); '6310' means
|
||||
// more occurrences, and a '61xx' page may mask that signal
|
||||
// (the GET RESPONSE's own SW replaces it) - try one more page
|
||||
if (p1 === '80') break;
|
||||
if (sw !== '6310' && !pending) break;
|
||||
}
|
||||
const parsed = data ? parser(data) : [];
|
||||
if (parsed.length) {
|
||||
@@ -9541,8 +9572,7 @@ async function ramExplore(sp) {
|
||||
// P1 per GP Card Spec v2.3.1 table 11-33:
|
||||
// 80=ISD, 40=Applications, 20=Executable Load Files (ELFs), 10=ELF+modules
|
||||
await paginate('80', isd, ramParseAppStatus, t('ISD'));
|
||||
await paginate('40', apps, ramParseAppStatus, t('Apps'));
|
||||
// GP 11.4.2.1: '10' returns the ELFs *with* their modules - the superset of
|
||||
await paginate('40', apps, ramParseAppStatus, t('Apps')); // GP 11.4.2.1: '10' returns the ELFs *with* their modules - the superset of
|
||||
// '20' (ELFs only). All four P1 values shall be supported, but cards
|
||||
// deviate: the live card answered 6A86 (incorrect P1-P2) to '20' while
|
||||
// '10' works (2026-09-28). Query '10' first; fall back to '20' (no
|
||||
@@ -9555,6 +9585,10 @@ async function ramExplore(sp) {
|
||||
await paginate('20', elfs, ramParseElfStatus, t('ELFs'), true);
|
||||
if (!elfs.length) errors.push(...elfFailure);
|
||||
}
|
||||
// A package whose own listing carries no version may still be known from
|
||||
// its applet instance (the app entry carries the load file version and the
|
||||
// package AID); the rows are annotated when this inference is used.
|
||||
ramDeriveElfVersions(elfs, apps);
|
||||
// Only write the counter when a packet was actually accepted: a run whose
|
||||
// start value was stale must never lower the preset counter.
|
||||
if (cntr !== sp.cntr) ramSaveCntr(cntr);
|
||||
@@ -11001,12 +11035,13 @@ function ramExpandedQueryApdu(p1, form, next) {
|
||||
return '80F2' + p1 + p2 + hex(tags.length / 2 + 4) + '4F005C' + hex(tags.length / 2) + tags + '00';
|
||||
}
|
||||
|
||||
// GET RESPONSE for a '61xx' remote status word: the card holds xx bytes of
|
||||
// response data (the compact path chains C0000000 for the same situation).
|
||||
function ramGetResponseApdu(sw) {
|
||||
const s = String(sw || '').toUpperCase();
|
||||
const len = /^61[0-9A-F]{2}$/.test(s) ? s.substr(2, 2) : '00';
|
||||
return '00C00000' + len;
|
||||
// A '61xx' remote answer means the card holds the response data. The live
|
||||
// card (2026-09-30) rejects a standalone GET RESPONSE with 6700 - the GET
|
||||
// RESPONSE must ride in the same compact command string as the query, exactly
|
||||
// like the compact path does (`80F2..C0000000`). Only the compact command
|
||||
// string can chain commands; the expanded AA/22 format has no retry.
|
||||
function ramChainGetResponse(apdu) {
|
||||
return apdu + 'C0000000';
|
||||
}
|
||||
|
||||
// The ISD (80) and memory responses fit the ENVELOPE; the Apps (40) and ELF
|
||||
@@ -18730,6 +18765,7 @@ const LANG_RU = {
|
||||
'compact listing only (package versions unavailable)': 'только компактный список (версии пакетов недоступны)',
|
||||
'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': 'от экземпляра апплета',
|
||||
'STK menu entry:': 'Пункт меню STK:',
|
||||
'position': 'позиция',
|
||||
'Other registry data:': 'Прочие данные реестра:',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v328';
|
||||
const CACHE = 'simple-v329';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+43
-10
@@ -25,7 +25,7 @@ function extractFunc(src, name) {
|
||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu',
|
||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries',
|
||||
'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramGetResponseApdu',
|
||||
'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
@@ -581,13 +581,45 @@ test('ramExpandedQueryApdu builds the expanded query forms', () => {
|
||||
assert.deepStrictEqual(RAM_EXPANDED_FORMS, ['tags', 'noea', 'lean', 'notags']);
|
||||
});
|
||||
|
||||
test('ramGetResponseApdu fetches a 61xx response (the announced length)', () => {
|
||||
assert.strictEqual(ramGetResponseApdu('6131'), '00C0000031');
|
||||
assert.strictEqual(ramGetResponseApdu('617F'), '00C000007F');
|
||||
assert.strictEqual(ramGetResponseApdu('613f'), '00C000003F');
|
||||
// not a 61xx: ask for all available data
|
||||
assert.strictEqual(ramGetResponseApdu('9000'), '00C0000000');
|
||||
assert.strictEqual(ramGetResponseApdu(''), '00C0000000');
|
||||
test('ramChainGetResponse chains the GET RESPONSE into the query string', () => {
|
||||
// the live card rejects a standalone GET RESPONSE with 6700; the compact
|
||||
// command string (query + C0000000) is how the compact path fetches too
|
||||
assert.strictEqual(ramChainGetResponse('80F280020C4F005C084F9F70C5C4CCCEEA00'),
|
||||
'80F280020C4F005C084F9F70C5C4CCCEEA00C0000000');
|
||||
assert.strictEqual(ramChainGetResponse('80F21002094F005C054F9F70CE8400'),
|
||||
'80F21002094F005C054F9F70CE8400C0000000');
|
||||
});
|
||||
|
||||
test('ramDeriveElfVersions reads a package version from its applet instance', () => {
|
||||
// the app entry carries CE (the load file version) and C4 (the package
|
||||
// AID), so an ELF whose own listing refused every tag list still gets one
|
||||
const elfs = [
|
||||
{ aid: 'A1130001180001FFFFFFFF89A1003900', lifecycle: '01', privileges: '00' },
|
||||
{ aid: 'F0414C46416001', lifecycle: '01', privileges: '00', version: '0100' },
|
||||
{ aid: 'DEADBEEF', lifecycle: '01', privileges: '00' },
|
||||
];
|
||||
const apps = [
|
||||
{ aid: 'A1130001180001FFFFFFFF89A1003908', version: '0903', elfAid: 'A1130001180001FFFFFFFF89A1003900' },
|
||||
{ aid: 'A0000005591010FFFFFFFF8900001200', version: '0001', elfAid: 'A0000005591010FFFFFFFF8900000E00' },
|
||||
];
|
||||
ramDeriveElfVersions(elfs, apps);
|
||||
assert.strictEqual(elfs[0].version, '0903');
|
||||
assert.strictEqual(elfs[0].versionFrom, 'app');
|
||||
// an own version is never overwritten, and an unmatched package stays bare
|
||||
assert.strictEqual(elfs[1].version, '0100');
|
||||
assert.strictEqual(elfs[1].versionFrom, undefined);
|
||||
assert.strictEqual(elfs[2].version, undefined);
|
||||
});
|
||||
|
||||
test('the explorer annotates a derived package version', () => {
|
||||
globalThis.t = s => s;
|
||||
const out = ramRenderExploreHtml(null, [], [],
|
||||
[{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903', versionFrom: 'app' }]);
|
||||
const plain = ramRenderExploreHtml(null, [], [],
|
||||
[{ aid: 'E1', lifecycle: '01', privileges: '00', version: '0903' }]);
|
||||
delete globalThis.t;
|
||||
assert.ok(out.includes('Version: 9.3 <span class="text-gray-500 dark:text-slate-400">(from the app instance)</span>'), out);
|
||||
assert.ok(!plain.includes('from the app instance'), plain);
|
||||
});
|
||||
|
||||
test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
|
||||
@@ -671,8 +703,9 @@ test('the Explore detects the expanded query form and reports it', () => {
|
||||
// the ISD gets a single attempt: its 6A88 must not poison the detection
|
||||
assert.ok(src.includes("if (p1 === '80') forms = [expandedForm || 'tags'];"),
|
||||
'the ISD must not walk the ladder');
|
||||
// a 61xx answer holds the data (GET RESPONSE) and a 6310 pages
|
||||
assert.ok(src.includes('ramGetResponseApdu(sw)'), '61xx must be answered with GET RESPONSE');
|
||||
// a 61xx answer holds the data (chained GET RESPONSE) and a 6310 pages
|
||||
assert.ok(src.includes('ramChainGetResponse(apdu)'), '61xx must be answered with a chained GET RESPONSE');
|
||||
assert.ok(!src.includes('00C00000'), 'no standalone GET RESPONSE (it answers 6700)');
|
||||
assert.ok(src.includes('ramExpandedQueryApdu(p1, form, page > 1)'), '6310 must page');
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.6.55"
|
||||
version = "3.6.56"
|
||||
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.
|
||||
|
||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.6.55'
|
||||
VERSION = '3.6.56'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user