v1.9.7: spec fixes, T=1/parity, EOF guards, dead code + rate scaling
The audit's P2 items: - ODD_INS: add 0x73 MANAGE SECURE CHANNEL and 0x75 TRANSACT DATA (TS 102 221 Table 10.5); the tools/vs_reader.py mirror and the pinned test are updated. - 9E/9F are SW1: ISO 7816-3 10.3.3 Table 11 makes every 9X value SW1. Remove the fabricated "extended-length procedure byte" that consumed SW2 as a length and lost the status word. - T=1: EOF-guard every block read, bound the M-bit chain with T1_CHAIN_MAX, and drop the caller's duplicate chain loop -- it read the next block after every I-block (usually the peer's R-block) and merged the following I-block's INF into the packet. R/S blocks emit nothing; the (possibly chained) I-block INF is the APDU payload. - Parity: a packet containing a parity-error byte is emitted with GSMTAP_FLAG_BAD_FCS (the CHKSUM annotation was invisible downstream), with a log note when the exchange is otherwise clean. - EOF guards: handle_atr (TA/TB/TC/TD, historical, TCK), handle_pps (request/response fields incl. PPS1-3) and the T=1 block parser return cleanly on a truncated capture instead of raising on None. - Dead code: _edge_read was initialized True and never cleared, so the ATR-based T=0 procedure loop (58 lines) and the 1.5-ETU guard branch were unreachable. Remove the flag, both dead paths and the branch; dedent the live loop (behavior unchanged). - Samplerate scaling: _samples_for_us() replaces the hard-coded 16 MHz thresholds -- start-bit deglitch (0.125 us), min_high floor (0.25 us) and the ETU glitch floor (0.75 us). At 16 MHz the values are unchanged. Validation: 130/130 unit tests (13 new: 9E/9F SW1, parity BAD_FCS, T=1 I/R/S blocks + M chain + truncations + one-block-per-step, PPS truncations, truncated ATR TA, samplerate/ETU-floor vectors); the 10-trace suite is unchanged from v1.9.6 (identical APDU/CHKSUM/CONCAT/ BAD_FCS counts, RESULT: OK); compare_baseline.sh reports IDENTICAL. VERSION -> 1.9.7.
This commit is contained in:
1 parent
3e39032802
commit
6e6f465841
7 files changed
+588
-354
No files matched your search
@@ -142,10 +142,10 @@ the reader log for all reader exchanges:
|
||||
|
||||
Both traces show 0 garbage APDUs, 0 payload mismatches, and RESULT: OK.
|
||||
|
||||
**Root cause:** `_edge_read` is initialized to `True` and never set to
|
||||
`False`, so the edge-read T=0 procedure loop (lines 1808–1895) always
|
||||
executes — the ATR-based procedure loop (lines 1898–1941) is dead code.
|
||||
The edge-read path had two bugs:
|
||||
**Root cause:** `_edge_read` was initialized to `True` and never set to
|
||||
`False`, so the edge-read T=0 procedure loop always executed and the
|
||||
ATR-based procedure loop was dead code. **FIXED (v1.9.7):** the flag and
|
||||
the dead loop are removed. The edge-read path had two bugs:
|
||||
|
||||
1. **NULLs treated as command data (FIXED).** The first post-header byte
|
||||
was checked for ACK; if it was NULL (0x60), it entered the case-3/4
|
||||
@@ -333,6 +333,54 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||
|
||||
### v1.9.7: spec fixes, T=1/parity, EOF guards, dead code + rate scaling
|
||||
|
||||
The audit's P2 items:
|
||||
|
||||
- **Odd INS set completed.** `0x73` MANAGE SECURE CHANNEL and `0x75`
|
||||
TRANSACT DATA (TS 102 221 Table 10.5) join `ODD_INS`; the mirrored set in
|
||||
`tools/vs_reader.py` and the pinned test are updated.
|
||||
- **9E/9F are SW1.** ISO 7816-3 §10.3.3 Table 11: every `9X` value is
|
||||
SW1. The fabricated "extended-length procedure byte" (it consumed the
|
||||
next byte as a length, eating SW2 and losing the status word) is gone.
|
||||
The second copy lived in the dead ATR-based loop removed below.
|
||||
- **T=1 blocks.** `t1_parse_block()` EOF-guards every read (a truncated
|
||||
block returns no half INF instead of raising) and bounds the M-bit chain
|
||||
with `T1_CHAIN_MAX` instead of unbounded recursion. The caller's
|
||||
duplicate "chain follow-up" loop -- it read the *next* block after every
|
||||
I-block, usually the peer's R-block, and merged the following I-block's
|
||||
INF into the packet -- is gone. R/S blocks emit nothing; the (possibly
|
||||
chained) I-block INF is emitted as the APDU payload. No T=1 trace
|
||||
exists in the corpus, so this is unit-tested only.
|
||||
- **Parity flag.** A packet containing a parity-error byte is now emitted
|
||||
with `GSMTAP_FLAG_BAD_FCS` (the CHKSUM annotation alone was invisible to
|
||||
downstream tooling), with a log note when the exchange is otherwise
|
||||
clean.
|
||||
- **EOF guards.** `handle_atr()` (TA/TB/TC/TD, historical bytes, TCK) and
|
||||
`handle_pps()` (request and response fields, including the optional
|
||||
PPS1-3) return cleanly on a truncated capture instead of raising on a
|
||||
None byte; the T=1 block parser likewise.
|
||||
- **Dead code removed.** `_edge_read` was initialized `True` and never
|
||||
cleared, so the "ATR-based" T=0 procedure loop (58 lines) never ran and
|
||||
the 1.5-ETU guard branch was unreachable. The flag and both dead paths
|
||||
are removed; the edge reader is the only T=0 framing reader (the
|
||||
CLK-sync reader is selected by `_clock_skip_confident`). The live loop
|
||||
was dedented, not rewritten -- the trace metrics are unchanged.
|
||||
- **Samplerate scaling.** `_samples_for_us()` replaces the hard-coded
|
||||
16 MHz thresholds: the start-bit deglitch (`_min_pulse_samples`; `None`
|
||||
= 2 samples @ 16 MHz), the `min_high` floor (0.25 us) and the ETU glitch
|
||||
floor in `_etu_candidate` (0.75 us). At 16 MHz the values are
|
||||
unchanged; another capture rate now scales instead of breaking the
|
||||
filters.
|
||||
|
||||
Validation: 130/130 unit tests (13 new: 9E/9F SW1 for both values, the
|
||||
parity BAD_FCS flag, T=1 I/R/S blocks + M chain + two truncation cases
|
||||
+ a one-block-per-step caller test + two PPS truncations, the truncated
|
||||
ATR TA, the samplerate/ETU-floor vectors); the 10-trace suite is
|
||||
unchanged from v1.9.6 (identical
|
||||
APDU/CHKSUM/CONCAT/BAD_FCS counts, `RESULT: OK` everywhere);
|
||||
`compare_baseline.sh` reports IDENTICAL. VERSION -> 1.9.7.
|
||||
|
||||
### v1.9.6: ETU lock validation, hunt recovery, resync + ATR acceptance
|
||||
|
||||
The audit's P1 findings. The validation helpers are unit-tested; the trace
|
||||
@@ -838,13 +886,11 @@ The 2-sample advance in `wait_data_falling` shifts `_start_fall` by 2
|
||||
samples, but at Samsung's 16 CLK/ETU (~72 samples) this is negligible
|
||||
(<3% of a bit period) and does not affect downstream byte reading.
|
||||
|
||||
**Design note: magic number.** `_min_pulse_samples = 2` is tuned for the
|
||||
primary 16 MHz sample rate (2 samples ≈ 125 ns, well within the FX2LP
|
||||
pull-up rise time of 200–500 ns). At other sample rates the value would
|
||||
need adjustment — but since all current traces are 16 MHz, this is left
|
||||
as a hardcoded constant rather than a decoder option. If 12 MHz or
|
||||
24 MHz traces are added later, consider making it configurable or
|
||||
scaling it from the sample rate.
|
||||
**Design note: pulse threshold.** `_min_pulse_samples` used to be a
|
||||
hard-coded 2 samples, tuned for the 16 MHz corpus (≈125 ns, within the
|
||||
FX2LP pull-up rise time of 200–500 ns). Since v1.9.7 it is `None` by
|
||||
default and scales from `samplerate` (`_samples_for_us(0.125)` → 2 at
|
||||
16 MHz); a test or option may still override it (0 disables the check).
|
||||
|
||||
**Logging.** Each rejected start-bit glitch is logged with a running
|
||||
count (`start-bit glitch rejected (#N)`) visible in the session log.
|
||||
@@ -868,8 +914,7 @@ window may be filtering real start bits.
|
||||
| ACK (== INS or == ~INS) | Full data match: card acknowledges and expects all P3 data bytes |
|
||||
| INS^0x01 or ~INS^0x01 | Single-byte match: card validates syntax but processes data one byte at a time |
|
||||
| 0x60 | NULL: card busy, wait for next procedure byte (never an SW1) |
|
||||
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
||||
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
||||
| SW1 (`0x61`–`0x6F` / `0x90`–`0x9F`) | Status word: exchange complete. ISO 7816-3 §10.3.3 Table 11 has no exception for `9X`: `9E`/`9F` are SW1 too (the v1.9.7 fix) |
|
||||
|
||||
##### CLA (Class) Byte — Coding and Logical Channels
|
||||
|
||||
@@ -971,7 +1016,7 @@ append the measured CLK frequency when known
|
||||
|
||||
### Versioning
|
||||
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.6'`.
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.7'`.
|
||||
The version is printed to the log on decoder startup.
|
||||
|
||||
### Testing after decoder changes
|
||||
@@ -1166,7 +1211,8 @@ framing classifies the first post-header byte in this order (v1.9.5):
|
||||
the following exchanges could be merged silently).
|
||||
- **~INS** → byte-by-byte transfer: one data byte per ACK, read until SW.
|
||||
- Otherwise → case 3/4: the byte is terminal command data; read `P3`
|
||||
command-data bytes, then the card's procedure byte (ACK/SW/`9E`/`9F`).
|
||||
command-data bytes, then the card's procedure byte (ACK/SW; `9E`/`9F`
|
||||
are SW1, not an extended length).
|
||||
On ACK, append any inline response data until SW (ISO 7816-3 §12.2 maps
|
||||
case 4 to a separate GET RESPONSE, so conforming cards send SW/61XY
|
||||
here; the append is a permissive extension).
|
||||
@@ -1213,15 +1259,19 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
`clock_skip=369` before this fix). Logs "RST low at start: expecting cold-boot
|
||||
ATR". Decodes the full cold-boot ATR on `xiaomi_mi_a1_coldboot_sample.sr` /
|
||||
`4gmodem_coldboot_sample.sr` (0 GARBAGE).
|
||||
- **1.5-ETU start-bit guard.** Rejects the phantom `0x00` an idle glitch
|
||||
inserts before an ATR. Reliable.
|
||||
- **Half-ETU start-bit guard (edge reader).** Rejects the phantom `0x00`
|
||||
an idle glitch inserts before an ATR while still accepting the ~1-ETU
|
||||
inter-byte gap on fast ETUs (v1.9.7 removed the unreachable 1.5-ETU
|
||||
CLK-sync branch). Reliable.
|
||||
- **VCC/RST 10 ms level-stability hysteresis.** Reports a level only after
|
||||
≥10 ms stable. Reliable; symmetric power-up/down, kills spurious events
|
||||
from a ringing/floating sense line.
|
||||
- **PPS speed-switch follow (`clock_skip = F/D`).** After an accepted PPS the
|
||||
native skip becomes F/D CLK cycles/bit. Reliable (verified live).
|
||||
- **T=1 block-chain follow-up bound.** Caps chained I-blocks so garbage
|
||||
cannot loop forever. Reliable.
|
||||
- **T=1 block decode (v1.9.7).** NAD/PCB/LEN/INF/LRC with the M-bit chain
|
||||
followed inside `t1_parse_block()` (bounded by `T1_CHAIN_MAX`) and every
|
||||
read EOF-guarded; R/S blocks carry no INF and are not emitted. Unit
|
||||
tested only (no T=1 capture in the corpus). Reliable by construction.
|
||||
- **Hunt give-up (no synthetic ATR).** After `ATR_HUNT_FAIL_LIMIT` failed
|
||||
hunts the decoder resumes DATA without fabricating an ATR (v1.6.0 removed
|
||||
the old `[0x3B, 0x00]` fallback); the mid-session ETU estimator then
|
||||
@@ -1235,7 +1285,8 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
v1.9.6); a rate that fails parity is invalidated and re-measured.
|
||||
- **Edge-list bit reader (`_read_byte_edges`).** Per-byte edge reconstruction
|
||||
from the DATA edge list (re-anchors the next byte via `_next_start_fall`).
|
||||
Drift-immune. Reliable for mid-session/edge mode; gated by `_edge_read`.
|
||||
Drift-immune. The T=0 framing reader; the CLK-sync reader is selected
|
||||
when `_clock_skip_confident` is set (`_edge_read` removed in v1.9.7).
|
||||
- **`decode_step` unified dispatch.** One state machine handles
|
||||
ATR/PPS/DATA + mid-session. Reliable; current architecture.
|
||||
- **`hasT0`/`hasT1` guards + init.** Prevents `AttributeError` and skips byte
|
||||
@@ -1246,10 +1297,11 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
else command data (case 3/4). Reliable for consolidating STK envelope
|
||||
commands. P3-bounded reads avoid FCP tag misidentification (v1.1.1
|
||||
reverted read-until-SW from v1.1.0).
|
||||
- **1.5-ETU start-bit guard in `wait_data_falling`.** `min_high = 1.5 *
|
||||
bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
|
||||
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
|
||||
after `_measure_etu()` consumption (v1.1.1 fix for post-ATR desync).
|
||||
- **Half-ETU start-bit guard in `wait_data_falling` (v1.9.7).** `min_high
|
||||
= 0.5 * bit_samples` (with a rate-scaled floor) rejects intra-byte glitch
|
||||
falls while accepting the ~1-ETU inter-byte gap; the next byte's start is
|
||||
queued by the edge reader (`_next_start_fall`), so no 1.5-ETU guard is
|
||||
needed on this path.
|
||||
- **Inverse-convention support (TS=0x3F, v1.9.6).** The hunt already read
|
||||
`0x3F`; the DATA state now accepts it too, the start bit stays a falling
|
||||
edge from an idle-HIGH line (ISO 7816-3 7.1), and `get_bytes()` assembles
|
||||
@@ -1288,11 +1340,11 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
`P3`) with P3-bounded reads for all paths (v1.1.1 reverted the
|
||||
read-until-SW experiment from v1.1.0 which mis-identified FCP `0x62` as
|
||||
SW1).
|
||||
- **0.5-ETU start-bit guard in `wait_data_falling` (for `_edge_read`).**
|
||||
`min_high = 0.5 * bit_samples` was too small for slow ETUs — it accepted
|
||||
intra-byte HIGH periods (data bit transitions) as start bits, causing byte
|
||||
misalignment after `_measure_etu()` consumption. Replaced by 1.5-ETU guard
|
||||
in v1.1.1.
|
||||
- **0.5-ETU start-bit guard for the ATR-based CLK-sync loop.** It was
|
||||
rejected in v1.1.1 for that path (it accepted intra-byte HIGH periods on
|
||||
slow ETUs). The path was dead (`_edge_read` never False) and is removed
|
||||
in v1.9.7; the surviving edge reader uses the 0.5 guard because its next
|
||||
start bit is queued by `_next_start_fall` (no re-hunt mid-burst).
|
||||
|
||||
### Open residual limitations
|
||||
|
||||
|
||||
@@ -55,10 +55,13 @@ this decoder's `pcap_file=<path>` option).
|
||||
| RST tracking (optional) | `rst_detect=true`: emits reset assert/deassert events (sub_type 0x10) |
|
||||
| VCC tracking (optional) | `vcc_detect=true`: emits power on/off events (sub_type 0x11) |
|
||||
| Direct PCAP file | `pcap_file=<path>` writes PCAP without shell redirection |
|
||||
| Start-bit deglitch | 2-sample deglitch in the start-bit detector eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
||||
| Start-bit deglitch | Deglitch in the start-bit detector (2 samples at 16 MHz, scaled from the sample rate since v1.9.7) eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
||||
| Mid-session ETU lock | The DATA-edge ETU estimator rejects guard-only (all-zero) and single-spacing bursts and invalidates a rate that fails parity, so a capture at a non-default F/D locks safely |
|
||||
| ATR / resync guards | A DATA-state `0x3B`/`0x3F` is an ATR candidate only before the first APDU; parity-dirty or bare (TS+T0) frames are ignored, and `resync_idle` really waits for the idle level |
|
||||
| Inverse convention | TS=0x3F ATRs follow ISO 7816-3 7.1: inverted data, msb first, start bit still LOW from an idle-HIGH line |
|
||||
| T=1 blocks | NAD/PCB/LEN/INF/LRC decode with M-bit chaining; the I-block INF is emitted as the APDU, R/S blocks are not; a truncated capture ends cleanly |
|
||||
| Parity flag | A packet containing a parity-error byte is emitted with the GSMTAP BAD_FCS flag (not just an annotation) |
|
||||
| Rate-aware thresholds | Start-bit deglitch, guard floors and the ETU glitch floor scale from the capture sample rate instead of assuming 16 MHz |
|
||||
|
||||
## Decoder options
|
||||
|
||||
@@ -158,7 +161,8 @@ shift the whole ATR parse off-by-one). The decoder now:
|
||||
- never lets a decode exception kill the capture — errors are logged,
|
||||
annotated, and the state machine resyncs (end-of-stream still exits
|
||||
cleanly);
|
||||
- bounds T=1 block-chain follow-up so garbage cannot loop forever.
|
||||
- decodes T=1 blocks (NAD/PCB/LEN/INF/LRC) and bounds the M-bit chain so garbage
|
||||
cannot loop forever.
|
||||
|
||||
After an accepted PPS the decoder **follows the speed switch**: in
|
||||
native CLK mode `clock_skip` becomes F/D CLK cycles per bit (e.g.
|
||||
@@ -170,7 +174,7 @@ answer with a fresh ATR at the default 372 cycles/bit. The decoder
|
||||
**re-arms the ATR hunt on RST deassert**: state back to `FIND START`,
|
||||
native skip reset to 372, hunt count cleared — so the re-ATR parses as
|
||||
an ATR instead of being mis-read as TPDU traffic at the PPS-negotiated
|
||||
rate. A **1.5-ETU start-bit guard** additionally rejects the phantom
|
||||
rate. A **half-ETU start-bit guard** (edge reader) additionally rejects the phantom
|
||||
`0x00` an idle glitch can insert before that ATR. Requires
|
||||
`rst_detect=true` (the `rst` channel assigned); without it a mid-session
|
||||
reset is still mis-read.
|
||||
@@ -186,7 +190,7 @@ mis-read as a "premature abort", but there was never a device fault.
|
||||
Most of the early "noise" seen in captures (parity errors, mis-framed
|
||||
garbage, a bouncing VCC line) was a badly-decoded state machine, not the
|
||||
hardware: re-seating the tap did nothing, while the decoder rewrite fixed
|
||||
it completely (warm-reset re-arm, 1.5-ETU start-bit guard, 10 ms VCC
|
||||
it completely (warm-reset re-arm, half-ETU start-bit guard, 10 ms VCC
|
||||
debounce, T=0 exchange reassembly). A clean capture now decodes to zero
|
||||
garbage — see the `vs_reader.py` validation below.
|
||||
|
||||
|
||||
+8
-5
@@ -58,10 +58,13 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
||||
| Отслеживание RST (опционально) | `rst_detect=true`: события assert/deassert сброса (sub_type 0x10) |
|
||||
| Отслеживание VCC (опционально) | `vcc_detect=true`: события включения/выключения питания (sub_type 0x11) |
|
||||
| PCAP сразу в файл | `pcap_file=<path>` пишет PCAP без перенаправления вывода оболочки |
|
||||
| Деглитч стартового бита | Деглитч в 2 отсчёта в детекторе стартового бита убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
||||
| Деглитч стартового бита | Деглитч в детекторе стартового бита (2 отсчёта при 16 МГц, с v1.9.7 масштабируется от частоты дискретизации) убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
||||
| Захват ETU в середине сессии | Оценщик ETU по фронтам DATA отбрасывает пачки только со сторожевым интервалом (нулевые данные) и одиночные интервалы, а также сбрасывает скорость, не прошедшую проверку чётности, поэтому трасса с нестандартным F/D захватывается надёжно |
|
||||
| Защита ATR / ресинхронизации | `0x3B`/`0x3F` в состоянии DATA считается кандидатом в ATR только до первого APDU; кадры с ошибкой чётности или голый TS+T0 игнорируются, а `resync_idle` действительно ждёт уровень простоя |
|
||||
| Инверсная конвенция | ATR с TS=0x3F разбирается по ISO 7816-3 7.1: инверсные данные, msb первым, стартовый бит по-прежнему LOW при простое линии HIGH |
|
||||
| Блоки T=1 | Разбор NAD/PCB/LEN/INF/LRC с цепочкой по биту M; INF I-блока выдаётся как APDU, R/S-блоки не выдаются; обрезанная запись завершается чисто |
|
||||
| Флаг чётности | Пакет с байтом с ошибкой чётности выдаётся с флагом GSMTAP BAD_FCS (не только аннотацией) |
|
||||
| Пороги от частоты | Деглитч стартового бита, пороги защиты и порог глитча ETU масштабируются от частоты дискретизации, а не считаются от 16 МГц |
|
||||
|
||||
## Опции декодера
|
||||
|
||||
@@ -163,8 +166,8 @@ flags) USB-протокола simtrace2 и не является частью с
|
||||
- никогда не даёт исключению при декодировании убить запись — ошибки
|
||||
логируются, аннотируются, а конечный автомат ресинхронизируется
|
||||
(конец потока по-прежнему завершается чисто);
|
||||
- ограничивает продолжение цепочки T=1-блоков, чтобы мусор не мог
|
||||
зациклиться навсегда.
|
||||
- разбирает блоки T=1 (NAD/PCB/LEN/INF/LRC) и ограничивает цепочку по биту M,
|
||||
чтобы мусор не мог зациклиться навсегда.
|
||||
|
||||
После принятого PPS декодер **следует переключению скорости**: в режиме
|
||||
native CLK `clock_skip` становится равным F/D тактов CLK на бит (например,
|
||||
@@ -178,7 +181,7 @@ native CLK `clock_skip` становится равным F/D тактов CLK
|
||||
`FIND START`, native-пропуск сбрасывается в 372, счётчик охоты очищается —
|
||||
поэтому повторный ATR разбирается как ATR, а не ошибочно читается как
|
||||
TPDU-трафик на скорости, согласованной PPS. **Защита стартового бита в
|
||||
1.5 ETU** дополнительно отбрасывает фантомный `0x00`, который глитч на
|
||||
0.5 ETU** (edge-ридер) дополнительно отбрасывает фантомный `0x00`, который глитч на
|
||||
холостом ходу может вставить перед этим ATR. Требуется `rst_detect=true`
|
||||
(назначенный канал `rst`); без него сброс в середине сессии всё равно
|
||||
будет прочитан неверно.
|
||||
@@ -196,7 +199,7 @@ TPDU-трафик на скорости, согласованной PPS. **За
|
||||
неверного кадрирования, дребезг линии VCC) была следствием плохо
|
||||
декодированного конечного автомата, а не железа: переподключение съёма
|
||||
ничего не давало, а переписанный декодер полностью это вылечил (re-arm по
|
||||
тёплому сбросу, защита стартового бита 1.5 ETU, антидребезг VCC 10 мс,
|
||||
тёплому сбросу, защита стартового бита 0.5 ETU, антидребезг VCC 10 мс,
|
||||
сборка обмена T=0). Чистая запись теперь декодируется с нулём мусора —
|
||||
см. проверку `vs_reader.py` ниже.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||
GSMTAP_FLAG_BAD_FCS)
|
||||
|
||||
VERSION = '1.9.6'
|
||||
VERSION = '1.9.7'
|
||||
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ def plausible_cla(cla):
|
||||
# lands on a data byte -- the text run "I M" (0x49 0x4D) started a bogus
|
||||
# frame on xiaomi_phone_sample when every odd INS was accepted.
|
||||
ODD_INS = frozenset((
|
||||
0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0x0F, 0x21, 0x73, 0x75, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD,
|
||||
))
|
||||
|
||||
@@ -382,6 +382,7 @@ MAX_RESYNC_ATTEMPTS = 1024 # bail out if no idle gap found in that many tries
|
||||
# idle 0xFF forever at the wrong rate and can never reach the DATA state,
|
||||
# where the live ETU is measured -- a deadlock. These cap the retries.
|
||||
ATR_HUNT_ETU_ATTEMPTS = 3 # live-ETU recovery attempts before resuming DATA
|
||||
T1_CHAIN_MAX = 32 # T=1 I-block M-bit chain depth bound
|
||||
ATR_HUNT_IDLE_RESETS = 3 # all-0xFF hunt resets before resuming DATA
|
||||
ATR_HUNT_FAIL_LIMIT = 12 # total failed hunt rounds before resuming DATA
|
||||
|
||||
@@ -569,7 +570,6 @@ class Decoder(srd.Decoder):
|
||||
# uses bit_samples) is the safe fallback.
|
||||
self._clock_skip_confident = False
|
||||
self._start_fall = None
|
||||
self._edge_read = True
|
||||
self._last_sn = -1
|
||||
# Bounded allowance for steps that consume only queued bytes
|
||||
# (_replay / peeked_byte) and therefore do not advance the sample
|
||||
@@ -577,7 +577,7 @@ class Decoder(srd.Decoder):
|
||||
# would mistake a re-frame step for end-of-capture.
|
||||
self._no_advance = 0
|
||||
self._eof = False
|
||||
self._min_pulse_samples = 2 # tuned for 16 MHz sample rate
|
||||
self._min_pulse_samples = None # None = scale with samplerate (2 @ 16 MHz)
|
||||
self._glitch_rejects = 0
|
||||
self._prev_wait_sn = None
|
||||
self._stall = 0
|
||||
@@ -798,6 +798,18 @@ class Decoder(srd.Decoder):
|
||||
return (cand, cand_sn, cand, lvl, now)
|
||||
return (None, 0, confirmed, lvl, now)
|
||||
|
||||
def _samples_for_us(self, us):
|
||||
'''Return the sample count for a duration in microseconds, scaled
|
||||
to the capture samplerate.
|
||||
|
||||
The glitch/pulse thresholds used to be hard-coded for 16 MHz
|
||||
(`_min_pulse_samples = 2`, `min_high = 4`, `MIN_ETU = 12`); at
|
||||
another rate those become far too large or too small. The examples
|
||||
are 16 MHz, which is also the fallback when the framework has not
|
||||
reported a rate (unit tests, older sigrok-cli).'''
|
||||
sr = getattr(self, 'samplerate', None) or 16000000
|
||||
return max(1, int(round(float(sr) * us / 1000000.0)))
|
||||
|
||||
def _clk_hz(self):
|
||||
'''Return the measured CLK frequency in Hz, or None when unknown.
|
||||
|
||||
@@ -1066,7 +1078,9 @@ class Decoder(srd.Decoder):
|
||||
has a smaller spacing somewhere, so the guard pattern is
|
||||
distinguishable when every non-minimum spacing is at least 2x the
|
||||
minimum.'''
|
||||
MIN_ETU = 12
|
||||
# ~0.75 us: 12 samples at 16 MHz. A real ETU is far longer, but a
|
||||
# glitch can be shorter; scale with the capture rate.
|
||||
MIN_ETU = self._samples_for_us(0.75)
|
||||
valid = [p for p in pairs if p[0] >= MIN_ETU]
|
||||
if len(valid) >= 2:
|
||||
med = sorted(p[0] for p in valid)[len(valid) // 2]
|
||||
@@ -1148,7 +1162,6 @@ class Decoder(srd.Decoder):
|
||||
# (immune to the drift of fixed sample-skip timing) for every byte.
|
||||
# The CLK-sync reader is used once _derive_clock_skip_from_etu()
|
||||
# has a real measurement; a parity failure invalidates it.
|
||||
self._edge_read = True
|
||||
if self._etu_measured:
|
||||
self.log("etu recovered (samples):", self.bit_samples)
|
||||
|
||||
@@ -1577,20 +1590,13 @@ class Decoder(srd.Decoder):
|
||||
The bit period is measured directly from the DATA line in SAMPLES
|
||||
(not by counting CLK edges), so a frozen CLK during Clock Stop Mode
|
||||
cannot desync the decoder. A start bit is accepted only after DATA
|
||||
has been HIGH for >= 1.5 bit periods (inter-character guard, rejects
|
||||
intra-byte edges / idle glitches).'''
|
||||
min_high = 4
|
||||
has been HIGH for >= half an ETU: this rejects intra-byte glitch
|
||||
falls yet still accepts the legitimate inter-byte gap (the ~1-etu
|
||||
stop bit), which a 1.5-etu guard would wrongly reject on fast ETUs
|
||||
(e.g. a phone SIM at ~53 samples/etu).'''
|
||||
min_high = self._samples_for_us(0.25)
|
||||
if self.bit_samples:
|
||||
if self._edge_read:
|
||||
# DATA must be HIGH (idle) for at least half an etu before a
|
||||
# fall is accepted as a start bit. This rejects intra-byte
|
||||
# glitch falls yet still accepts the legitimate inter-byte gap
|
||||
# (the ~1-etu stop bit), which a 1.5-etu guard would wrongly
|
||||
# reject on fast ETUs (e.g. a phone SIM at ~53 samples/etu),
|
||||
# causing bytes to be skipped / 0x00 inserted.
|
||||
min_high = max(int(0.5 * self.bit_samples), 4)
|
||||
else:
|
||||
min_high = max(int(1.5 * self.bit_samples), 4)
|
||||
min_high = max(int(0.5 * self.bit_samples), min_high)
|
||||
while True:
|
||||
# Wait for DATA idle, servicing RST/VCC.
|
||||
# ISO 7816-3 7.1: the line idles HIGH and the start bit is
|
||||
@@ -1630,8 +1636,11 @@ class Decoder(srd.Decoder):
|
||||
break
|
||||
if (self.samplenum - hi_start) < min_high:
|
||||
continue
|
||||
if self._min_pulse_samples > 0:
|
||||
pins = self.wait({'skip': self._min_pulse_samples})
|
||||
pulse = self._min_pulse_samples
|
||||
if pulse is None:
|
||||
pulse = self._samples_for_us(0.125) # 2 samples @ 16 MHz
|
||||
if pulse > 0:
|
||||
pins = self.wait({'skip': pulse})
|
||||
if pins is not None:
|
||||
bit = pins[self.DATA_IDX]
|
||||
idle_val = 1 # line HIGH is idle in both conventions
|
||||
@@ -1674,10 +1683,9 @@ class Decoder(srd.Decoder):
|
||||
self.peeked_byte = None
|
||||
self.peeked_samplenum = -1
|
||||
return byte;
|
||||
# In edge-read (mid-session, gated-CLK) mode the next start bit is
|
||||
# already queued by _read_byte_edges, so re-hunting would wrongly
|
||||
# reject the short inter-byte gap. Otherwise re-hunt normally.
|
||||
if not (self._edge_read and self._start_fall is not None):
|
||||
# The next start bit is already queued by _read_byte_edges, so
|
||||
# re-hunting would wrongly reject the short inter-byte gap.
|
||||
if self._start_fall is None:
|
||||
self.wait_data_falling()
|
||||
if self._eof:
|
||||
return None
|
||||
@@ -1688,7 +1696,7 @@ class Decoder(srd.Decoder):
|
||||
return self._replay[0]
|
||||
if (self.peeked_byte != None):
|
||||
return self.peeked_byte
|
||||
if not (self._edge_read and self._start_fall is not None):
|
||||
if self._start_fall is None:
|
||||
self.wait_data_falling()
|
||||
if self._eof:
|
||||
return None
|
||||
@@ -1967,18 +1975,26 @@ class Decoder(srd.Decoder):
|
||||
return
|
||||
if (firstT0 & 0b00010000):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
tA.append(byte)
|
||||
self.ATR.append(byte)
|
||||
if (firstT0 & 0b00100000):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
tB.append(byte)
|
||||
self.ATR.append(byte)
|
||||
if (firstT0 & 0b01000000):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
tC.append(byte)
|
||||
self.ATR.append(byte)
|
||||
if (firstT0 & 0b10000000):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
if ((byte & 0x0F) == 0):
|
||||
self.hasT0 = True
|
||||
elif ((byte & 0x0F) == 1):
|
||||
@@ -1997,6 +2013,8 @@ class Decoder(srd.Decoder):
|
||||
|
||||
for _ in range(0, t0 & 0x0F):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
self.ATR.append(byte)
|
||||
|
||||
if (self.hasT1 == False and self.hasT0 == False):
|
||||
@@ -2004,6 +2022,8 @@ class Decoder(srd.Decoder):
|
||||
|
||||
if (self.hasT1 == True or self.hasT15):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
return
|
||||
self.ATR.append(byte)
|
||||
xor = 0
|
||||
for i in range(1, len(self.ATR)): xor = xor ^ self.ATR[i]
|
||||
@@ -2068,17 +2088,29 @@ class Decoder(srd.Decoder):
|
||||
self.hasT0 = False
|
||||
self.hasT1 = True
|
||||
|
||||
def t1_parse_block(self, es):
|
||||
def t1_parse_block(self, es, depth=0):
|
||||
'''Parse one T=1 block (NAD, PCB, LEN, INF, LRC) and return
|
||||
(is_i_block, inf_bytes).
|
||||
|
||||
An I-block with the M bit set is followed by the chain of further
|
||||
I-blocks in the same direction; their INF fields are concatenated
|
||||
(the T=1 application APDU may span them). Every read is
|
||||
EOF-guarded -- a truncated capture returns no half block instead of
|
||||
raising -- and the chain is bounded by T1_CHAIN_MAX so a garbage
|
||||
M bit cannot recurse without limit.'''
|
||||
packet = []
|
||||
lrc = 0;
|
||||
nad = self.read_byte()
|
||||
lrc = lrc ^ nad
|
||||
if nad is None:
|
||||
return (False, packet)
|
||||
lrc = nad
|
||||
self.put(es, self.samplenum, self.out_ann, [1, "NAD:" + hex(nad)])
|
||||
sad = nad & 0x70
|
||||
dad = nad & 0x07
|
||||
self.log("T=1 NAD=", hex(nad), "SAD=", hex(sad), "DAD=", hex(dad))
|
||||
|
||||
pcb = self.read_byte()
|
||||
if pcb is None:
|
||||
return (False, packet)
|
||||
lrc = lrc ^ pcb
|
||||
|
||||
isSBlock = False
|
||||
@@ -2101,15 +2133,23 @@ class Decoder(srd.Decoder):
|
||||
self.log("PCB I-Block", hex(pcb))
|
||||
|
||||
bLen = self.read_byte()
|
||||
if bLen is None:
|
||||
return (False, packet)
|
||||
lrc = lrc ^ bLen
|
||||
if (bLen > 0):
|
||||
for _ in range(bLen):
|
||||
byte = self.read_byte()
|
||||
if byte is None:
|
||||
self.log("T=1 block truncated in INF at EOF")
|
||||
return (False, [])
|
||||
lrc = lrc ^ byte
|
||||
if (isIBlock): packet.append(byte)
|
||||
|
||||
# CRC to implement
|
||||
bLrc = self.read_byte()
|
||||
# CRC to implement (TC1 b0); LRC only for now.
|
||||
bLrc = self.read_byte()
|
||||
if bLrc is None:
|
||||
self.log("T=1 block truncated before the LRC at EOF")
|
||||
return (False, [])
|
||||
lrc = lrc ^ bLrc
|
||||
|
||||
if (lrc != 0):
|
||||
@@ -2125,16 +2165,13 @@ class Decoder(srd.Decoder):
|
||||
if (isSBlock):
|
||||
self.put(es, self.samplenum, self.out_ann, [8, ["S-Block", "S-Block flag={flag:02x}".format(flag=pcb & 0b00111111)]])
|
||||
|
||||
if (isIBlock and pcb & 0b00100000): # m-flag
|
||||
if (isIBlock and pcb & 0b00100000 and depth < T1_CHAIN_MAX):
|
||||
self.log("T=1 Multiblock flag", hex(pcb))
|
||||
for _ in range(32): # bounded: garbage must not loop forever
|
||||
isIBlock2,packet2 = self.t1_parse_block(self.samplenum)
|
||||
if (isIBlock2):
|
||||
packet = packet + packet2
|
||||
break
|
||||
next_is_i, packet2 = self.t1_parse_block(self.samplenum, depth + 1)
|
||||
if next_is_i:
|
||||
packet = packet + packet2
|
||||
|
||||
|
||||
return isIBlock,packet;
|
||||
return isIBlock, packet
|
||||
|
||||
def handle_pps(self):
|
||||
# Mark PPS attempted so idle 0xFF bytes after a failed/no-PPS
|
||||
@@ -2143,8 +2180,12 @@ class Decoder(srd.Decoder):
|
||||
lrc = 0
|
||||
ss = self.peeked_samplenum
|
||||
pps = self.read_byte()
|
||||
if pps is None:
|
||||
return
|
||||
pps_req = [pps]
|
||||
pps0 = self.read_byte()
|
||||
if pps0 is None:
|
||||
return
|
||||
pps_req.append(pps0)
|
||||
# An idle I/O line idles HIGH (repeated 0xFF). A genuine PPS0 always
|
||||
# has its reserved bits clear (bit7 and bits 0..3 == 0), so the value
|
||||
@@ -2159,17 +2200,25 @@ class Decoder(srd.Decoder):
|
||||
pps1 = 0; pps2 = 0; pps3 = 0
|
||||
if (pps0 & 0b00010000):
|
||||
pps1 = self.read_byte()
|
||||
if pps1 is None:
|
||||
return
|
||||
pps_req.append(pps1)
|
||||
lrc = lrc ^ pps1
|
||||
if (pps0 & 0b00100000):
|
||||
pps2 = self.read_byte()
|
||||
if pps2 is None:
|
||||
return
|
||||
pps_req.append(pps2)
|
||||
lrc = lrc ^ pps2
|
||||
if (pps0 & 0b01000000):
|
||||
pps3 = self.read_byte()
|
||||
if pps3 is None:
|
||||
return
|
||||
pps_req.append(pps3)
|
||||
lrc = lrc ^ pps3
|
||||
pck = self.read_byte()
|
||||
if pck is None:
|
||||
return
|
||||
pps_req.append(pck)
|
||||
lrc = lrc ^ pps ^ pps0 ^ pck
|
||||
if (lrc != 0):
|
||||
@@ -2182,26 +2231,38 @@ class Decoder(srd.Decoder):
|
||||
|
||||
r_lrc = 0
|
||||
r_pps = self.read_byte()
|
||||
if r_pps is None:
|
||||
return
|
||||
pps_rsp = [r_pps]
|
||||
r_pps1 = 0; r_pps2 = 0; r_pps3 = 0
|
||||
if (r_pps != 0xFF):
|
||||
self.put(ss, self.samplenum, self.out_ann, [0, ["PPS Request not confirmed"]])
|
||||
self.log("PPS Request not confirmed", r_pps)
|
||||
r_pps0 = self.read_byte()
|
||||
if r_pps0 is None:
|
||||
return
|
||||
pps_rsp.append(r_pps0)
|
||||
if (r_pps0 & 0b00010000):
|
||||
r_pps1 = self.read_byte()
|
||||
if r_pps1 is None:
|
||||
return
|
||||
pps_rsp.append(r_pps1)
|
||||
r_lrc = r_lrc ^ r_pps1
|
||||
if (r_pps0 & 0b00100000):
|
||||
r_pps2 = self.read_byte()
|
||||
if r_pps2 is None:
|
||||
return
|
||||
pps_rsp.append(r_pps2)
|
||||
r_lrc = r_lrc ^ r_pps2
|
||||
if (r_pps0 & 0b01000000):
|
||||
r_pps3 = self.read_byte()
|
||||
if r_pps3 is None:
|
||||
return
|
||||
pps_rsp.append(r_pps3)
|
||||
r_lrc = r_lrc ^ r_pps3
|
||||
r_pck = self.read_byte()
|
||||
if r_pck is None:
|
||||
return
|
||||
pps_rsp.append(r_pck)
|
||||
r_lrc = r_lrc ^ r_pps ^ r_pps0 ^ r_pck
|
||||
if (r_lrc != 0):
|
||||
@@ -2423,6 +2484,10 @@ class Decoder(srd.Decoder):
|
||||
elif self.state == 'DATA':
|
||||
packet = [];
|
||||
forced_bad = False
|
||||
# A parity error on any byte of this exchange flags the emitted
|
||||
# packet BAD_FCS (the annotation/log alone is not visible to
|
||||
# downstream tooling).
|
||||
parity_before = self._parity_errors
|
||||
|
||||
# Mid-session capture with an explicit protocol: there is no ATR to
|
||||
# derive the bit period from, so recover it once from the live line
|
||||
@@ -2585,284 +2650,210 @@ class Decoder(srd.Decoder):
|
||||
# event. Procedure bytes (ACK == INS, NULL 0x60, 9E/9F) are
|
||||
# framing and omitted.
|
||||
got_sw = False
|
||||
if self._edge_read:
|
||||
# Mid-session (gated-CLK) path.
|
||||
#
|
||||
# T=0 case 3/4 commands carry Lc (= P3) command-data bytes
|
||||
# on the wire IMMEDIATELY after the 5-byte header, BEFORE
|
||||
# the card's first procedure byte. The original loop (which
|
||||
# treats the first post-header byte as a procedure byte)
|
||||
# therefore mis-frames them. We distinguish the cases by
|
||||
# the card's first procedure byte:
|
||||
# * ACK (== INS or ~INS) right after the header -> case 2/4, the
|
||||
# terminal sent no command data and the card is
|
||||
# acknowledging; read P3 response bytes (+SW), or, for
|
||||
# Le == 0 (GET RESPONSE), read until the SW.
|
||||
# * any other byte -> case 3/4, it
|
||||
# is terminal command data; read P3 command-data bytes,
|
||||
# then the card's procedure byte (ACK/SW/9x).
|
||||
# Mid-session (gated-CLK) path.
|
||||
#
|
||||
# T=0 case 3/4 commands carry Lc (= P3) command-data bytes
|
||||
# on the wire IMMEDIATELY after the 5-byte header, BEFORE
|
||||
# the card's first procedure byte. The original loop (which
|
||||
# treats the first post-header byte as a procedure byte)
|
||||
# therefore mis-frames them. We distinguish the cases by
|
||||
# the card's first procedure byte:
|
||||
# * ACK (== INS or ~INS) right after the header -> case 2/4, the
|
||||
# terminal sent no command data and the card is
|
||||
# acknowledging; read P3 response bytes (+SW), or, for
|
||||
# Le == 0 (GET RESPONSE), read until the SW.
|
||||
# * any other byte -> case 3/4, it
|
||||
# is terminal command data; read P3 command-data bytes,
|
||||
# then the card's procedure byte (ACK/SW/9x).
|
||||
pb0 = self.read_byte()
|
||||
if pb0 is None:
|
||||
return True
|
||||
# Skip NULL (0x60) procedure bytes and turnaround idle
|
||||
# bytes (0xFF): the card may send NULLs while processing
|
||||
# before the ACK, or the bus may idle at xFF during
|
||||
# turnaround on the bidirectional DATA line. Without
|
||||
# this the NULLs / idle bytes are mis-identified as
|
||||
# case-3/4 command data.
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
if pb0 != 0x60 and pb0 != 0xFF:
|
||||
break
|
||||
self._note_discard(pb0,
|
||||
'idle/NULL byte before procedure byte',
|
||||
idle=True)
|
||||
pb0 = self.read_byte()
|
||||
if pb0 is None:
|
||||
return True
|
||||
# Skip NULL (0x60) procedure bytes and turnaround idle
|
||||
# bytes (0xFF): the card may send NULLs while processing
|
||||
# before the ACK, or the bus may idle at xFF during
|
||||
# turnaround on the bidirectional DATA line. Without
|
||||
# this the NULLs / idle bytes are mis-identified as
|
||||
# case-3/4 command data.
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
if pb0 != 0x60 and pb0 != 0xFF:
|
||||
break
|
||||
self._note_discard(pb0,
|
||||
'idle/NULL byte before procedure byte',
|
||||
idle=True)
|
||||
pb0 = self.read_byte()
|
||||
if pb0 is None:
|
||||
return True
|
||||
if plausible_sw(pb0):
|
||||
# Immediate status word: ISO 7816-3 §10.3.3 lets the
|
||||
# card finish the command at the first procedure
|
||||
# byte; a case-2 rejection ('6A82', '6CXX', ...)
|
||||
# carries no ACK and must not be read as data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb0)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
elif pb0 == bIns:
|
||||
if (p3 == 0):
|
||||
# P3=0 in an outgoing command is Le=256
|
||||
# (ISO 7816-3 §10.3.2): the card ACKs and sends
|
||||
# response data until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# ACK: read the P3 response bytes, then the SW.
|
||||
# The card may return FEWER bytes than Le
|
||||
# (6282 end-of-file; Lr <= Le, TS 102 221
|
||||
# §10.1.6): if the byte after the read is not
|
||||
# SW1, split the buffer at the first plausible
|
||||
# SW1 followed by a plausible CLA and replay the
|
||||
# tail (flagged BAD_FCS).
|
||||
resp = []
|
||||
eof = False
|
||||
for _ in range(p3):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
eof = True
|
||||
break
|
||||
resp.append(b)
|
||||
s1 = None if eof else self.read_byte()
|
||||
if plausible_sw(pb0):
|
||||
# Immediate status word: ISO 7816-3 §10.3.3 lets the
|
||||
# card finish the command at the first procedure
|
||||
# byte; a case-2 rejection ('6A82', '6CXX', ...)
|
||||
# carries no ACK and must not be read as data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb0)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
elif pb0 == bIns:
|
||||
if (p3 == 0):
|
||||
# P3=0 in an outgoing command is Le=256
|
||||
# (ISO 7816-3 §10.3.2): the card ACKs and sends
|
||||
# response data until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# ACK: read the P3 response bytes, then the SW.
|
||||
# The card may return FEWER bytes than Le
|
||||
# (6282 end-of-file; Lr <= Le, TS 102 221
|
||||
# §10.1.6): if the byte after the read is not
|
||||
# SW1, split the buffer at the first plausible
|
||||
# SW1 followed by a plausible CLA and replay the
|
||||
# tail (flagged BAD_FCS).
|
||||
resp = []
|
||||
eof = False
|
||||
for _ in range(p3):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
eof = True
|
||||
break
|
||||
resp.append(b)
|
||||
s1 = None if eof else self.read_byte()
|
||||
if s1 is None:
|
||||
eof = True
|
||||
while s1 in (0x60, 0xFF):
|
||||
s1 = self.read_byte()
|
||||
if s1 is None:
|
||||
eof = True
|
||||
while s1 in (0x60, 0xFF):
|
||||
s1 = self.read_byte()
|
||||
if s1 is None:
|
||||
eof = True
|
||||
break
|
||||
if s1 is not None and plausible_sw(s1):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.extend(resp)
|
||||
packet.extend((s1, sw2))
|
||||
got_sw = True
|
||||
else:
|
||||
buf = resp + ([s1] if s1 is not None else [])
|
||||
split = None
|
||||
for j in range(len(buf) - 1):
|
||||
if not plausible_sw(buf[j]):
|
||||
continue
|
||||
k = j + 2
|
||||
if k >= len(buf) or plausible_cla(buf[k]):
|
||||
split = j
|
||||
break
|
||||
if s1 is not None and plausible_sw(s1):
|
||||
if split is not None:
|
||||
packet.extend(buf[:split])
|
||||
packet.extend(buf[split:split + 2])
|
||||
for t in reversed(buf[split + 2:]):
|
||||
self._replay.appendleft(t)
|
||||
got_sw = True
|
||||
forced_bad = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: split %d data byte(s) "
|
||||
"before SW1 0x%02x (Le %d)"
|
||||
% (split, buf[split], p3))
|
||||
else:
|
||||
packet.extend(buf)
|
||||
forced_bad = True
|
||||
self._resync = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: no status word in "
|
||||
"%d byte(s) read for Le %d"
|
||||
% (len(buf), p3))
|
||||
elif pb0 == (bIns ^ 0xFF):
|
||||
# ~INS ACK: one data byte at a time. Read data
|
||||
# until the SW, consuming the procedure bytes in
|
||||
# between (ISO 7816-3 Table 11).
|
||||
if self._t0_read_response_data(packet, bIns):
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 3/4: the first post-header byte is command
|
||||
# data (P3 = Lc).
|
||||
packet.append(pb0)
|
||||
if (p3 == 0):
|
||||
# P3=0 and no procedure byte: treat the byte as
|
||||
# response data (Le=256, permissive) and read
|
||||
# until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 3/4: P3 command-data bytes, then
|
||||
# procedure byte.
|
||||
for _ in range(max(p3 - 1, 0)):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
for _proc in range(32):
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return True
|
||||
if (pb == bIns or pb == (bIns ^ 0xFF)):
|
||||
# ACK: case-4 response data follows.
|
||||
# Append it until the status word (it
|
||||
# used to be discarded). INS means all
|
||||
# remaining data; ~INS is per byte.
|
||||
if pb == bIns:
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
elif self._t0_read_response_data(packet, bIns):
|
||||
got_sw = True
|
||||
break
|
||||
elif (pb == (bIns ^ 0x01)
|
||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||
# Single-byte match (INS^0x01 / ~INS^0x01):
|
||||
# read 1 byte, continue handshake.
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
continue
|
||||
elif (pb == 0x60 or pb == 0xFF):
|
||||
# NULL (0x60): card busy. Turnaround
|
||||
# idle (0xFF): bidirectional bus turnaround
|
||||
# between terminal command data and the
|
||||
# card's procedure byte. Both: consume and
|
||||
# wait for next procedure byte.
|
||||
continue
|
||||
elif plausible_sw(pb):
|
||||
# SW1: case 3, no response data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.extend(resp)
|
||||
packet.extend((s1, sw2))
|
||||
packet.append(pb)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
else:
|
||||
buf = resp + ([s1] if s1 is not None else [])
|
||||
split = None
|
||||
for j in range(len(buf) - 1):
|
||||
if not plausible_sw(buf[j]):
|
||||
continue
|
||||
k = j + 2
|
||||
if k >= len(buf) or plausible_cla(buf[k]):
|
||||
split = j
|
||||
break
|
||||
if split is not None:
|
||||
packet.extend(buf[:split])
|
||||
packet.extend(buf[split:split + 2])
|
||||
for t in reversed(buf[split + 2:]):
|
||||
self._replay.appendleft(t)
|
||||
got_sw = True
|
||||
forced_bad = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: split %d data byte(s) "
|
||||
"before SW1 0x%02x (Le %d)"
|
||||
% (split, buf[split], p3))
|
||||
else:
|
||||
packet.extend(buf)
|
||||
forced_bad = True
|
||||
self._resync = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: no status word in "
|
||||
"%d byte(s) read for Le %d"
|
||||
% (len(buf), p3))
|
||||
elif pb0 == (bIns ^ 0xFF):
|
||||
# ~INS ACK: one data byte at a time. Read data
|
||||
# until the SW, consuming the procedure bytes in
|
||||
# between (ISO 7816-3 Table 11).
|
||||
if self._t0_read_response_data(packet, bIns):
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 3/4: the first post-header byte is command
|
||||
# data (P3 = Lc).
|
||||
packet.append(pb0)
|
||||
if (p3 == 0):
|
||||
# P3=0 and no procedure byte: treat the byte as
|
||||
# response data (Le=256, permissive) and read
|
||||
# until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 3/4: P3 command-data bytes, then
|
||||
# procedure byte.
|
||||
for _ in range(max(p3 - 1, 0)):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
for _proc in range(32):
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return True
|
||||
if (pb == bIns or pb == (bIns ^ 0xFF)):
|
||||
# ACK: case-4 response data follows.
|
||||
# Append it until the status word (it
|
||||
# used to be discarded). INS means all
|
||||
# remaining data; ~INS is per byte.
|
||||
if pb == bIns:
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
elif self._t0_read_response_data(packet, bIns):
|
||||
# Unexpected byte: scan for SW,
|
||||
# skip non-SW bytes (e.g. 0xFF
|
||||
# turnaround artifacts).
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
if plausible_sw(pb):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
elif (pb == (bIns ^ 0x01)
|
||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||
# Single-byte match (INS^0x01 / ~INS^0x01):
|
||||
# read 1 byte, continue handshake.
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
break
|
||||
self._note_pre_sw(pb,
|
||||
'non-SW byte during '
|
||||
'unexpected-byte scan')
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
continue
|
||||
elif (pb == 0x60 or pb == 0xFF):
|
||||
# NULL (0x60): card busy. Turnaround
|
||||
# idle (0xFF): bidirectional bus turnaround
|
||||
# between terminal command data and the
|
||||
# card's procedure byte. Both: consume and
|
||||
# wait for next procedure byte.
|
||||
continue
|
||||
elif (pb == 0x9e or pb == 0x9f):
|
||||
# Extended-length procedure byte: L
|
||||
# response data bytes follow, then SW.
|
||||
L = self.read_byte()
|
||||
if L is None:
|
||||
return True
|
||||
for _ in range(L):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
continue
|
||||
elif plausible_sw(pb):
|
||||
# SW1: case 3, no response data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
else:
|
||||
# Unexpected byte: scan for SW,
|
||||
# skip non-SW bytes (e.g. 0xFF
|
||||
# turnaround artifacts).
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
if plausible_sw(pb):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
self._note_pre_sw(pb,
|
||||
'non-SW byte during '
|
||||
'unexpected-byte scan')
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return True
|
||||
break
|
||||
else:
|
||||
# ATR-based captures (test_8 / test_7): keep the original
|
||||
# procedure loop unchanged.
|
||||
data_read = False
|
||||
for _proc in range(8):
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
break
|
||||
if (pb == bIns or pb == (bIns ^ 0xFF)) and not data_read:
|
||||
# ACK (full match / ~INS): the P3 bytes follow on
|
||||
# the wire (Lc command data for case 3, Le response
|
||||
# data for case 2).
|
||||
data_read = True
|
||||
for _ in range(p3):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
break
|
||||
packet.append(b)
|
||||
elif (pb == (bIns ^ 0x01)
|
||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||
# Single-byte match (INS^0x01 / ~INS^0x01):
|
||||
# read 1 byte, continue handshake.
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
break
|
||||
packet.append(b)
|
||||
elif (pb == 0x60):
|
||||
# NULL: card busy; wait for the next procedure byte.
|
||||
pass
|
||||
elif (pb == 0x9e or pb == 0x9f):
|
||||
# T=0 extended-length procedure byte: the next byte
|
||||
# L is the number of response data bytes the card
|
||||
# now sends, followed by the status word.
|
||||
L = self.read_byte()
|
||||
if L is None:
|
||||
break
|
||||
for _ in range(L):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
break
|
||||
packet.append(b)
|
||||
elif plausible_sw(pb):
|
||||
# SW1: append SW2; the exchange is complete.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
break
|
||||
packet.append(pb)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
else:
|
||||
# Mis-framed procedure byte: stop reading. The
|
||||
# partial packet is flagged desynced downstream and
|
||||
# the decoder re-aligns on the next idle gap.
|
||||
self.put(es, self.samplenum, self.out_ann,
|
||||
[0, ["INVALID Procedure Byte"]])
|
||||
self.log("INVALID Procedure Byte", hex(pb))
|
||||
self._resync = True
|
||||
break
|
||||
self.put(es, self.samplenum, self.out_ann, [4, ["T=0"]])
|
||||
if (got_sw and len(packet) >= 7):
|
||||
self.put(es, self.samplenum, self.out_ann, [9, ["APDU", "APDU cls={cls:02x} ins={ins:02x}".format(cls=bClass,ins=bIns), "APDU cls={cls:02x} ins={ins:02x} p1={p1:02x} p2={p2:02x} p3={p3:02x} len={len} status={sw1:02x}{sw2:02x}".format(cls=bClass,ins=bIns,p1=p1,p2=p2,p3=p3,len=p3,sw1=packet[-2],sw2=packet[-1])]])
|
||||
elif (self.hasT1):
|
||||
isIBlock,packet = self.t1_parse_block(es)
|
||||
if (isIBlock):
|
||||
for _ in range(32): # bounded chain follow-up
|
||||
isIBlock,packet2 = self.t1_parse_block(es)
|
||||
if (isIBlock):
|
||||
packet = packet + packet2
|
||||
break
|
||||
# One block per step (the M-bit chain is followed inside
|
||||
# t1_parse_block); the old extra chain loop read the next
|
||||
# block -- usually the other side's R-block -- and merged
|
||||
# the following I-block into this packet.
|
||||
isIBlock, packet = self.t1_parse_block(es)
|
||||
self.put(es, self.samplenum, self.out_ann, [4, ["T=1", "T=1 (reassembled)"]])
|
||||
if (len(packet) >= 8):
|
||||
self.put(es, self.samplenum, self.out_ann, [9, ["APDU", "APDU cls={cls:02x} ins={ins:02x}".format(cls=packet[0],ins=packet[1]), "APDU cls={cls:02x} ins={ins:02x} p1={p1:02x} p2={p2:02x} p3={p3:02x} len={len} status={sw1:02x}{sw2:02x}".format(cls=packet[0],ins=packet[1],p1=packet[2],p2=packet[3],p3=packet[4],len=len(packet) - 7,sw1=packet[-2],sw2=packet[-1])]])
|
||||
@@ -2912,9 +2903,14 @@ class Decoder(srd.Decoder):
|
||||
flags=GSMTAP_FLAG_BAD_FCS)
|
||||
else:
|
||||
self._idle_low = 0
|
||||
parity_bad = (self._parity_errors != parity_before)
|
||||
if parity_bad and not forced_bad:
|
||||
self.log("APDU flagged BAD_FCS: parity error "
|
||||
"in the exchange")
|
||||
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet),
|
||||
es, self.samplenum,
|
||||
flags=GSMTAP_FLAG_BAD_FCS if forced_bad else 0)
|
||||
flags=GSMTAP_FLAG_BAD_FCS
|
||||
if (forced_bad or parity_bad) else 0)
|
||||
|
||||
self.log("PACKETEND", codecs.encode(bytes(packet), 'hex'))
|
||||
else:
|
||||
|
||||
+25
-12
@@ -1,7 +1,7 @@
|
||||
=== Baseline captured 2026-10-20T17:56:26+03:00 ===
|
||||
=== Baseline captured 2026-10-20T18:55:32+03:00 ===
|
||||
|
||||
=== test_8 ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('etu recovered (samples):', 1237)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== test_7 ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('etu recovered (samples):', 1236)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_phone_sample ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('etu recovered (samples):', 53)
|
||||
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_mi_a1_coldboot ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== 4gmodem_coldboot ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== alcor_reader_sample ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.9.6 ===',)
|
||||
('=== iso7816 decoder v1.9.7 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -350,6 +350,7 @@ test_handoff_on_odd_ins_89 (__main__.TestAtrHuntDetect.test_handoff_on_odd_ins_8
|
||||
test_inverse_ts_pattern_is_parsed (__main__.TestAtrHuntDetect.test_inverse_ts_pattern_is_parsed) ... ok
|
||||
test_no_handoff_on_basic_channel_cla (__main__.TestAtrHuntDetect.test_no_handoff_on_basic_channel_cla) ... ok
|
||||
test_no_handoff_on_invalid_ins (__main__.TestAtrHuntDetect.test_no_handoff_on_invalid_ins) ... ok
|
||||
test_truncated_atr_ta_is_clean (__main__.TestAtrHuntDetect.test_truncated_atr_ta_is_clean) ... ok
|
||||
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
||||
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
||||
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
||||
@@ -360,6 +361,7 @@ test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recov
|
||||
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
||||
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
||||
test_seeded_bit_samples_does_not_block_recovery (__main__.TestAtrHuntEscape.test_seeded_bit_samples_does_not_block_recovery) ... ok
|
||||
test_9e_9f_are_status_words (__main__.TestDecodeStep.test_9e_9f_are_status_words) ... ok
|
||||
test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_dense_valid_cla_is_not_idle_noise) ... ok
|
||||
test_data_0x3b_before_traffic_is_atr_candidate (__main__.TestDecodeStep.test_data_0x3b_before_traffic_is_atr_candidate) ... ok
|
||||
test_data_0x3b_is_not_an_atr_after_traffic (__main__.TestDecodeStep.test_data_0x3b_is_not_an_atr_after_traffic) ... ok
|
||||
@@ -371,6 +373,7 @@ test_immediate_sw_rejection_frames (__main__.TestDecodeStep.test_immediate_sw_re
|
||||
test_invalid_ins_re_frame_recovers_command (__main__.TestDecodeStep.test_invalid_ins_re_frame_recovers_command) ... ok
|
||||
test_inverse_ts_raw_pattern_is_an_atr_candidate (__main__.TestDecodeStep.test_inverse_ts_raw_pattern_is_an_atr_candidate) ... ok
|
||||
test_le_256_response_data_is_kept (__main__.TestDecodeStep.test_le_256_response_data_is_kept) ... ok
|
||||
test_parity_error_flags_bad_fcs (__main__.TestDecodeStep.test_parity_error_flags_bad_fcs) ... ok
|
||||
test_short_response_is_split_and_flagged (__main__.TestDecodeStep.test_short_response_is_split_and_flagged) ... ok
|
||||
test_short_response_replays_following_exchange (__main__.TestDecodeStep.test_short_response_replays_following_exchange) ... ok
|
||||
test_text_byte_boundary_is_discarded_not_framed (__main__.TestDecodeStep.test_text_byte_boundary_is_discarded_not_framed) ... ok
|
||||
@@ -430,11 +433,21 @@ test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall
|
||||
Regression test for Samsung 16 CLK/ETU concatenation. ... ok
|
||||
test_rejects_phantom_sub_etu_fall (__main__.TestNextStartFall.test_rejects_phantom_sub_etu_fall) ... ok
|
||||
test_returns_none_when_no_fall (__main__.TestNextStartFall.test_returns_none_when_no_fall) ... ok
|
||||
test_etu_glitch_floor_scales (__main__.TestSamplerateScaling.test_etu_glitch_floor_scales) ... ok
|
||||
test_samples_for_us (__main__.TestSamplerateScaling.test_samples_for_us) ... ok
|
||||
test_advancing_sample_is_not_stall (__main__.TestStallCheck.test_advancing_sample_is_not_stall) ... ok
|
||||
test_none_pins_is_eof (__main__.TestStallCheck.test_none_pins_is_eof) ... ok
|
||||
test_repeated_same_sample_is_bounded_stall (__main__.TestStallCheck.test_repeated_same_sample_is_bounded_stall) ... ok
|
||||
test_untracked_level_wait_never_stalls (__main__.TestStallCheck.test_untracked_level_wait_never_stalls) ... ok
|
||||
test_untracked_resets_stall_window (__main__.TestStallCheck.test_untracked_resets_stall_window) ... ok
|
||||
test_decode_step_reads_exactly_one_block (__main__.TestT1Blocks.test_decode_step_reads_exactly_one_block) ... ok
|
||||
test_handle_pps_truncated_is_clean (__main__.TestT1Blocks.test_handle_pps_truncated_is_clean) ... ok
|
||||
test_handle_pps_truncated_optional_is_clean (__main__.TestT1Blocks.test_handle_pps_truncated_optional_is_clean) ... ok
|
||||
test_i_block (__main__.TestT1Blocks.test_i_block) ... ok
|
||||
test_m_chain_concatenates_inf (__main__.TestT1Blocks.test_m_chain_concatenates_inf) ... ok
|
||||
test_r_and_s_blocks_have_no_inf (__main__.TestT1Blocks.test_r_and_s_blocks_have_no_inf) ... ok
|
||||
test_truncated_block_before_lrc_is_clean (__main__.TestT1Blocks.test_truncated_block_before_lrc_is_clean) ... ok
|
||||
test_truncated_inf_is_clean (__main__.TestT1Blocks.test_truncated_inf_is_clean) ... ok
|
||||
test_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
||||
test_invalid_cla_reserved_and_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_reserved_and_unseen) ... ok
|
||||
test_invalid_cla_untraced_variants (__main__.TestValidateT0Apdu.test_invalid_cla_untraced_variants) ... ok
|
||||
@@ -457,6 +470,6 @@ test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_vari
|
||||
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 117 tests in 0.026s
|
||||
Ran 130 tests in 0.029s
|
||||
|
||||
OK
|
||||
+169
-3
@@ -266,7 +266,6 @@ class TestInverseConvention(unittest.TestCase):
|
||||
def test_wait_data_falling_uses_high_idle_and_falling_start(self):
|
||||
self.inst._inverse_convention = True
|
||||
self.inst.bit_samples = 100
|
||||
self.inst._edge_read = True
|
||||
self.inst._min_pulse_samples = 0
|
||||
self.inst.samplenum = 0
|
||||
conds = []
|
||||
@@ -284,6 +283,130 @@ class TestInverseConvention(unittest.TestCase):
|
||||
self.assertEqual(self.inst._start_fall, self.inst.samplenum)
|
||||
|
||||
|
||||
class TestT1Blocks(unittest.TestCase):
|
||||
"""T=1 block parsing: NAD/PCB/LEN/INF/LRC, the M-bit chain, and the
|
||||
EOF guards (a truncated block never raises or emits a half INF)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def _make(self, byte_seq):
|
||||
inst = object.__new__(self.pd.Decoder)
|
||||
inst.reset()
|
||||
inst.samplenum = 0
|
||||
inst.out_ann = 0
|
||||
inst.log = lambda *a, **k: None
|
||||
inst.put = lambda *a, **k: None
|
||||
inst.wait = lambda cond=None: [1] * 8
|
||||
inst._bytes = collections.deque(byte_seq)
|
||||
inst.read_byte = lambda: (inst._bytes.popleft()
|
||||
if inst._bytes else None)
|
||||
return inst
|
||||
|
||||
def _block(self, nad, pcb, inf):
|
||||
body = [nad, pcb, len(inf)] + list(inf)
|
||||
lrc = 0
|
||||
for b in body:
|
||||
lrc ^= b
|
||||
return body + [lrc]
|
||||
|
||||
def test_i_block(self):
|
||||
inst = self._make(self._block(0x00, 0x00, [0x6f, 0x00]))
|
||||
self.assertEqual(inst.t1_parse_block(0), (True, [0x6f, 0x00]))
|
||||
|
||||
def test_r_and_s_blocks_have_no_inf(self):
|
||||
inst = self._make(self._block(0x00, 0x80, []))
|
||||
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||
inst = self._make(self._block(0x00, 0xc0, []))
|
||||
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||
|
||||
def test_m_chain_concatenates_inf(self):
|
||||
seq = (self._block(0x00, 0x20, [0x01])
|
||||
+ self._block(0x00, 0x00, [0x02]))
|
||||
inst = self._make(seq)
|
||||
self.assertEqual(inst.t1_parse_block(0), (True, [0x01, 0x02]))
|
||||
|
||||
def test_truncated_block_before_lrc_is_clean(self):
|
||||
seq = self._block(0x00, 0x00, [0x6f, 0x00])[:5] # no LRC
|
||||
inst = self._make(seq)
|
||||
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||
|
||||
def test_truncated_inf_is_clean(self):
|
||||
inst = self._make([0x00, 0x00, 0x05, 0x01, 0x02])
|
||||
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||
|
||||
def test_decode_step_reads_exactly_one_block(self):
|
||||
# The T=1 caller used to run an extra chain loop after every
|
||||
# I-block: it consumed the peer's R-block and merged the following
|
||||
# I-block's INF into this packet. One step must parse one block.
|
||||
apdu = [0x00, 0xa4, 0x00, 0x00, 0x00]
|
||||
rblock = self._block(0x00, 0x80, [])
|
||||
inst = self._make(self._block(0x00, 0x00, apdu) + rblock)
|
||||
inst.state = 'DATA'
|
||||
inst.hasT0 = False
|
||||
inst.hasT1 = True
|
||||
inst.bit_samples = 100
|
||||
inst._pps_speed_changed = False
|
||||
inst._resync = False
|
||||
inst._at_eof = lambda: False
|
||||
inst.out_ann = 0
|
||||
inst.emitted = []
|
||||
inst.emit_packet = lambda st, pl, ss, es, flags=0: \
|
||||
inst.emitted.append((bytes(pl), flags))
|
||||
inst._note_discard = lambda *a, **k: None
|
||||
inst._note_pre_sw = lambda *a, **k: None
|
||||
inst._consume_first = lambda: (inst._bytes.popleft()
|
||||
if inst._bytes else None)
|
||||
inst.peek_byte = lambda: inst._bytes[0] if inst._bytes else None
|
||||
self.assertTrue(inst.decode_step())
|
||||
self.assertEqual(len(inst.emitted), 1)
|
||||
self.assertEqual(inst.emitted[0][0], bytes(apdu))
|
||||
# The peer's R-block is left for the next step.
|
||||
self.assertEqual(list(inst._bytes), rblock)
|
||||
|
||||
def test_handle_pps_truncated_is_clean(self):
|
||||
inst = self._make([0x55])
|
||||
inst.peeked_samplenum = 0
|
||||
inst.handle_pps() # pps0 missing: must not raise
|
||||
|
||||
def test_handle_pps_truncated_optional_is_clean(self):
|
||||
inst = self._make([0x55, 0x70]) # PPS0 asks for PPS1..3, then EOF
|
||||
inst.peeked_samplenum = 0
|
||||
inst.handle_pps() # PPS1 missing: must not raise
|
||||
|
||||
|
||||
class TestSamplerateScaling(unittest.TestCase):
|
||||
"""The pulse/glitch thresholds scale with the capture rate instead of
|
||||
assuming 16 MHz."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def test_samples_for_us(self):
|
||||
inst = object.__new__(self.pd.Decoder)
|
||||
inst.samplerate = 16000000
|
||||
self.assertEqual(inst._samples_for_us(0.125), 2)
|
||||
self.assertEqual(inst._samples_for_us(0.25), 4)
|
||||
self.assertEqual(inst._samples_for_us(0.75), 12)
|
||||
inst.samplerate = 96000000
|
||||
self.assertEqual(inst._samples_for_us(0.125), 12)
|
||||
self.assertEqual(inst._samples_for_us(0.75), 72)
|
||||
inst.samplerate = None
|
||||
self.assertEqual(inst._samples_for_us(0.75), 12) # 16 MHz fallback
|
||||
|
||||
def test_etu_glitch_floor_scales(self):
|
||||
inst = object.__new__(self.pd.Decoder)
|
||||
inst.samplerate = 96000000
|
||||
# 12-sample spacings are sub-ETU glitches at 96 MHz (floor 72).
|
||||
self.assertEqual(inst._etu_candidate([(12, 0), (12, 1)]),
|
||||
(None, False))
|
||||
inst.samplerate = 16000000
|
||||
self.assertEqual(inst._etu_candidate([(12, 0), (12, 1)]),
|
||||
(12, True))
|
||||
|
||||
|
||||
class TestValidateT0Apdu(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -386,10 +509,15 @@ class TestValidateT0Apdu(unittest.TestCase):
|
||||
# Table 4 pairs (0F ERASE BINARY, 21 VERIFY, 87 GENERAL
|
||||
# AUTHENTICATE, A1 SEARCH BINARY, B1 READ BINARY, B3 READ RECORD,
|
||||
# C3 ENVELOPE, CB GET DATA, D1 WRITE BINARY, D7 UPDATE BINARY,
|
||||
# DB PUT DATA, DD UPDATE RECORD) plus TS 102 221 AUTHENTICATE 89.
|
||||
# DB PUT DATA, DD UPDATE RECORD), TS 102 221 AUTHENTICATE 89, and
|
||||
# TS 102 221 Table 10.5 MANAGE SECURE CHANNEL 73 / TRANSACT DATA 75
|
||||
# (the v1.9.6 audit found these two missing).
|
||||
self.assertEqual(self.pd.ODD_INS, frozenset((
|
||||
0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0x0F, 0x21, 0x73, 0x75, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD)))
|
||||
for ins in (0x73, 0x75):
|
||||
with self.subTest(ins=ins):
|
||||
self.assertTrue(self.pd.plausible_ins(ins))
|
||||
|
||||
def test_vs_reader_mirror(self):
|
||||
# tools/vs_reader.py duplicates the acceptance predicates for its
|
||||
@@ -968,6 +1096,36 @@ class TestDecodeStep(unittest.TestCase):
|
||||
inst.wait = lambda cond=None: [1] * 8
|
||||
return inst
|
||||
|
||||
def test_9e_9f_are_status_words(self):
|
||||
# ISO 7816-3 10.3.3 Table 11: '9X' is SW1. The old code treated
|
||||
# 9E/9F as an "extended-length" procedure byte and consumed SW2 as
|
||||
# a bogus length, losing the status word.
|
||||
for sw1 in (0x9e, 0x9f):
|
||||
with self.subTest(sw1=sw1):
|
||||
inst = self._make(bytes.fromhex('00a40000023f00')
|
||||
+ bytes([sw1, 0x00]))
|
||||
self.assertTrue(inst.decode_step())
|
||||
self.assertEqual(
|
||||
inst.emitted,
|
||||
[(bytes.fromhex('00a40000023f00') + bytes([sw1, 0x00]), 0)])
|
||||
|
||||
def test_parity_error_flags_bad_fcs(self):
|
||||
# A parity error is annotated and must also reach downstream as
|
||||
# GSMTAP_FLAG_BAD_FCS on the emitted packet.
|
||||
inst = self._make(bytes.fromhex('00a40000023f009000'))
|
||||
orig = inst.read_byte
|
||||
|
||||
def read():
|
||||
b = orig()
|
||||
if b == 0x3f:
|
||||
inst._parity_errors += 1
|
||||
return b
|
||||
|
||||
inst.read_byte = read
|
||||
self.assertTrue(inst.decode_step())
|
||||
self.assertEqual(len(inst.emitted), 1)
|
||||
self.assertTrue(inst.emitted[0][1] & self.pd.GSMTAP_FLAG_BAD_FCS)
|
||||
|
||||
def test_extended_channel_exchange_frames(self):
|
||||
# 'CX' STATUS on channel 4 (v1.9.3): the whole exchange must be
|
||||
# read and emitted clean.
|
||||
@@ -1222,6 +1380,14 @@ class TestAtrHuntDetect(unittest.TestCase):
|
||||
self.assertEqual(inst.emitted, [])
|
||||
self.assertEqual(inst.state, 'DATA')
|
||||
|
||||
def test_truncated_atr_ta_is_clean(self):
|
||||
# T0 says TA1 present but the capture ends: the parse must return
|
||||
# without committing an ATR (or raising on the None byte).
|
||||
inst = self._make([0x3B, 0x10])
|
||||
inst.handle_atr(None)
|
||||
self.assertFalse(inst._atr_parsed)
|
||||
self.assertEqual(inst.emitted, [])
|
||||
|
||||
def test_inverse_ts_pattern_is_parsed(self):
|
||||
# 0x03 + the raw inverse TS pattern must be committed as 0x3F and
|
||||
# switch the readers to the inverse convention.
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ def _plausible_sw(sw1):
|
||||
# values stay rejected (no command uses them; mis-framed boundary data
|
||||
# bytes with odd INS started bogus frames on the phone traces).
|
||||
ODD_INS = frozenset((
|
||||
0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0x0F, 0x21, 0x73, 0x75, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD,
|
||||
))
|
||||
|
||||
|
||||
Reference in new issue
Block a user