Expanded listings follow the command format; registry gets a format fallback
The TCA loader's Explore traces settle the format question: it sends the same P2=00 listing queries wrapped and unchained (`AA0A2208 80F24000024F0000`, pages `...01024F0000`) - no GET RESPONSE anywhere - while the live card needs the chain and answers 6700 to the wrapped unchained form; and the live ISD refuses the compact P2=02 registry query (6700) while serving it wrapped (`AA11220F 80F2400209 4F005C054F9F70C5CC00` -> 6310 + entries with CE versions). Both the listing and the registry queries now follow the operation's command format and fall back across formats. - ramFormatIsExpanded(fmt) + ramFmtLadder(runFmt, working) (pure): the run's format first, then expanded, expanded-ae and compact as the last resort; a format that served data is tried first and never repeated. - ramGetStatusApdu(p1, p2, fmt): the chain exists in compact only (TS 102 226 5.2.1.1) - an expanded run sends the bare query. - paginate: the P2=00 listing follows the run's format and pins it per send, so a GET RESPONSE can never ride inside an expanded packet. - expandedPass: each registry form goes through the format ladder; the chained 61xx retry is compact-only (it was guarded for 'expanded' but not 'expanded-ae'); the serving format is remembered for the remaining listings and named in the report ([AA]/[AE 80]); ramExpandedGroups carries it. - Tests: the helper + ladder vectors, the unchained expanded listing builder (both codings), the fallback/chain source guards, the report marker. - Help EN/RU, the repo AGENTS.md; sw cache simple-v353.
This commit is contained in:
@@ -314,7 +314,7 @@
|
||||
<p class="text-sm mb-3">Кнопка <strong>Проверить в pySim</strong> сверяет собранный пакет с эталонной реализацией <code class="font-mono text-sm">OtaDialectSms.encode_cmd</code>. Кнопка <strong>Отправить на карту</strong> доставляет пакет через ENVELOPE SMS-PP-DOWNLOAD (при подключении к серверу). 2-байтовый префикс CPL передаётся, когда пакет зашифрован или несёт RC/CC/DS (входит в их входные данные), а также при конкатенации SMS; одиночное незащищённое SMS сохраняет форму pySim, начинающуюся с CHL (TS 31.115 Table 1 NOTE). Под полем пакета показывается его размер и число SMS; пакет, не помещающийся в одно SMS, отправляется <strong>конкатенированной</strong> загрузкой (TS 31.115 §4.3) — пакет делится на части SMS-пользовательских данных (первое SMS 132 октета, последующие по 134; первое дополнительно несёт информационные элементы конкатенации и CPI), и сегменты отправляются по порядку. Более 5 сегментов отклоняется: это практический предел буфера конкатенации карты. Полученный Proof of Receipt декодируется и показывается строкой статуса PoR (статус, TAR, счётчик, сырой PoR); статусное слово и данные ответа последней команды подставляются в подвкладку <strong>«Разбор R-APDU»</strong> (Remote APDU → «Разбор»), а успешный PoR увеличивает счётчик повторов (когда в SPI1 есть проверка счётчика — b5 b4 ≠ 00; без неё счётчик не увеличивается и не сохраняется) и очищает пакет.</p>
|
||||
|
||||
<h3 id="ram" class="text-lg font-medium mb-2">3.2 RAM</h3>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> пресета, по умолчанию <code class="font-mono text-sm">000000</code>). Селектор <strong>Keyset</strong> рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор <strong>Формат</strong> фиксирует формат удалённой команды: <strong>Авто (определение)</strong> опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; <strong>Компактный</strong> отправляет голую C-APDU; <strong>Expanded (AA, определённая длина)</strong> / <strong>Expanded (AE 80, неопределённая длина)</strong> заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага).</p>
|
||||
<p class="mb-2">Выполняет операции удалённого управления приложениями (Remote Application Management) как защищённые пакеты SCP80 через SMS-PP-DOWNLOAD ENVELOPE. Карта должна поддерживать SCP03 (AES или 3DES). Пресет карты со вкладки <strong>Карты</strong> обеспечивает SPI, ключи, TAR и счётчик (RAM использует <strong>ISD TAR</strong> пресета, по умолчанию <code class="font-mono text-sm">000000</code>). Селектор <strong>Keyset</strong> рядом с выбором пресета указывает, из какого keyset берутся ключи и счётчик операции. Селектор недоступен, пока не выбран пресет карты. Кнопка запуска недоступна, пока не выбран пресет карты и один из его keysets. Селектор <strong>Формат</strong> фиксирует формат удалённой команды: <strong>Авто (определение)</strong> опрашивает read-only команду GET STATUS в компактном виде и в обоих expanded-кодированиях и оставляет первое, которое карта выполнила; <strong>Компактный</strong> отправляет голую C-APDU; <strong>Expanded (AA, определённая длина)</strong> / <strong>Expanded (AE 80, неопределённая длина)</strong> заворачивают её в шаблон Command Scripting из TS 102 226 §5.2.1 (на живой карте выполняются обе; шаблон, у которого заявленная длина не совпадает с телом, получает Bad format TLV — операция показывает это как ошибку шага). Запросы листингов и реестра следуют этому формату: компактные листинги несут сцепленный GET RESPONSE, а expanded-прогон отправляет те же запросы P2=00 обёрнутыми и без цепочки (форма эталонного загрузчика TCA; Le=’00’ возвращает листинг сразу), и запрос реестра, отклонённый картой в текущем формате, повторяется в остальных — отчёт называет тот, который сработал.</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Операции</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+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 SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default). 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).</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 SPI, keys, TAR, and counter (RAM uses the preset's <strong>ISD TAR</strong>, <code class="font-mono text-sm">000000</code> by default). 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.</p>
|
||||
|
||||
<h4 id="ram-operations" class="font-medium mb-1">Operations</h4>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
|
||||
+147
-78
@@ -9341,7 +9341,7 @@ function ramExpandedGroups(report) {
|
||||
let g = groups.find(x => x.label === r.label);
|
||||
if (!g) {
|
||||
g = { p1: r.p1 || '', label: r.label, form: null, entries: 0, score: 0,
|
||||
sw: '', compact: false, refused: false };
|
||||
sw: '', compact: false, refused: false, fmt: null };
|
||||
groups.push(g);
|
||||
}
|
||||
g.compact = g.compact || !!r.compact;
|
||||
@@ -9354,6 +9354,7 @@ function ramExpandedGroups(report) {
|
||||
g.form = r.form;
|
||||
g.entries = r.entries || 0;
|
||||
g.score = score;
|
||||
g.fmt = r.fmt || null;
|
||||
}
|
||||
} else {
|
||||
g.refused = true;
|
||||
@@ -9383,10 +9384,15 @@ function ramExpandedReport(report) {
|
||||
if (!groups.length) return '';
|
||||
const formName = f => f === 'tags' ? t('tag list + EA')
|
||||
: (f === 'noea' ? t('tag list') : (f === 'lean' ? t('lean tag list') : t('no tag list')));
|
||||
// The serving command format is named when it is one of the expanded
|
||||
// codings: some cards refuse the compact P2=02 query and serve it only
|
||||
// inside the TS 102 226 scripting template (5.2.1)
|
||||
const fmtName = f => f === 'expanded' ? 'AA' : (f === 'expanded-ae' ? 'AE 80' : '');
|
||||
const parts = [];
|
||||
RAM_EXPANDED_FORMS.forEach(f => {
|
||||
const labels = groups.filter(g => g.form === f)
|
||||
.map(g => g.label + (g.more ? ' (' + t('first page only') + ')' : ''));
|
||||
.map(g => g.label + (g.more ? ' (' + t('first page only') + ')' : '') +
|
||||
(ramFormatIsExpanded(g.fmt) ? ' [' + fmtName(g.fmt) + ']' : ''));
|
||||
if (labels.length) parts.push(formName(f) + ' (' + labels.join(', ') + ')');
|
||||
});
|
||||
groups.forEach(g => {
|
||||
@@ -9599,9 +9605,12 @@ async function ramExplore(sp) {
|
||||
const mem = { appCount: null, freeNV: null, freeV: null };
|
||||
const isd = [], apps = [], elfs = [];
|
||||
// Expanded-registry state for this run: the query form the card accepted
|
||||
// (remembered and tried first for the later listings) and the per-listing
|
||||
// outcome report shown in the explorer.
|
||||
// (remembered and tried first for the later listings), the command format
|
||||
// the registry queries needed (null = the run's format; the live ISD
|
||||
// answers the compact P2=02 query with 6700 and serves it wrapped) and the
|
||||
// per-listing outcome report shown in the explorer.
|
||||
let expandedForm = null;
|
||||
let expandedFmt = null;
|
||||
const expandedReport = [];
|
||||
// Low counter: the card rejected a packet whose counter is not above its
|
||||
// own - a retry cannot help, so the run stops immediately and the user is
|
||||
@@ -9618,87 +9627,111 @@ async function ramExplore(sp) {
|
||||
// the remembered form first, then the others. The ISD gets a single
|
||||
// attempt - cards commonly answer 6A88/6A86 there ("no data to report",
|
||||
// GPPro), which must not poison the detection for the apps/ELF queries.
|
||||
// A '61xx' answer means the card holds the response data (fetched with
|
||||
// GET RESPONSE); a '6310' means more occurrences (re-issued as a
|
||||
// next-occurrence page, P2.b1=1) - both are chained here, so an answer is
|
||||
// only called empty or refused when it really is. Returns
|
||||
// {form, parsed, sw, entries}; form is null when no form produced data.
|
||||
// Each form is tried in the run's command format first, then in the two
|
||||
// expanded codings and compact as the last resort: the live ISD refuses
|
||||
// the compact P2=02 query with 6700 and serves it wrapped, while other
|
||||
// cards do the opposite - the format that works is remembered for the
|
||||
// remaining listings. A '61xx' answer means the card holds the response
|
||||
// data - only the compact command string may chain a GET RESPONSE, the
|
||||
// expanded format must not (TS 102 226 5.2.1.1); a '6310' means more
|
||||
// occurrences (re-issued as a next-occurrence page, P2.b1=1). Returns
|
||||
// {form, parsed, sw, entries, fmt}; form is null when no form produced
|
||||
// data.
|
||||
async function expandedPass(p1, label, parser, spi2) {
|
||||
let forms = expandedForm
|
||||
? [expandedForm].concat(RAM_EXPANDED_FORMS.filter(f => f !== expandedForm))
|
||||
: RAM_EXPANDED_FORMS.slice();
|
||||
if (p1 === '80') forms = [expandedForm || 'tags'];
|
||||
const runFmt = sp.ram_format || 'auto';
|
||||
let lastSw = '';
|
||||
let lastTransport = false;
|
||||
let stopPass = false;
|
||||
for (const form of forms) {
|
||||
if (cntrLow) break;
|
||||
let data = '';
|
||||
let sw = '';
|
||||
let accepted = true;
|
||||
let transportError = false;
|
||||
const sendOne = async (apdu, what) => {
|
||||
ramShowProgress(label + ' P1=' + p1 + ' ' + what + '...');
|
||||
const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 }));
|
||||
if (res.ram_format) sp.ram_format = res.ram_format;
|
||||
if (res.final_cntr) cntr = res.final_cntr;
|
||||
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
|
||||
if (!res.success || !res.por || !spPorAccepted(res.por)) {
|
||||
accepted = false;
|
||||
// a request/transport failure is not a remote status word
|
||||
transportError = !res.por;
|
||||
sw = res.por ? res.por.response_status : (res.error || t('no data'));
|
||||
return '';
|
||||
}
|
||||
sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase();
|
||||
return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
|
||||
};
|
||||
let page = 0;
|
||||
let promised = false;
|
||||
while (accepted && page++ < 8) {
|
||||
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
|
||||
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')';
|
||||
data += await sendOne(apdu, what);
|
||||
if (cntrLow || !accepted) break;
|
||||
// '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 (cntrLow || stopPass) break;
|
||||
const fmts = ramFmtLadder(runFmt, expandedFmt);
|
||||
for (const fmt of fmts) {
|
||||
if (cntrLow) break;
|
||||
let data = '';
|
||||
let sw = '';
|
||||
let accepted = true;
|
||||
let transportError = false;
|
||||
let usedFmt = fmt || runFmt;
|
||||
const sendOne = async (apdu, what) => {
|
||||
ramShowProgress(label + ' P1=' + p1 + ' ' + what + '...');
|
||||
const params = Object.assign({}, sp, { cntr, spi2 });
|
||||
if (fmt) params.ram_format = fmt;
|
||||
const res = await ramSendOta(apdu, params);
|
||||
usedFmt = res.ram_format || fmt || sp.ram_format || usedFmt;
|
||||
// an explicit fallback format is not adopted as the run's
|
||||
// format - the selected/pinned format keeps applying to the
|
||||
// other passes
|
||||
if (!fmt && res.ram_format) sp.ram_format = res.ram_format;
|
||||
if (res.final_cntr) cntr = res.final_cntr;
|
||||
else if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
|
||||
if (!res.success || !res.por || !spPorAccepted(res.por)) {
|
||||
accepted = false;
|
||||
// a request/transport failure is not a remote status word
|
||||
transportError = !res.por;
|
||||
sw = res.por ? res.por.response_status : (res.error || t('no data'));
|
||||
return '';
|
||||
}
|
||||
sw = ((res.por.decoded && res.por.decoded.last_status_word) || '').toUpperCase();
|
||||
return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
|
||||
};
|
||||
let page = 0;
|
||||
let promised = false;
|
||||
while (accepted && page++ < 8) {
|
||||
const apdu = ramExpandedQueryApdu(p1, form, page > 1);
|
||||
const what = (page > 1 ? 'P2=03 (' : 'P2=02 (') + form +
|
||||
(fmt ? ', ' + fmt : '') + ')';
|
||||
data += await sendOne(apdu, what);
|
||||
if (cntrLow || !accepted) break;
|
||||
// '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 format
|
||||
// has no command string, so there is no retry there.
|
||||
const pending = /^61[0-9A-F]{2}$/.test(sw);
|
||||
if (pending && !ramFormatIsExpanded(usedFmt)) {
|
||||
data += await sendOne(ramChainGetResponse(apdu), what + ' + GET RESPONSE');
|
||||
if (cntrLow || !accepted) 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;
|
||||
// continue only when the answer really says 'more occurrences'
|
||||
// or the chained retry was accepted (a '61xx' page masks the
|
||||
// 6310 signal with the GET RESPONSE's own SW); a refused retry
|
||||
// must not cost another packet (v3.7.0)
|
||||
if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break;
|
||||
promised = true;
|
||||
}
|
||||
// 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;
|
||||
// continue only when the answer really says 'more occurrences'
|
||||
// or the chained retry was accepted (a '61xx' page masks the
|
||||
// 6310 signal with the GET RESPONSE's own SW); a refused retry
|
||||
// must not cost another packet (v3.7.0)
|
||||
if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break;
|
||||
promised = true;
|
||||
}
|
||||
// An unfulfilled 6310 only: a follow-up page that was accepted and
|
||||
// ended the listing fulfils the promise.
|
||||
const more = ramExpandedMore(promised, sw, page);
|
||||
const parsed = data ? parser(data) : [];
|
||||
if (parsed.length) {
|
||||
expandedForm = form;
|
||||
return { form: form, parsed: parsed, sw: sw, entries: parsed.length,
|
||||
transport: transportError, more: more };
|
||||
}
|
||||
lastSw = sw;
|
||||
lastTransport = transportError;
|
||||
if (!accepted) break;
|
||||
if (ramRemoteSwOk(sw)) {
|
||||
// accepted but empty: nothing to gain from another form
|
||||
return { form: form, parsed: [], sw: sw, entries: 0,
|
||||
transport: transportError, more: more };
|
||||
// An unfulfilled 6310 only: a follow-up page that was accepted and
|
||||
// ended the listing fulfils the promise.
|
||||
const more = ramExpandedMore(promised, sw, page);
|
||||
const parsed = data ? parser(data) : [];
|
||||
if (parsed.length) {
|
||||
expandedForm = form;
|
||||
if (fmt) expandedFmt = fmt;
|
||||
return { form: form, parsed: parsed, sw: sw, entries: parsed.length,
|
||||
transport: transportError, more: more, fmt: usedFmt };
|
||||
}
|
||||
lastSw = sw;
|
||||
lastTransport = transportError;
|
||||
if (!accepted) {
|
||||
stopPass = true;
|
||||
break;
|
||||
}
|
||||
if (ramRemoteSwOk(sw)) {
|
||||
// accepted but empty: nothing to gain from another form
|
||||
return { form: form, parsed: [], sw: sw, entries: 0,
|
||||
transport: transportError, more: more, fmt: usedFmt };
|
||||
}
|
||||
// refused: the same form in the next command format
|
||||
}
|
||||
}
|
||||
return { form: null, parsed: [], sw: lastSw, entries: 0,
|
||||
transport: lastTransport, more: false };
|
||||
transport: lastTransport, more: false, fmt: null };
|
||||
}
|
||||
|
||||
async function paginate(p1, collector, parser, label, quiet) {
|
||||
@@ -9712,10 +9745,15 @@ async function ramExplore(sp) {
|
||||
let compactError = null;
|
||||
let p2 = '00';
|
||||
let guard = 0;
|
||||
// The P2=00 listing follows the run's command format: compact with the
|
||||
// chained GET RESPONSE (the live card) or wrapped unchained (the TCA
|
||||
// loader's form). The format is pinned per send, so a chain can never
|
||||
// ride inside an expanded packet.
|
||||
const listingFmt = ramFormatIsExpanded(sp.ram_format) ? sp.ram_format : 'compact';
|
||||
while (guard++ < 32) {
|
||||
const apdu = ramGetStatusApdu(p1, p2);
|
||||
const apdu = ramGetStatusApdu(p1, p2, listingFmt);
|
||||
ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...');
|
||||
const res = await sendOta(apdu, Object.assign({}, sp, { cntr, spi2 }));
|
||||
const res = await sendOta(apdu, Object.assign({}, sp, { cntr, spi2, ram_format: listingFmt }));
|
||||
if (cntrLow) break;
|
||||
// Reuse the format detected for the first packet of this run
|
||||
if (res.ram_format) sp.ram_format = res.ram_format;
|
||||
@@ -9739,7 +9777,9 @@ async function ramExplore(sp) {
|
||||
break;
|
||||
}
|
||||
// Defensive: a remote 61XX means the data is still pending (the
|
||||
// chained GET RESPONSE normally prevents this).
|
||||
// chained GET RESPONSE normally prevents this; the expanded format
|
||||
// has no chain, so such a page yields nothing and the expanded TLV
|
||||
// pass below provides the listing).
|
||||
if (sw && sw.startsWith('61')) {
|
||||
if (data) {
|
||||
const parsed61 = parser(data);
|
||||
@@ -9774,7 +9814,7 @@ async function ramExplore(sp) {
|
||||
const out = await expandedPass(p1, label, parser, spi2);
|
||||
expandedReport.push({ p1: p1, label: label, compact: collector.length > before,
|
||||
form: out.form, sw: out.sw, entries: out.entries,
|
||||
transport: out.transport, more: out.more });
|
||||
transport: out.transport, more: out.more, fmt: out.fmt });
|
||||
if (collector.length === before) {
|
||||
// Nothing from the compact pass: the expanded result is the listing
|
||||
collector.push(...out.parsed);
|
||||
@@ -12067,6 +12107,28 @@ function spCntrLow(res) {
|
||||
// Remote-command status words that count as success (mirrors the server's
|
||||
// `_ram_remote_sw_ok`): 9000 normal; 61xx more data available; 62xx/63xx
|
||||
// warnings; CAFE GlobalPlatform "more data available" (GET STATUS pages).
|
||||
// True for the two expanded command formats (TS 102 226 5.2.1). The expanded
|
||||
// format does not use GET RESPONSE (5.2.1.1): a command may never be chained
|
||||
// there - the reference TCA loader sends the same P2=00 listing queries
|
||||
// wrapped and unchained (Le='00' returns the listing directly), while the
|
||||
// live card serves them compact with the chain. The listing builders follow
|
||||
// the run's command format for exactly that reason.
|
||||
function ramFormatIsExpanded(fmt) {
|
||||
return fmt === 'expanded' || fmt === 'expanded-ae';
|
||||
}
|
||||
|
||||
// The command-format ladder of one expanded-registry form (pure): the run's
|
||||
// format first (null = no override), then the two expanded codings and
|
||||
// compact as the last resort - cards differ in which one they serve (the live
|
||||
// ISD refuses the compact P2=02 query and answers it wrapped). A format that
|
||||
// already produced data (`working`) is tried first and never repeated.
|
||||
function ramFmtLadder(runFmt, working) {
|
||||
const ladder = [null, 'expanded', 'expanded-ae', 'compact'].filter((v, i, a) =>
|
||||
a.indexOf(v) === i && (v === null || v !== runFmt));
|
||||
if (!working) return ladder;
|
||||
return [working].concat(ladder.filter(f => f !== null && f !== working));
|
||||
}
|
||||
|
||||
function ramRemoteSwOk(sw) {
|
||||
const s = (sw || '').toUpperCase();
|
||||
return !!s && (s === '9000' || s === 'CAFE' ||
|
||||
@@ -12079,8 +12141,15 @@ function ramRemoteSwOk(sw) {
|
||||
// payload. The expanded TLV format (P2.b2=1, P2=02/03) must NOT carry a GET
|
||||
// RESPONSE - it returns the E3 templates directly - so this builder is only
|
||||
// for the compact path.
|
||||
function ramGetStatusApdu(p1, p2) {
|
||||
return '80F2' + p1 + p2 + '024F0000' + 'C0000000';
|
||||
function ramGetStatusApdu(p1, p2, fmt) {
|
||||
// The chained GET RESPONSE exists in the compact command format only: the
|
||||
// expanded format must not carry one (TS 102 226 5.2.1.1), and the
|
||||
// reference TCA loader sends the same queries wrapped and unchained
|
||||
// (`AA0A2208 80F24000024F0000`; Le='00' returns the listing directly).
|
||||
// `fmt` is the run's command format; omitted = compact (the default the
|
||||
// live card needs).
|
||||
const query = '80F2' + p1 + p2 + '024F0000';
|
||||
return ramFormatIsExpanded(fmt) ? query : query + 'C0000000';
|
||||
}
|
||||
|
||||
// P2=02 expanded (E3) registry listing (GP Card Spec 11.4.2.3/11.4.3.1).
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v352';
|
||||
const CACHE = 'simple-v353';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -23,6 +23,7 @@ function extractFunc(src, name) {
|
||||
|
||||
// Extract chain builder functions and dependencies
|
||||
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',
|
||||
'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',
|
||||
@@ -375,6 +376,13 @@ test('ramGetStatusApdu builds the compact chain, never the expanded form', () =>
|
||||
// GP 11.4.2.2: compact listings (P2.b2=0) carry the chained GET RESPONSE
|
||||
assert.strictEqual(ramGetStatusApdu('80', '00'), '80F28000024F0000C0000000');
|
||||
assert.strictEqual(ramGetStatusApdu('20', '01'), '80F22001024F0000C0000000');
|
||||
// the expanded command format must not carry a GET RESPONSE (TS 102 226
|
||||
// 5.2.1.1): the reference TCA loader sends the same query wrapped unchained
|
||||
// (`AA0A2208 80F24000024F0000`), Le='00' returns the listing directly
|
||||
assert.strictEqual(ramGetStatusApdu('40', '00', 'expanded'), '80F24000024F0000');
|
||||
assert.strictEqual(ramGetStatusApdu('40', '01', 'expanded-ae'), '80F24001024F0000');
|
||||
assert.ok(!ramGetStatusApdu('40', '00', 'expanded').includes('C0000000'),
|
||||
'no GET RESPONSE in the expanded format');
|
||||
// the expanded TLV form (P2=02/03) takes no GET RESPONSE and is not built:
|
||||
// its GET STATUS bytes would be `80F2<P1>02 04 4F00 5C.. 00`
|
||||
assert.ok(!ramGetStatusApdu('20', '01').startsWith('80F2200204'),
|
||||
@@ -660,6 +668,51 @@ test('the explorer annotates a derived package version', () => {
|
||||
assert.ok(!plain.includes('from the app instance'), plain);
|
||||
});
|
||||
|
||||
test('the expanded formats never chain a GET RESPONSE (TCA loader form)', () => {
|
||||
assert.strictEqual(ramFormatIsExpanded('expanded'), true);
|
||||
assert.strictEqual(ramFormatIsExpanded('expanded-ae'), true);
|
||||
assert.strictEqual(ramFormatIsExpanded('compact'), false);
|
||||
assert.strictEqual(ramFormatIsExpanded('auto'), false);
|
||||
assert.strictEqual(ramFormatIsExpanded(undefined), false);
|
||||
const src = extractFunc(html, 'ramExplore');
|
||||
// the listing pass follows the run's command format and pins it per send,
|
||||
// so a chain can never ride inside an expanded packet
|
||||
assert.ok(src.includes('ramFormatIsExpanded(sp.ram_format)'), 'the listing format must follow the run');
|
||||
assert.ok(src.includes('ramGetStatusApdu(p1, p2, listingFmt)'), 'the builder must follow the format');
|
||||
assert.ok(src.includes('ram_format: listingFmt'), 'the listing format must be pinned per send');
|
||||
// the registry pass: the chained retry is compact-only, the format ladder
|
||||
// covers the expanded codings, and the working format is remembered
|
||||
assert.ok(src.includes('!ramFormatIsExpanded(usedFmt)'), 'the chained retry must be compact-only');
|
||||
assert.ok(src.includes('ramFmtLadder(runFmt, expandedFmt)'),
|
||||
'the format ladder (with its remembered format) must drive the retries');
|
||||
assert.ok(src.includes('expandedFmt = fmt'), 'the working format must be remembered');
|
||||
assert.ok(src.includes('fmt: out.fmt'), 'the report must carry the serving format');
|
||||
});
|
||||
|
||||
test('ramFmtLadder orders the registry command formats cautiously', () => {
|
||||
// the run's format first (null = no override), then the expanded codings,
|
||||
// then compact as the last resort; the run's own format is not repeated
|
||||
assert.deepStrictEqual(ramFmtLadder('compact', null), [null, 'expanded', 'expanded-ae']);
|
||||
assert.deepStrictEqual(ramFmtLadder('expanded', null), [null, 'expanded-ae', 'compact']);
|
||||
assert.deepStrictEqual(ramFmtLadder('expanded-ae', null), [null, 'expanded', 'compact']);
|
||||
assert.deepStrictEqual(ramFmtLadder('auto', null), [null, 'expanded', 'expanded-ae', 'compact']);
|
||||
// a format that already served data is tried first and never repeated
|
||||
assert.deepStrictEqual(ramFmtLadder('compact', 'expanded'), ['expanded', 'expanded-ae']);
|
||||
assert.deepStrictEqual(ramFmtLadder('expanded-ae', 'expanded-ae'), ['expanded-ae', 'expanded', 'compact']);
|
||||
});
|
||||
|
||||
test('ramExpandedReport names the expanded serving format', () => {
|
||||
globalThis.t = s => s;
|
||||
const out = ramExpandedReport([
|
||||
{ p1: '80', label: 'ISD', compact: false, form: 'lean', sw: '6310', entries: 3, fmt: 'expanded' },
|
||||
{ p1: '40', label: 'Apps', compact: false, form: 'lean', sw: '9000', entries: 2, fmt: 'expanded-ae' },
|
||||
]);
|
||||
delete globalThis.t;
|
||||
assert.ok(out.startsWith('Expanded registry:'), out);
|
||||
assert.ok(out.includes('ISD [AA]'), out);
|
||||
assert.ok(out.includes('Apps [AE 80]'), out);
|
||||
});
|
||||
|
||||
test('ramExpandedReport summarizes the query forms and the fallbacks', () => {
|
||||
globalThis.t = s => s;
|
||||
// every listing answered the reference form
|
||||
|
||||
Reference in New Issue
Block a user