v3.19.2: the three-status-words disambiguation (envelope SW vs por.sw)

The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).

- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
  status / R-APDU SW)' table in the authoring section - where each one
  comes from, the script field and the report row - plus a cross-reference
  from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
  detail 'the response is application data - an applet's own TAR has no
  R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
  responses instead of the misleading R-APDU SW=(none) data=...; the
  compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
This commit is contained in:
2026-10-04 22:19:58 +03:00
parent 14c43f37b3
commit 78bc91d297
9 changed files with 88 additions and 22 deletions
+23 -1
View File
@@ -34,6 +34,26 @@ and the card's preset):
| PoR transport + expected PoR | `scp80.check.por`/`check.data` (inline) or `expect SEND SHORT MESSAGE` + `por` check | pick per the SPI2 the applet expects |
| Delivery timing | a `status` action when the card delivers on poll | the runner never polls by itself |
### The three status words (envelope SW · PoR status · R-APDU SW)
An SCP80 step carries three different "SW" values; keep them apart when
writing checks:
| Where it comes from | What it means | Script field | Report row |
|---|---|---|---|
| the **ENVELOPE** exchange | the card's answer to the command that was sent (`9000` executed, `91XX` a proactive command is pending, `62XX`/`63XX` a warning) | the action's `check.sw` | `SW` |
| the **PoR status** | the SCP80 security-processing verdict inside the response packet (`por_ok`, `cntr_low`, `rc_cc_ds_failed`, `tar_unknown`, …) | `por.status` (or `"por": "ok"`) | `PoR status` |
| the **R-APDU SW** | the response payload's own status word - the last executed command's / GET RESPONSE's status in the compact form (TS 102 226 §5.1.2 Table 5.1), or the last R-APDU's in the scripting (`AB`/`AF`) form | `por.sw` | `PoR SW` |
`por.sw` is a child of the **response payload**, not of the envelope: it
exists only when the addressed TAR is a **remote-management application**
(ISD / RFM). A third-party applet's **own TAR** answers with
application-defined bytes - the platform passes them through unchanged - so
there is no R-APDU SW to check: `por.sw` reports `(none)` and the whole
secured data is exposed as `por.data`. Assert such a response with `data`
and leave `sw` empty; the step's `check.sw` still asserts the envelope
exchange.
### Worksheet 1 - RFM update → file change → REFRESH (full FCN)
```json
@@ -283,7 +303,9 @@ command-details byte 3 (one byte, exact/mask).
SHORT MESSAGE is decoded with that step's keyset, SPI and counter. `status`
is the decoded response status name (`por_ok`, `cntr_low`,
`rc_cc_ds_failed`, …; `ok` is accepted for `por_ok`); `sw` is the R-APDU
status word and `data` the R-APDU response data (exact/mask) - both come from
status word **inside the response payload** - not the envelope's, see
*The three status words* in the authoring section - and `data` the R-APDU
response data (exact/mask); both come from
the decoded response (scripting `AB`/`AF`/compact forms). An applet's **own
TAR** answers with its application-defined bytes: when the data cannot be the
compact remote response (its command count exceeds the command script that
+1 -1
View File
@@ -564,7 +564,7 @@
<p class="text-sm mb-2"><strong>Тест-скрипт</strong> &mdash; упорядоченный список <strong>действий</strong> и <strong>ожиданий</strong> проактивных команд, выполняемый на подключённой карте на стороне сервера. Пока запуск активен, карта принадлежит скрипту: остальные операции с картой отклоняются (HTTP&nbsp;409), фоновый опрос STATUS приостанавливается; отчёт о запуске хранится в памяти (в том числе после перезагрузки страницы) до нажатия <strong>Очистить отчёт</strong>. Скрипты хранятся в <strong>хранилище на сервере</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), поэтому все браузеры работают с одним набором; изменения записываются в хранилище по мере правки (кнопки &laquo;Сохранить&raquo; нет &mdash; черновик без валидного шага остаётся локальным); экспорт/импорт JSON остаётся для резервных копий и переноса скриптов между стендами (старый экспорт из localStorage импортируется как есть).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Действия</strong> &mdash; <em>Envelope</em> (Event Download: сырой тип события + данные, с опциональным переопределением источника &mdash; 82 терминал / 83 сеть) или <em>Event</em> (семантические формы: Location status, Access technology change, Network rejection, Data connection status change &mdash; поля редактируются как объект JSON в редакторе шагов, а для остальных событий форма события на вкладке «Телефон» показывает готовый hex), <em>Menu Selection</em> (item id 1&ndash;255 или текст пункта с точным/частичным совпадением, по умолчанию без учёта регистра &mdash; выбор <em>«Учёт регистра»</em> в редакторе шагов включает точное совпадение; текст подбирается по меню карты, которое следует за параметрами установки апплета), <em>UPDATE/READ файла</em> (путь + режим; чтение проверяет данные файла), <em>сырой APDU</em> (без авто-обработчика), <em>SCP80</em> (C-APDU или готовый защищённый пакет, опциональные переопределения номера keyset (kvn) / TAR / SPI1 / SPI2 и формат команды &mdash; компактный или обёрнутый в шаблон Command Scripting TS 102 226 &sect;5.2.1 <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em>; ключи и счётчик берутся из пресета карты, подобранного по ICCID подключённой карты &mdash; запуск называет пресет в хранилище, поэтому всегда используются его keysets, TAR и счётчик &mdash; а счётчик записывается обратно после запуска) и <em>STATUS</em> (при <em>попытках</em> &gt; 1 опрашивает, пока карта не объявит команду). У каждого действия есть проверка ответа: ожидаемые SW и данные (точно или маска с подстановкой <code class="font-mono text-sm">?</code> &mdash; <code class="font-mono text-sm">91??</code>) и проверка PoR для SCP80 (<em>любой / без PoR / por_ok</em> или своя проверка декодированного PoR &mdash; статус / SW / данные; транспорт через SEND SHORT MESSAGE работает так же).</li>
<li><strong>Ожидания</strong> &mdash; карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); <code class="font-mono text-sm">9000</code> означает &laquo;команды нет&raquo; и считается ошибкой &mdash; добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные &mdash; для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80) и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
<li><strong>Ожидания</strong> &mdash; карта должна объявить проактивную команду на предыдущем шаге (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); <code class="font-mono text-sm">9000</code> означает &laquo;команды нет&raquo; и считается ошибкой &mdash; добавьте действие STATUS, если апплет выдаёт команду при опросе. Проверяются тип команды, квалификатор и содержимое: текст / пункт / raw, проверка <em>Alpha</em> (идентификатор Alpha команды, тег <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; отличается от текстовой строки <code class="font-mono text-sm">8D</code>), проверка <em>SMS TPDU</em> (submit-TPDU команды SEND SHORT MESSAGE: получатель / PID / DCS / UDL / данные &mdash; для собственного SMS апплета), проверка <em>PoR</em> (SEND SHORT MESSAGE несёт PoR, когда пакет запросил PoR-in-submit, бит SPI2 <code class="font-mono text-sm">20</code>: статус ответа, SW R-APDU и данные ответа; перед этим должен выполниться шаг SCP80 &mdash; SW R-APDU находится <em>внутри</em> полезной нагрузки ответа: собственный TAR апплета возвращает данные приложения, поэтому проверяйте данные, а SW оставляйте пустым) и проверка <em>Files</em> (File List команды REFRESH, например квалификатор <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). <strong>TERMINAL RESPONSE</strong> (результат, идентификатор пункта, текст, доп. TLV) является частью инструкции.</li>
<li><strong>Уровни ошибок</strong> &mdash; несовпадение помечает шаг как <em>Ошибка</em> (скрипт останавливается) или <em>Предупреждение</em> (продолжает); каждая проверка показывается с ожидаемым и фактическим значением.</li>
<li><strong>Шаблоны</strong> &mdash; Empty, <em>Обзор меню STK</em>, <em>Апплет через SCP80</em>; редактор шагов — формный (поля действия Event вводятся объектом JSON), отдельные скрипты экспортируются/импортируются как JSON.</li>
</ul>
+1 -1
View File
@@ -563,7 +563,7 @@
<p class="text-sm mb-2">A <strong>test script</strong> is an ordered list of <strong>actions</strong> and proactive-command <strong>expectations</strong> executed server-side on the equipped card. While a run is active the card belongs to the script: other card actions are refused (HTTP&nbsp;409) and background STATUS polling is paused; the run report is kept in memory (also after reloading the page) until <strong>Clear report</strong>. Scripts live in the <strong>server-side store</strong> (<code class="font-mono text-sm">~/.pysim-simple-server/test_scripts.json</code>, <code class="font-mono text-sm">--test-scripts</code>), so every browser manages the same set; edits are written to the store as you change them (there is no Save button &mdash; a draft without a valid step stays local until it is valid); JSON export/import remains for backup and for moving scripts between benches (the old localStorage export imports as-is).</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Actions</strong> &mdash; <em>Envelope</em> (Event Download: raw event type + data, with an optional source override &mdash; 82 terminal / 83 network) or <em>Event</em> (the semantic forms: Location status, Access technology change, Network rejection, Data connection status change &mdash; the fields are edited as a JSON object in the step editor, and the Phone tab's event form shows the built hex for the other events), <em>Menu Selection</em> (item id 1&ndash;255, or the item's text with an exact/contains match, case-insensitive by default &mdash; the step editor's <em>Match case</em> choice switches to an exact-case match; the text is resolved against the card's menu, which follows the applet's install parameters), <em>UPDATE/READ file</em> (path + mode; reads verify the file data), <em>raw APDU</em> (no auto-handler), <em>SCP80</em> (C-APDU or a pre-built secured packet, optional keyset number (kvn) / TAR / SPI1 / SPI2 overrides and a command format &mdash; compact, or the TS 102 226 &sect;5.2.1 Command Scripting template <em>expanded</em> (AA) / <em>expanded AE&nbsp;80</em> around the C-APDU; the keys and the counter come from the card preset matched by the equipped card's ICCID &mdash; the run names the stored preset, so it always uses its keysets, TARs and counter &mdash; and the counter is written back to it after the run) and <em>STATUS</em> (with <em>attempts</em> &gt; 1 it polls until the card announces a command). Every action has a response check: expected SW and data (exact, or a mask with <code class="font-mono text-sm">?</code> nibble wildcards &mdash; <code class="font-mono text-sm">91??</code>) plus a PoR check for SCP80 (<em>any / no PoR / por_ok</em>, or a custom assertion of the decoded PoR &mdash; status / SW / data; the SEND SHORT MESSAGE transport works the same).</li>
<li><strong>Expectations</strong> &mdash; the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); a <code class="font-mono text-sm">9000</code> means &ldquo;no command&rdquo; and is an error &mdash; add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data &mdash; for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
<li><strong>Expectations</strong> &mdash; the card must have announced a proactive command in the previous step (SW <code class="font-mono text-sm">91XX</code>, TS 102 221 &sect;7.4.2.1 / TS 102 223 &sect;6.3); a <code class="font-mono text-sm">9000</code> means &ldquo;no command&rdquo; and is an error &mdash; add a STATUS action when the applet delivers on poll. The command type, qualifier and contents are verified: text / item / raw checks, an <em>Alpha</em> check (the command's Alpha identifier, tag <code class="font-mono text-sm">05</code>/<code class="font-mono text-sm">85</code> &mdash; distinct from the <code class="font-mono text-sm">8D</code> text string), an <em>SMS TPDU</em> check (the submit TPDU of a SEND SHORT MESSAGE: destination / PID / DCS / UDL / user data &mdash; for an applet's own SMS), a <em>PoR</em> check (a SEND SHORT MESSAGE carries the PoR when the packet requested PoR-in-submit, SPI2 bit <code class="font-mono text-sm">20</code>: the response status, the R-APDU SW and the response data; make sure an SCP80 step ran first &mdash; the R-APDU SW lives <em>inside</em> the response payload: an applet's own TAR returns application data, so check its data and leave the SW empty) and a <em>Files</em> check (the File List of a REFRESH, e.g. qualifier <code class="font-mono text-sm">00</code> = NAA initialization with full file change notification). The scripted <strong>TERMINAL RESPONSE</strong> (result, item identifier, text, extra TLVs) is part of the instruction.</li>
<li><strong>Fail levels</strong> &mdash; a mismatch flags the step as <em>Error</em> (terminates the script) or <em>Warning</em> (continues); each check is reported with expected vs actual values.</li>
<li><strong>Templates</strong> &mdash; Empty, <em>STK menu browsing</em>, <em>Applet via SCP80</em>; the step editor is form-based (the event action's fields are a JSON object) and individual scripts export/import as JSON.</li>
</ul>
+4 -1
View File
@@ -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.19.1';
const SIMPLE_VERSION = '3.19.2';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -7442,6 +7442,8 @@ function testStepRender() {
html += testFormRow('PoR status', testFormInput('test-f-porstatus', porObj.status, 'por_ok'));
html += testFormRow('PoR SW (hex)', testFormInput('test-f-por-sw', porObj.sw && porObj.sw.value, '6A88'));
html += testFormRow('PoR data (hex)', testFormInput('test-f-por-data', porObj.data && porObj.data.value, 'AA??'));
html += '<p class="mt-1 text-xs text-gray-400 dark:text-slate-500">'
+ esc(t("PoR SW is the R-APDU SW inside a remote-management TAR's response; an applet's own TAR returns application data - assert it with PoR data and leave SW empty.")) + '</p>';
}
}
html += testFormRow('On mismatch', testFormSelect('test-f-fail', step.on_fail || 'error', [
@@ -21194,6 +21196,7 @@ const LANG_RU = {
'PoR status': 'Статус PoR',
'PoR SW (hex)': 'SW PoR (hex)',
'PoR data (hex)': 'Данные PoR (hex)',
"PoR SW is the R-APDU SW inside a remote-management TAR's response; an applet's own TAR returns application data - assert it with PoR data and leave SW empty.": 'SW PoR — это SW R-APDU внутри ответа remote-management TAR; собственный TAR апплета возвращает данные приложения — проверяйте «Данные PoR», оставив SW пустым.',
'PoR check: status, SW or data is required': 'Проверка PoR: нужен status, SW или data',
'Command format': 'Формат команды',
'Compact': 'Компактный',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v390';
const CACHE = 'simple-v391';
const URLS = [
'index.html',
'help.html',
+3
View File
@@ -250,6 +250,9 @@ test('the SCP80 step form renders the format select and the custom PoR fields',
assert.match(body, /<option value="custom" selected>/);
assert.match(body, /id="test-f-porstatus" value="por_ok"/);
assert.match(body, /id="test-f-por-sw" value=""/);
// the block explains where PoR SW comes from (the applet-TAR confusion)
assert.ok(body.includes('R-APDU SW inside a remote-management'), body);
assert.ok(body.includes('leave SW empty'), body);
// the pre-built packet source has no format select (the wrapper applies
// to a C-APDU only)
_testEditStep = { type: 'action', kind: 'scp80', params: { source: 'sp', sp: 'AABB' } };
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.19.1"
version = "3.19.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.
+23 -10
View File
@@ -34,7 +34,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.19.1'
VERSION = '3.19.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5839,9 +5839,14 @@ def _por_check_results(spec, por, level, prefix='PoR'):
actual or '(none)', level))
if 'sw' in spec:
actual = str((por.get('decoded') or {}).get('last_status_word') or '')
checks.append(_test_check_result(
prefix + ' SW', testscript.match_value(spec['sw'], actual),
spec['sw']['value'], actual or '(none)', level))
ok = testscript.match_value(spec['sw'], actual)
detail = None
if not ok and por.get('response_type') == 'raw':
detail = ("the response is application data - an applet's own TAR "
"has no R-APDU SW; assert it with 'data'")
checks.append(_test_check_result(prefix + ' SW', ok,
spec['sw']['value'], actual or '(none)',
level, detail=detail))
if 'data' in spec:
actual = str((por.get('decoded') or {}).get('last_response_data') or '')
checks.append(_test_check_result(
@@ -6024,9 +6029,13 @@ def _test_run_scp80(server, step, ctx):
_test_log('PoR[inline] status=%s TAR=%s CNTR=%s%s raw=%s'
% (por.get('response_status'), por.get('tar'),
por.get('cntr'), extra, por.get('raw') or data))
_test_log('R-APDU SW=%s data=%s'
% (dec.get('last_status_word') or '(none)',
dec.get('last_response_data') or '(none)'))
if por.get('response_type') == 'raw':
_test_log('APP DATA=%s (no R-APDU SW)'
% (dec.get('last_response_data') or '(none)'))
else:
_test_log('R-APDU SW=%s data=%s'
% (dec.get('last_status_word') or '(none)',
dec.get('last_response_data') or '(none)'))
elif data:
_test_log('PoR[inline] undecodable raw=%s' % data)
else:
@@ -6284,9 +6293,13 @@ def _test_run_expect(server, step, pending, ctx=None):
_test_log('PoR[sms-submit] status=%s TAR=%s CNTR=%s raw=%s'
% (por.get('response_status'), por.get('tar'),
por.get('cntr'), por.get('raw') or '(none)'))
_test_log('R-APDU SW=%s data=%s'
% (dec.get('last_status_word') or '(none)',
dec.get('last_response_data') or '(none)'))
if por.get('response_type') == 'raw':
_test_log('APP DATA=%s (no R-APDU SW)'
% (dec.get('last_response_data') or '(none)'))
else:
_test_log('R-APDU SW=%s data=%s'
% (dec.get('last_status_word') or '(none)',
dec.get('last_response_data') or '(none)'))
else:
_test_log('PoR[sms-submit] none (no decodable PDU or no scp80 '
'context)')
+31 -6
View File
@@ -1106,6 +1106,31 @@ class TestRunnerActions(RunnerTestCase):
# the live alfa-dsa applet's response (2026-10-04): the secured data
# `80 01 10` is the applet's own, so the compact parse must not claim
# 128 commands - the runner exposes it as the PoR data
preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16,
'kidKey': 'BB' * 16, 'counter': '00000001',
'tars': preset_tars()}
buf = io.StringIO()
with mock.patch.object(S, '_build_secured_packet', return_value=('AA' * 10, {})), \
mock.patch.object(S, '_send_secured_packet',
return_value={'success': True, 'sw': '9000',
'response_data': self.APP_POR_RAW}):
with contextlib.redirect_stderr(buf):
run = self.run_script(FakeServer(FakeScc()), [
{'type': 'action', 'kind': 'scp80', 'params': {'apdu': '01'},
'check': {'por': {'status': 'por_ok', 'data': '800110'}}},
], preset)
self.assertEqual(run['status'], 'ok', run['steps'])
labels = {c['label']: c for c in run['steps'][0]['checks']}
self.assertTrue(labels['PoR status']['ok'], labels)
self.assertTrue(labels['PoR data']['ok'], labels)
self.assertEqual(run['steps'][0]['por']['response_type'], 'raw')
# the log names application data instead of inventing an R-APDU line
self.assertIn('APP DATA=800110 (no R-APDU SW)', buf.getvalue())
self.assertNotIn('R-APDU SW=', buf.getvalue())
def test_scp80_por_sw_check_on_applet_data_reports_the_hint(self):
# a `por.sw` expectation cannot be satisfied by an applet's own TAR -
# the report explains why on the check row (the live confusion)
preset = {'kic': '15', 'kid': '15', 'kicKey': 'AA' * 16,
'kidKey': 'BB' * 16, 'counter': '00000001',
'tars': preset_tars()}
@@ -1115,13 +1140,13 @@ class TestRunnerActions(RunnerTestCase):
'response_data': self.APP_POR_RAW}):
run = self.run_script(FakeServer(FakeScc()), [
{'type': 'action', 'kind': 'scp80', 'params': {'apdu': '01'},
'check': {'por': {'status': 'por_ok', 'data': '800110'}}},
'check': {'por': {'sw': '9000'}}},
], preset)
self.assertEqual(run['status'], 'ok', run['steps'])
labels = {c['label']: c for c in run['steps'][0]['checks']}
self.assertTrue(labels['PoR status']['ok'], labels)
self.assertTrue(labels['PoR data']['ok'], labels)
self.assertEqual(run['steps'][0]['por']['response_type'], 'raw')
self.assertNotEqual(run['status'], 'ok')
sw_check = [c for c in run['steps'][0]['checks'] if c['label'] == 'PoR SW'][0]
self.assertFalse(sw_check['ok'])
self.assertIn('application data', sw_check.get('detail') or '')
self.assertIn("assert it with 'data'", sw_check.get('detail') or '')
def test_scp80_format_wraps_the_apdu(self):
# `format` wraps the C-APDU in the expanded Command Scripting template