release: v3.9.1 - the counter probe sends a fixed read-only APDU

- The probe no longer takes a caller-supplied `apdu`: it always sends the
  read-only SELECT MF (the counter advances on the packet's security
  acceptance whatever the applet answers - 6A86/6D00 are normal there), so
  no caller can turn the sync probe into a write.
- Version 3.9.1: server VERSION, pyproject.toml and the PWA header; sw cache
  simple-v357; docs/api.md and the repo AGENTS.md updated, and the workspace
  AGENTS.md gains the v3.9.1 history entry (its "Currently" line was stale at
  v3.7.1).
This commit is contained in:
2026-10-02 07:29:12 +03:00
parent e147ab90b4
commit f059d05040
5 changed files with 14 additions and 11 deletions
+6 -6
View File
@@ -252,7 +252,7 @@ import also accepts presets exported by the older localStorage-based builds.
Sync a preset's SCP80 counter with the card. The card accepts only a counter
*above* its own (TS 102 225 §5.1.1, SPI1 b5b4 = `10`) and updates it only when
the packet is accepted, so rejected attempts change nothing; the probe walks a
doubling ladder (`start+1`, `+2`, `+4`, …) with a read-only command until one
doubling ladder (`start+1`, `+2`, `+4`, …) with a fixed read-only command until one
is accepted, then persists `accepted+1` (monotonic) into the keyset. The
search stops at a ceiling and after a bounded number of packets, so it can
never run towards the 40-bit maximum, where the card blocks the counter. The
@@ -261,15 +261,15 @@ the packet's, TS 102 225 §5.2 Table 3), which is why this probe exists.
```json
{"preset_id": "…", "kvn": 2, "tar": "000000", "spi1": "16", "spi2": "21",
"cntr": "0001000000", "ceiling": "FFFFFFFF", "max_attempts": 40,
"apdu": "00A40000023F00"}
"cntr": "0001000000", "ceiling": "FFFFFFFF", "max_attempts": 40}
```
All fields except `preset_id` are optional: `kvn` picks a keyset (the first
otherwise), `tar`/`spi1`/`spi2` default to the preset's values, `cntr` to the
keyset's counter, `ceiling` to `FFFFFFFF` (must stay below `FFFFFFFFFF`),
`max_attempts` to 40 (clamped to 1–64), and `apdu` to a read-only `SELECT MF`
(the probe command should never modify anything). A SPI1 without a counter
keyset's counter, `ceiling` to `FFFFFFFF` (must stay below `FFFFFFFFFF`) and
`max_attempts` to 40 (clamped to 1–64). The probe always sends the read-only
`SELECT MF` - the counter advances on the packet's security acceptance whatever
the applet answers (`6A86`/`6D00` are normal there). A SPI1 without a counter
check is refused (400).
**Response:** `{"success": true, "accepted_cntr": "0001000000",
+1 -1
View File
@@ -1801,7 +1801,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.9.0';
const SIMPLE_VERSION = '3.9.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v356';
const CACHE = 'simple-v357';
const URLS = [
'index.html',
'help.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.9.0"
version = "3.9.1"
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.
+5 -2
View File
@@ -32,7 +32,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.9.0'
VERSION = '3.9.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -1668,7 +1668,10 @@ def _counter_probe_params(preset, body):
'spi1': spi1,
'spi2': str(body.get('spi2') or preset.get('spi2') or '21').strip().upper(),
'tar': str(body.get('tar') or preset.get('tar') or '000000').strip().upper(),
'apdu': str(body.get('apdu') or COUNTER_PROBE_DEFAULT_APDU).replace(' ', '').upper(),
# the probe command is fixed: read-only SELECT MF, which every applet
# answers (an error SW is fine - the counter advances on the packet's
# security acceptance, not on the command's own result)
'apdu': COUNTER_PROBE_DEFAULT_APDU,
}