fix: capture the submit PoR on every card + per-operation RAM format check (v3.6.24)
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow showed why only one card works: - The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries (SPI2=0x01) the fetched TPDU was thrown away - the Explore died with "no data" and the RAM install steps silently degraded to an unverified "no_por". The capture is now unconditional (inline ENVELOPE responses and proactive submits both work), and the STATUS-poll fallback is enabled for every card. - `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step - the server-side half of the v3.6.23 counter report. It now advances the full 40 bits. - RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded `AA` > `22` form) is detected per operation and never stored: /api/ram-install and /api/ram-install-app always probe, /api/send-ota probes when the caller passes `ram_format`. The probe is a read-only GET STATUS [ISD] as the first step (compact, then wrapped) and every command of the operation uses the detected format. `_wrap_expanded_apdu` is pinned to the reference bytes `AA0A220880F24000024F0000`; responses carry `ram_format` and `final_cntr` (accepted probes consume counters, so the views use the server's next counter). Applet-directed payloads stay byte-exact; only RAM views (and RAM/GP chain packs) opt in. 504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
This commit is contained in:
+17
-3
@@ -306,6 +306,16 @@ Scripting template (`AB`/`AF`: executed-count TLV `80` + R-APDU TLV `23`) are
|
|||||||
decoded the same way (`response_type: "scripting"`), with the R-APDU's own
|
decoded the same way (`response_type: "scripting"`), with the R-APDU's own
|
||||||
status word and data.
|
status word and data.
|
||||||
|
|
||||||
|
With `ram_format` the server handles the two RAM command formats of
|
||||||
|
TS 102 226 §5.2.1: `"auto"` (the default when the key is present) sends a
|
||||||
|
read-only `GET STATUS [ISD]` probe first — compact, then wrapped as `AA`
|
||||||
|
(Command TLV `22`) — and sends the command in the format the card answered;
|
||||||
|
`"compact"` / `"expanded"` pin it. The response reports the detected
|
||||||
|
`ram_format` and `final_cntr` (the counter to use next — accepted probe
|
||||||
|
packets consume counters). Without the key the command data is sent
|
||||||
|
byte-exact: applet-directed payloads (HTTP OTA triggers, §9 push, expanded
|
||||||
|
scripts) must not be wrapped.
|
||||||
|
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -317,7 +327,8 @@ status word and data.
|
|||||||
"tar": "b00000",
|
"tar": "b00000",
|
||||||
"cntr": "0000000001",
|
"cntr": "0000000001",
|
||||||
"kicKey": "D6FCC023...",
|
"kicKey": "D6FCC023...",
|
||||||
"kidKey": "1B07E7E0..."
|
"kidKey": "1B07E7E0...",
|
||||||
|
"ram_format": "auto"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -325,6 +336,7 @@ status word and data.
|
|||||||
```json
|
```json
|
||||||
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
|
{"success": true, "sw": "9000", "response_data": "027100000e0a...",
|
||||||
"bytes": 36, "segments": 1,
|
"bytes": 36, "segments": 1,
|
||||||
|
"ram_format": "compact", "final_cntr": "0000000002",
|
||||||
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
|
"por": {"response_status": "por_ok", "tar": "B00000", "pcntr": 0,
|
||||||
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
|
"decoded": {"number_of_commands": 1, "last_status_word": "6e00",
|
||||||
"last_response_data": ""}}}
|
"last_response_data": ""}}}
|
||||||
@@ -489,7 +501,7 @@ Clears a finished run report (409 while a run is active).
|
|||||||
|
|
||||||
### `POST /api/ram-install`
|
### `POST /api/ram-install`
|
||||||
|
|
||||||
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
|
Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL[for load] → LOAD ×N → INSTALL[for install (+ make selectable)]) wrapped in SCP80 secured packets. Each step is sent via ENVELOPE; the PoR verdict and the remote command's own status word are both checked and the sequence aborts on the first failure (a non-`por_ok` PoR, a remote SW outside the success set, or an undecodable PoR). The counter advances only for a packet the card accepted (PoR `por_ok`); `final_cntr` is returned on success **and** on failure, so the caller keeps the card's consumed counter (a rejected packet leaves it unchanged). The RAM command format is detected per operation: a read-only `GET STATUS [ISD]` probe (`FORMAT CHECK (compact)`, then `FORMAT CHECK (expanded)`) decides between the compact C-APDU and the expanded `AA`/`22` form (TS 102 226 §5.2.1), and every INSTALL/LOAD step of the chain then uses the detected format — the response carries `ram_format`. Pass `ram_format` (`compact`/`expanded`) to pin it. The `.cap` archive (a ZIP of nested components) is parsed server-side in `_cap_parse`; no external tooling is required.
|
||||||
|
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```json
|
```json
|
||||||
@@ -519,6 +531,7 @@ Install a Java Card `.cap` file on the card via GlobalPlatform commands (INSTALL
|
|||||||
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) for `gen_install_parameters()` |
|
| `nv_quota` / `volatile_quota` | no | Integer memory quotas (bytes) for `gen_install_parameters()` |
|
||||||
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
|
| `make_selectable` | no | If true (default), final INSTALL uses P1=`0C` (install + make selectable) |
|
||||||
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
| `load_block_size` | no | Bytes of load-file payload per LOAD APDU, 1–240 (default 240 when omitted). SCP80 concatenation carries a secured packet larger than one SMS over up to 5 SMs, so the block size is no longer clamped to fit a single SMS. |
|
||||||
|
| `ram_format` | no | RAM command format: `auto` (default — the read-only probe decides, recorded as `FORMAT CHECK (...)` steps), `compact` or `expanded` to pin it |
|
||||||
|
|
||||||
**Response (success):**
|
**Response (success):**
|
||||||
```json
|
```json
|
||||||
@@ -594,8 +607,9 @@ Run the single **INSTALL [for install]** (or **INSTALL [for make selectable]**)
|
|||||||
| `instance_aid` | no | Application AID; empty → the module AID |
|
| `instance_aid` | no | Application AID; empty → the module AID |
|
||||||
| `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` |
|
| `privileges` | no | Hex privileges value (1 or 3 bytes), default `00` |
|
||||||
| `install_params` / `stk_params` | no | As in `/api/ram-install` (the PWA composes `C9`+`EF`+raw and appends the STK part) |
|
| `install_params` / `stk_params` | no | As in `/api/ram-install` (the PWA composes `C9`+`EF`+raw and appends the STK part) |
|
||||||
|
| `ram_format` | no | As in `/api/ram-install`: `auto` (default — a read-only probe step decides), `compact` or `expanded` |
|
||||||
|
|
||||||
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
|
**Response:** `{"success": bool, "steps": [...], "final_cntr": "...", "ram_format": "...", "error": "...", "failed_step": N}` — the same step records as `/api/ram-install`.
|
||||||
|
|
||||||
### `POST /api/sp-verify`
|
### `POST /api/sp-verify`
|
||||||
|
|
||||||
|
|||||||
+26
-10
@@ -1711,7 +1711,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.6.23';
|
const SIMPLE_VERSION = '3.6.24';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== Tab switching =====
|
// ===== Tab switching =====
|
||||||
@@ -4562,6 +4562,10 @@ async function ramQueueScp81() {
|
|||||||
// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> ISD,
|
// Which TAR of the selected preset feeds the SCP80 form: RAM/GP -> ISD,
|
||||||
// SIM RFM -> UICC, USIM RFM -> USIM (spec defaults when the value is empty).
|
// SIM RFM -> UICC, USIM RFM -> USIM (spec defaults when the value is empty).
|
||||||
let _spTarKey = 'tar';
|
let _spTarKey = 'tar';
|
||||||
|
// RAM/GP chains packed into the SCP80 form opt in to the per-operation
|
||||||
|
// command-format detection ('auto'); other packs (HOTA, §9 push, expanded
|
||||||
|
// script) keep their payload byte-exact.
|
||||||
|
let _spRamFormat = null;
|
||||||
|
|
||||||
function spTarKeyForPack(sourceId) {
|
function spTarKeyForPack(sourceId) {
|
||||||
return {
|
return {
|
||||||
@@ -4589,6 +4593,7 @@ function packToSp(sourceId) {
|
|||||||
_spTarKey = key;
|
_spTarKey = key;
|
||||||
document.getElementById('sp-tar').value = spPresetTar(key);
|
document.getElementById('sp-tar').value = spPresetTar(key);
|
||||||
}
|
}
|
||||||
|
_spRamFormat = sourceId === 'chain-ram-preview' ? 'auto' : null;
|
||||||
}
|
}
|
||||||
function spCntrAdjust(delta) {
|
function spCntrAdjust(delta) {
|
||||||
const el = document.getElementById('sp-cntr');
|
const el = document.getElementById('sp-cntr');
|
||||||
@@ -8034,7 +8039,7 @@ function spCntrSyncPreset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function spParams() {
|
function spParams() {
|
||||||
return {
|
const p = {
|
||||||
spi1: document.getElementById('sp-spi1').value,
|
spi1: document.getElementById('sp-spi1').value,
|
||||||
spi2: document.getElementById('sp-spi2-hex').value,
|
spi2: document.getElementById('sp-spi2-hex').value,
|
||||||
kic: document.getElementById('sp-kic-hex').value,
|
kic: document.getElementById('sp-kic-hex').value,
|
||||||
@@ -8045,6 +8050,8 @@ function spParams() {
|
|||||||
kidKey: document.getElementById('sp-kid-key').value,
|
kidKey: document.getElementById('sp-kid-key').value,
|
||||||
apdu: document.getElementById('sp-apdu').value,
|
apdu: document.getElementById('sp-apdu').value,
|
||||||
};
|
};
|
||||||
|
if (_spRamFormat) p.ram_format = _spRamFormat;
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pysimVerifySp() {
|
async function pysimVerifySp() {
|
||||||
@@ -8124,7 +8131,8 @@ async function pysimSendOta() {
|
|||||||
// "advance always" rule silently reverted preset edits).
|
// "advance always" rule silently reverted preset edits).
|
||||||
if (spPorAccepted(por)) {
|
if (spPorAccepted(por)) {
|
||||||
const cntrEl = document.getElementById('sp-cntr');
|
const cntrEl = document.getElementById('sp-cntr');
|
||||||
cntrEl.value = spNextCntr(cntrEl.value);
|
cntrEl.value = data.final_cntr || spNextCntr(cntrEl.value);
|
||||||
|
if (data.ram_format) _spRamFormat = data.ram_format;
|
||||||
msg += ' | CNTR -> ' + cntrEl.value;
|
msg += ' | CNTR -> ' + cntrEl.value;
|
||||||
document.getElementById('sp-result').value = '';
|
document.getElementById('sp-result').value = '';
|
||||||
spShowSizeInfo();
|
spShowSizeInfo();
|
||||||
@@ -8507,9 +8515,12 @@ function ramSaveCntr(cntr) {
|
|||||||
|
|
||||||
// Send a single GP APDU wrapped in SCP80 via /api/send-ota
|
// Send a single GP APDU wrapped in SCP80 via /api/send-ota
|
||||||
async function ramSendOta(apduHex, sp) {
|
async function ramSendOta(apduHex, sp) {
|
||||||
// RAM operations: send raw GP command + SCP80 params; server handles SCP80 wrapping
|
// RAM operations: send raw GP command + SCP80 params; server handles SCP80
|
||||||
// Caller controls SPI2: 0x01 = PoR via ENVELOPE response, 0x21 = PoR via SMS-SUBMIT
|
// wrapping. `ram_format` opts in to the per-operation command-format
|
||||||
const body = Object.assign({}, sp, { apdu: apduHex, sp: '', includeCpi: true });
|
// detection: 'auto' on the first packet, the detected value afterwards.
|
||||||
|
// Caller controls SPI2: 0x01 = PoR via ENVELOPE response, 0x21 = SMS-SUBMIT
|
||||||
|
const body = Object.assign({}, sp, { apdu: apduHex, sp: '', includeCpi: true,
|
||||||
|
ram_format: sp.ram_format || 'auto' });
|
||||||
return await pysimFetch('/api/send-ota', body);
|
return await pysimFetch('/api/send-ota', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8956,10 +8967,14 @@ async function ramExplore(sp) {
|
|||||||
const apdu = ramGetStatusApdu(p1, p2);
|
const apdu = ramGetStatusApdu(p1, p2);
|
||||||
ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...');
|
ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...');
|
||||||
const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 }));
|
const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 }));
|
||||||
|
// Reuse the format detected for the first packet of this run
|
||||||
|
if (res.ram_format) sp.ram_format = res.ram_format;
|
||||||
// A packet the card accepted advances the counter even when the
|
// A packet the card accepted advances the counter even when the
|
||||||
// page itself is incomplete: the card consumed it, and a retry with
|
// page itself is incomplete: the card consumed it, and a retry with
|
||||||
// the same counter is rejected (cntr_low).
|
// the same counter is rejected (cntr_low). The server reports the
|
||||||
if (res.success && spPorAccepted(res.por)) cntr = ramIncrementCntr(cntr);
|
// counter to use next (the format-check packets are included).
|
||||||
|
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)) {
|
if (!res.success || !res.por || !spPorAccepted(res.por)) {
|
||||||
const errorMsg = res.por ? res.por.response_status : (res.error || t('no data'));
|
const errorMsg = res.por ? res.por.response_status : (res.error || t('no data'));
|
||||||
errors.push(label + ': ' + errorMsg);
|
errors.push(label + ': ' + errorMsg);
|
||||||
@@ -9003,8 +9018,9 @@ async function ramExplore(sp) {
|
|||||||
ramShowProgress(t('Memory (GET DATA FF21)') + '...');
|
ramShowProgress(t('Memory (GET DATA FF21)') + '...');
|
||||||
try {
|
try {
|
||||||
const memRes = await ramSendOta('80CAFF2100', Object.assign({}, sp, { spi2: '01' }));
|
const memRes = await ramSendOta('80CAFF2100', Object.assign({}, sp, { spi2: '01' }));
|
||||||
|
if (memRes.ram_format) sp.ram_format = memRes.ram_format;
|
||||||
if (memRes.success && memRes.por && memRes.por.response_status === 'por_ok') {
|
if (memRes.success && memRes.por && memRes.por.response_status === 'por_ok') {
|
||||||
cntr = ramIncrementCntr(cntr);
|
cntr = memRes.final_cntr || ramIncrementCntr(cntr);
|
||||||
const data = memRes.por.decoded ? memRes.por.decoded.last_response_data : '';
|
const data = memRes.por.decoded ? memRes.por.decoded.last_response_data : '';
|
||||||
if (!data) {
|
if (!data) {
|
||||||
errors.push(t('Memory') + ': ' + t('(no data)'));
|
errors.push(t('Memory') + ': ' + t('(no data)'));
|
||||||
@@ -9137,7 +9153,7 @@ async function ramDeleteFromExplorer(aid, withCascade) {
|
|||||||
// when the remote command fails; a rejected packet (cntr_low, PoR error)
|
// when the remote command fails; a rejected packet (cntr_low, PoR error)
|
||||||
// leaves it untouched.
|
// leaves it untouched.
|
||||||
if (res.success && spPorAccepted(res.por)) {
|
if (res.success && spPorAccepted(res.por)) {
|
||||||
sp.cntr = ramIncrementCntr(sp.cntr);
|
sp.cntr = res.final_cntr || ramIncrementCntr(sp.cntr);
|
||||||
ramSaveCntr(sp.cntr);
|
ramSaveCntr(sp.cntr);
|
||||||
}
|
}
|
||||||
const resultEl = document.getElementById('ram-result');
|
const resultEl = document.getElementById('ram-result');
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v296';
|
const CACHE = 'simple-v297';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.6.23"
|
version = "3.6.24"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||||
|
|||||||
+110
-14
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.6.23'
|
VERSION = '3.6.24'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -917,7 +917,8 @@ def _send_envelope(tpdu_hex, scc, sm_sc='12345678912', submit_handler=None,
|
|||||||
submit_handler.submit_tpdu_hex = tpdu_hex
|
submit_handler.submit_tpdu_hex = tpdu_hex
|
||||||
_handle_proactive_chain(scc, sw, _capture_sms_tpdu)
|
_handle_proactive_chain(scc, sw, _capture_sms_tpdu)
|
||||||
data, sw = '', '9000'
|
data, sw = '', '9000'
|
||||||
if handle_proactive and sw == '9000' and submit_handler and not submit_handler.submit_tpdu_hex:
|
if (handle_proactive and sw == '9000' and submit_handler
|
||||||
|
and not submit_handler.submit_tpdu_hex and not data):
|
||||||
sys.stderr.write('STATUS poll (PoR not captured)\n')
|
sys.stderr.write('STATUS poll (PoR not captured)\n')
|
||||||
st_data, st_sw = _send_status(scc)
|
st_data, st_sw = _send_status(scc)
|
||||||
sys.stderr.write('STATUS -> %s\n' % st_sw)
|
sys.stderr.write('STATUS -> %s\n' % st_sw)
|
||||||
@@ -1153,10 +1154,58 @@ def _ram_next_cntr(cntr, advance):
|
|||||||
"""Advance the SCP80 counter by one only when the card accepted the
|
"""Advance the SCP80 counter by one only when the card accepted the
|
||||||
packet (PoR ok / no PoR expected): a rejected packet (cntr_low,
|
packet (PoR ok / no PoR expected): a rejected packet (cntr_low,
|
||||||
rc_cc_ds_failed, ...) leaves the card's expectation and the preset
|
rc_cc_ds_failed, ...) leaves the card's expectation and the preset
|
||||||
counter untouched."""
|
counter untouched. The counter is 5 bytes (40 bits); a 32-bit wrap
|
||||||
|
dropped the top byte of high counters (v3.6.24)."""
|
||||||
if not advance:
|
if not advance:
|
||||||
return cntr
|
return cntr
|
||||||
return '%010X' % ((int(cntr, 16) + 1) % (2 ** 32))
|
return '%010X' % ((int(cntr, 16) + 1) % (2 ** 40))
|
||||||
|
|
||||||
|
|
||||||
|
# RAM command formats (TS 102 226 5.2.1): the bare C-APDU (compact) or the
|
||||||
|
# Command TLV '22' inside the 'AA' scripting template (expanded) - the form
|
||||||
|
# the reference terminal traces use. The format is detected per operation
|
||||||
|
# and never stored: cards differ batch to batch and may behave differently
|
||||||
|
# later, so every operation re-checks (v3.6.24).
|
||||||
|
_RAM_PROBE_APDU = '80F28000024F0000C0000000' # GET STATUS [ISD], read-only
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_expanded_apdu(apdu_hex):
|
||||||
|
"""Wrap a C-APDU in the expanded remote-management format: the Command
|
||||||
|
TLV '22' inside the 'AA' template (TS 102 226 5.2.1), the exact form of
|
||||||
|
the reference trace `AA0A220880F24000024F0000`."""
|
||||||
|
cmd = '22' + _ber_len(len(apdu_hex) // 2) + apdu_hex
|
||||||
|
return ('AA' + _ber_len(len(cmd) // 2) + cmd).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_format_apdu(apdu_hex, ram_format):
|
||||||
|
"""Apply the RAM command format ('compact' or 'expanded')."""
|
||||||
|
return _wrap_expanded_apdu(apdu_hex) if ram_format == 'expanded' else apdu_hex
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_normalize_format(value):
|
||||||
|
"""'auto' | 'compact' | 'expanded'; anything else (or missing) is
|
||||||
|
'auto' for callers that opted in to the format handling."""
|
||||||
|
v = str(value or 'auto').strip().lower()
|
||||||
|
return v if v in ('auto', 'compact', 'expanded') else 'auto'
|
||||||
|
|
||||||
|
|
||||||
|
def _ram_detect_format(server, scc, sp, state):
|
||||||
|
"""Detect the card's RAM command format for one operation: send the
|
||||||
|
read-only GET STATUS [ISD] probe compact, then wrapped; the first format
|
||||||
|
whose remote SW succeeds wins (compact preferred). Probe packets are
|
||||||
|
recorded as steps and consume counters only when accepted; the result is
|
||||||
|
used for this operation only - the next operation re-checks (v3.6.24)."""
|
||||||
|
for fmt, apdu in (('compact', _RAM_PROBE_APDU),
|
||||||
|
('expanded', _wrap_expanded_apdu(_RAM_PROBE_APDU))):
|
||||||
|
scratch = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
|
'cntr': state['cntr']}
|
||||||
|
ok = _ram_send_gp_apdu(server, scc, sp, scratch,
|
||||||
|
'FORMAT CHECK (%s)' % fmt, apdu)
|
||||||
|
state['steps'].extend(scratch['steps'])
|
||||||
|
state['cntr'] = scratch['cntr']
|
||||||
|
if ok:
|
||||||
|
return fmt
|
||||||
|
return 'compact'
|
||||||
|
|
||||||
|
|
||||||
def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
||||||
@@ -1218,9 +1267,13 @@ def _ram_send_gp_apdu(server, scc, sp, state, step_name, apdu_hex):
|
|||||||
'sw': str(e)})
|
'sw': str(e)})
|
||||||
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
sys.stderr.write('RAM-INSTALL: %s encode failed: %s\n' % (step_name, e))
|
||||||
return False
|
return False
|
||||||
|
# Capture the PoR from either transport: inline in the ENVELOPE response
|
||||||
|
# or as a proactive SEND SHORT MESSAGE. Cards differ - some always submit,
|
||||||
|
# whatever the SPI2 request bit says - so the capture is unconditional
|
||||||
|
# (v3.6.24).
|
||||||
submit_handler = None
|
submit_handler = None
|
||||||
old_proactive = None
|
old_proactive = None
|
||||||
if bool(int(spi2, 16) & 0x20) and hasattr(scc, '_tp'):
|
if hasattr(scc, '_tp'):
|
||||||
submit_handler = PoRSubmitHandler()
|
submit_handler = PoRSubmitHandler()
|
||||||
old_proactive = scc._tp.proactive_handler
|
old_proactive = scc._tp.proactive_handler
|
||||||
scc._tp.proactive_handler = submit_handler
|
scc._tp.proactive_handler = submit_handler
|
||||||
@@ -6048,6 +6101,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
self._log_resp({'error': _err('reader_not_init', lang)})
|
self._log_resp({'error': _err('reader_not_init', lang)})
|
||||||
return
|
return
|
||||||
include_cpi = body.get('includeCpi', True)
|
include_cpi = body.get('includeCpi', True)
|
||||||
|
ram_format = None
|
||||||
try:
|
try:
|
||||||
if apdu:
|
if apdu:
|
||||||
# RAM operation: SCP80-wrap the raw GP command. The packet
|
# RAM operation: SCP80-wrap the raw GP command. The packet
|
||||||
@@ -6061,15 +6115,35 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
cntr = body.get('cntr', '')
|
cntr = body.get('cntr', '')
|
||||||
kic_key = body.get('kicKey', '')
|
kic_key = body.get('kicKey', '')
|
||||||
kid_key = body.get('kidKey', '')
|
kid_key = body.get('kidKey', '')
|
||||||
|
# RAM command format: only when the caller opted in with a
|
||||||
|
# ram_format value (the RAM views do; applet-directed or
|
||||||
|
# pre-built payloads must not be wrapped). The probe(s)
|
||||||
|
# consume counters, so the command uses the advanced value.
|
||||||
|
if body.get('ram_format') is not None:
|
||||||
|
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||||
|
if ram_format == 'auto':
|
||||||
|
probe_state = {'steps': [], 'encode_error': None,
|
||||||
|
'failure': {}, 'cntr': cntr}
|
||||||
|
sp_probe = {'spi1': spi1, 'spi2': spi2, 'kic': kic,
|
||||||
|
'kid': kid, 'tar': tar, 'kic_key': kic_key,
|
||||||
|
'kid_key': kid_key,
|
||||||
|
'include_cpi': include_cpi}
|
||||||
|
ram_format = _ram_detect_format(self.server, scc, sp_probe,
|
||||||
|
probe_state)
|
||||||
|
cntr = probe_state['cntr']
|
||||||
|
apdu = _ram_format_apdu(apdu, ram_format)
|
||||||
sp_hex, _ = _build_secured_packet(spi1, spi2, kic, kid, tar, cntr, apdu, kic_key, kid_key)
|
sp_hex, _ = _build_secured_packet(spi1, spi2, kic, kid, tar, cntr, apdu, kic_key, kid_key)
|
||||||
else:
|
else:
|
||||||
# Regular SCP80: use pre-built secured packet
|
# Regular SCP80: use pre-built secured packet
|
||||||
sp_hex = sp
|
sp_hex = sp
|
||||||
spi2_val = int(body.get('spi2', '00'), 16)
|
spi2_val = int(body.get('spi2', '00'), 16)
|
||||||
por_in_submit = bool(spi2_val & 0x20)
|
# Capture the PoR from either transport: inline in the
|
||||||
|
# ENVELOPE response or as a proactive SEND SHORT MESSAGE (some
|
||||||
|
# cards always submit it, whatever the SPI2 request bit says;
|
||||||
|
# v3.6.24).
|
||||||
submit_handler = None
|
submit_handler = None
|
||||||
old_proactive = None
|
old_proactive = None
|
||||||
if por_in_submit and hasattr(scc, '_tp'):
|
if hasattr(scc, '_tp'):
|
||||||
submit_handler = PoRSubmitHandler()
|
submit_handler = PoRSubmitHandler()
|
||||||
old_proactive = scc._tp.proactive_handler
|
old_proactive = scc._tp.proactive_handler
|
||||||
scc._tp.proactive_handler = submit_handler
|
scc._tp.proactive_handler = submit_handler
|
||||||
@@ -6086,6 +6160,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
submit_handler=submit_handler)
|
submit_handler=submit_handler)
|
||||||
if not result['success']:
|
if not result['success']:
|
||||||
resp = result
|
resp = result
|
||||||
|
if ram_format is not None:
|
||||||
|
resp['ram_format'] = ram_format
|
||||||
|
resp['final_cntr'] = cntr
|
||||||
sys.stderr.write('OTA SEND FAILED: %s\n' % result.get('error'))
|
sys.stderr.write('OTA SEND FAILED: %s\n' % result.get('error'))
|
||||||
else:
|
else:
|
||||||
resp = {'success': True, 'sw': result['sw'],
|
resp = {'success': True, 'sw': result['sw'],
|
||||||
@@ -6128,6 +6205,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
sys.stderr.write('OTA PoR[%s]: undecodable raw=%s\n' % (por_src, str(por_hex)))
|
sys.stderr.write('OTA PoR[%s]: undecodable raw=%s\n' % (por_src, str(por_hex)))
|
||||||
else:
|
else:
|
||||||
sys.stderr.write('OTA PoR[%s]: none\n' % por_src)
|
sys.stderr.write('OTA PoR[%s]: none\n' % por_src)
|
||||||
|
if ram_format is not None:
|
||||||
|
accepted = ((por is None) or por.get('response_status') in
|
||||||
|
('por_ok', 'actual_response_sms_submit'))
|
||||||
|
resp['ram_format'] = ram_format
|
||||||
|
resp['final_cntr'] = _ram_next_cntr(cntr, accepted)
|
||||||
finally:
|
finally:
|
||||||
if submit_handler and hasattr(scc, '_tp'):
|
if submit_handler and hasattr(scc, '_tp'):
|
||||||
scc._tp.proactive_handler = old_proactive
|
scc._tp.proactive_handler = old_proactive
|
||||||
@@ -6227,14 +6309,20 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||||
'include_cpi': body.get('includeCpi', True)}
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
|
|
||||||
|
# RAM command format: detected per operation (a read-only probe
|
||||||
|
# step) unless the caller pinned compact/expanded (v3.6.24).
|
||||||
|
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||||
|
if ram_format == 'auto':
|
||||||
|
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||||
|
|
||||||
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
||||||
seq = _cap_apdu_sequence(
|
seq = _cap_apdu_sequence(
|
||||||
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
||||||
privileges=privileges_hex,
|
privileges=privileges_hex,
|
||||||
install_params=install_params_hex, stk_params=stk_params_hex,
|
install_params=install_params_hex, stk_params=stk_params_hex,
|
||||||
make_selectable=make_selectable, block_size=block_size)
|
make_selectable=make_selectable, block_size=block_size)
|
||||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL) loadfile_aid=%s\n' % (
|
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||||
len(seq), len(seq) - 2, loadfile_aid))
|
len(seq), len(seq) - 2, ram_format, loadfile_aid))
|
||||||
for apdu_idx, gp_apdu in enumerate(seq):
|
for apdu_idx, gp_apdu in enumerate(seq):
|
||||||
if apdu_idx == 0:
|
if apdu_idx == 0:
|
||||||
step_name = 'INSTALL [for load]'
|
step_name = 'INSTALL [for load]'
|
||||||
@@ -6243,11 +6331,12 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||||
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
||||||
step_name, gp_apdu):
|
step_name,
|
||||||
|
_ram_format_apdu(gp_apdu, ram_format)):
|
||||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||||
'error': (state['encode_error'] or state['failure'].get('error')
|
'error': (state['encode_error'] or state['failure'].get('error')
|
||||||
or ('%s failed' % step_name)),
|
or ('%s failed' % step_name)),
|
||||||
'final_cntr': state['cntr'],
|
'final_cntr': state['cntr'], 'ram_format': ram_format,
|
||||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||||
'load_block_size': block_size,
|
'load_block_size': block_size,
|
||||||
'load_block_size_requested': block_size_req,
|
'load_block_size_requested': block_size_req,
|
||||||
@@ -6258,6 +6347,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
||||||
'module_aid': module_aid, 'final_cntr': state['cntr'],
|
'module_aid': module_aid, 'final_cntr': state['cntr'],
|
||||||
|
'ram_format': ram_format,
|
||||||
'load_block_size': block_size,
|
'load_block_size': block_size,
|
||||||
'load_block_size_requested': block_size_req,
|
'load_block_size_requested': block_size_req,
|
||||||
'load_block_size_auto': not block_size_req}
|
'load_block_size_auto': not block_size_req}
|
||||||
@@ -6318,11 +6408,17 @@ class PysimHandler(BaseHTTPRequestHandler):
|
|||||||
'include_cpi': body.get('includeCpi', True)}
|
'include_cpi': body.get('includeCpi', True)}
|
||||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
'cntr': sp_state['cntr']}
|
'cntr': sp_state['cntr']}
|
||||||
sys.stderr.write('RAM-INSTALL-APP: %s (%s) cntr=%s\n' % (
|
# RAM command format: detected per operation (a read-only probe
|
||||||
apdu, step_name, sp_state['cntr']))
|
# step) unless the caller pinned compact/expanded (v3.6.24).
|
||||||
|
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||||
|
if ram_format == 'auto':
|
||||||
|
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||||
|
apdu = _ram_format_apdu(apdu, ram_format)
|
||||||
|
sys.stderr.write('RAM-INSTALL-APP: %s (%s, %s) cntr=%s\n' % (
|
||||||
|
apdu, step_name, ram_format, sp_state['cntr']))
|
||||||
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||||
resp = {'success': bool(ok), 'steps': state['steps'],
|
resp = {'success': bool(ok), 'steps': state['steps'],
|
||||||
'final_cntr': state['cntr']}
|
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||||
if not ok:
|
if not ok:
|
||||||
resp['error'] = (state['encode_error'] or state['failure'].get('error')
|
resp['error'] = (state['encode_error'] or state['failure'].get('error')
|
||||||
or ('%s failed' % step_name))
|
or ('%s failed' % step_name))
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ from pysim_simple_server.server import (
|
|||||||
_parse_sms_concat,
|
_parse_sms_concat,
|
||||||
_por_remote_sw,
|
_por_remote_sw,
|
||||||
_ram_next_cntr,
|
_ram_next_cntr,
|
||||||
|
_ram_format_apdu,
|
||||||
|
_wrap_expanded_apdu,
|
||||||
_ram_remote_sw_ok,
|
_ram_remote_sw_ok,
|
||||||
_sms_submit_por,
|
_sms_submit_por,
|
||||||
_ram_step_result,
|
_ram_step_result,
|
||||||
@@ -1359,7 +1361,10 @@ class RamPorStepTest(unittest.TestCase):
|
|||||||
def test_counter_advances_only_for_accepted_packets(self):
|
def test_counter_advances_only_for_accepted_packets(self):
|
||||||
self.assertEqual(_ram_next_cntr('0000000010', True), '0000000011')
|
self.assertEqual(_ram_next_cntr('0000000010', True), '0000000011')
|
||||||
self.assertEqual(_ram_next_cntr('0000000010', False), '0000000010')
|
self.assertEqual(_ram_next_cntr('0000000010', False), '0000000010')
|
||||||
self.assertEqual(_ram_next_cntr('FFFFFFFF', True), '0000000000')
|
# 5-byte counter: high values keep their top byte (v3.6.24)
|
||||||
|
self.assertEqual(_ram_next_cntr('10000AAAC8', True), '10000AAAC9')
|
||||||
|
self.assertEqual(_ram_next_cntr('0000FFFFFF', True), '0001000000')
|
||||||
|
self.assertEqual(_ram_next_cntr('FFFFFFFFFF', True), '0000000000')
|
||||||
|
|
||||||
def test_remote_sw_from_expanded_response(self):
|
def test_remote_sw_from_expanded_response(self):
|
||||||
por = {'response_status': 'por_ok', 'decoded': {},
|
por = {'response_status': 'por_ok', 'decoded': {},
|
||||||
@@ -1510,3 +1515,76 @@ class RamSendGpApduLoggingTest(unittest.TestCase):
|
|||||||
out = buf.getvalue()
|
out = buf.getvalue()
|
||||||
self.assertIn('RAM C-APDU (LOAD (1/2)): 80E8800001AA', out)
|
self.assertIn('RAM C-APDU (LOAD (1/2)): 80E8800001AA', out)
|
||||||
self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)
|
self.assertIn('RAM SECURED-PACKET (LOAD (1/2)): AABB', out)
|
||||||
|
|
||||||
|
|
||||||
|
class RamCommandFormatTests(unittest.TestCase):
|
||||||
|
"""RAM command format detection helpers (TS 102 226 5.2.1, v3.6.24)."""
|
||||||
|
|
||||||
|
def test_expanded_wrapper_matches_the_reference_trace(self):
|
||||||
|
# TCA Loader trace: GET STATUS [ISD] sent as 'AA' > '22' > C-APDU
|
||||||
|
self.assertEqual(_wrap_expanded_apdu('80F24000024F0000'),
|
||||||
|
'AA0A220880F24000024F0000')
|
||||||
|
|
||||||
|
def test_expanded_wrapper_uses_ber_lengths(self):
|
||||||
|
apdu = '80' + 'AB' * 200
|
||||||
|
wrapped = _wrap_expanded_apdu(apdu)
|
||||||
|
self.assertTrue(wrapped.startswith('AA81'), wrapped[:8])
|
||||||
|
self.assertTrue(wrapped.endswith(apdu))
|
||||||
|
self.assertEqual(_ram_format_apdu(apdu, 'compact'), apdu)
|
||||||
|
self.assertEqual(_ram_format_apdu(apdu, 'expanded'), wrapped)
|
||||||
|
|
||||||
|
def test_detect_prefers_compact_and_records_the_probe(self):
|
||||||
|
from pysim_simple_server import server as srv
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_send(server, scc, sp, state, name, apdu):
|
||||||
|
calls.append((name, apdu))
|
||||||
|
state['steps'].append({'name': name, 'por_status': 'por_ok', 'sw': '9000'})
|
||||||
|
state['cntr'] = srv._ram_next_cntr(state['cntr'], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
orig = srv._ram_send_gp_apdu
|
||||||
|
srv._ram_send_gp_apdu = fake_send
|
||||||
|
try:
|
||||||
|
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
|
'cntr': '0000000010'}
|
||||||
|
fmt = srv._ram_detect_format(None, None, {}, state)
|
||||||
|
finally:
|
||||||
|
srv._ram_send_gp_apdu = orig
|
||||||
|
self.assertEqual(fmt, 'compact')
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(calls[0][0], 'FORMAT CHECK (compact)')
|
||||||
|
self.assertEqual(state['cntr'], '0000000011')
|
||||||
|
self.assertEqual(len(state['steps']), 1)
|
||||||
|
|
||||||
|
def test_detect_falls_back_to_expanded(self):
|
||||||
|
from pysim_simple_server import server as srv
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_send(server, scc, sp, state, name, apdu):
|
||||||
|
calls.append((name, apdu))
|
||||||
|
wrapped = apdu.startswith('AA')
|
||||||
|
state['steps'].append({'name': name,
|
||||||
|
'por_status': 'por_ok' if wrapped else 'por_remote_error',
|
||||||
|
'sw': '9000'})
|
||||||
|
if wrapped:
|
||||||
|
state['cntr'] = srv._ram_next_cntr(state['cntr'], True)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
orig = srv._ram_send_gp_apdu
|
||||||
|
srv._ram_send_gp_apdu = fake_send
|
||||||
|
try:
|
||||||
|
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||||
|
'cntr': '10000AAAC8'}
|
||||||
|
fmt = srv._ram_detect_format(None, None, {}, state)
|
||||||
|
finally:
|
||||||
|
srv._ram_send_gp_apdu = orig
|
||||||
|
self.assertEqual(fmt, 'expanded')
|
||||||
|
self.assertEqual([c[0] for c in calls],
|
||||||
|
['FORMAT CHECK (compact)', 'FORMAT CHECK (expanded)'])
|
||||||
|
self.assertTrue(calls[1][1].startswith('AA'))
|
||||||
|
# only the accepted (expanded) probe advanced the 5-byte counter
|
||||||
|
self.assertEqual(state['cntr'], '10000AAAC9')
|
||||||
|
self.assertEqual(len(state['steps']), 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user