From 3bf5cee222de4beddf85a787e30783c25fa8b63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 27 Sep 2026 23:52:40 +0300 Subject: [PATCH] v1.9.2: PPS CLK upshift + exact CLK-edge projection (new reader fixture) - handle_pps(): flag the first post-PPS character; _read_byte_clk() re-measures the CLK period there and accounts the measurement's 16 edges. The Alcor reader upclocks 4.0 -> 4.8 MHz at PPS (FI/DI still 512/32), so the ATR measurement was stale: 64 vs 53.33 samples/ETU, no post-PPS APDU framed. - _wait_clk_rising(): project to the n-th edge (skip to target - half a period, take one edge) instead of "skip n-2 cycles + 2 edges", which lost ~1 edge per call on fractional periods and slid the samples ~0.5 ETU early by the parity bit. xiaomi 7698 -> 2, 4gmodem 7218 -> 0, S21p_sim2 394 -> 0 CHKSUM lines (previously misread as dirty-wire noise). - CONCAT gate: EMBEDDED_CLA excludes the structural 0x4x/0x5x classes (payload false hit on the Alcor secured packet); mirrored in tools/vs_reader.py and covered by a new vector in tests/test_gsmtap.py. - tools/capture_baseline.sh: add the alcor_reader_sample fixture; baseline regenerated (APDU counts and RESULT: OK unchanged everywhere, 0 CONCAT). - 75/75 unit tests. README/AGENTS updated. --- AGENTS.md | 51 +++++++++++++++++- README.md | 1 + pd.py | 105 +++++++++++++++++++++--------------- tests/baseline.txt | 108 +++++++++++++++++--------------------- tests/test_gsmtap.py | 11 ++++ tools/capture_baseline.sh | 7 +++ tools/vs_reader.py | 5 +- 7 files changed, 184 insertions(+), 104 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8febb5e..c524e45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ Every change to `pd.py` must be followed by running the full test suite: for trace in test_8_raw16 test_7_raw16 xiaomi_phone_sample \ samsung_phone_sample \ xiaomi_mi_a1_coldboot_sample 4gmodem_coldboot_sample \ + alcor_reader_sample \ samsung_A55_coldboot_new_cable samsung_A55_coldboot_new_cable_sim2 \ samsung_S21p_coldboot_new_cable samsung_S21p_coldboot_new_cable_sim2; do # Run sigrok-cli and vs_reader as shown below @@ -308,6 +309,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.2: PPS CLK upshift + exact CLK-edge projection (new reader support) + +Two timing bugs surfaced by `examples/alcor_reader_sample.sr`, a PC/SC reader +that changes the CLK frequency at PPS: 4.0 MHz at the ATR, 4.8 MHz after +(raise-to-raise 4.000 -> 3.333 samples at 16 MHz), with FI/DI still 512/32 = +16 CLK/bit. Before the fix the whole post-PPS session framed nothing. + +- **Stale samples-per-clock after PPS.** The native/CLK-sync path assumed + "the CLK period is unchanged by PPS" and kept the period measured at the + ATR, so the post-PPS ETU was 16 x 4.0 = 64 samples instead of the real + 16 x 3.333 = 53.33 -- every byte was mis-sampled. `handle_pps()` now sets + `_clk_remeasure_pending`; `_read_byte_clk()` consumes it in the first + post-PPS character (CLK is guaranteed to be running inside a frame), + re-runs `_measure_clock_period()` and accounts the measurement's 16 edges + as the first edges of the frame (`bit_samples` refreshed). +- **Edge-skip deficit on fractional periods.** `_wait_clk_rising()` used to + "skip (n - 2) cycles by samples, then take 2 edges". With a period that is + a fraction of a sample the skip under-covers by ~1 edge per call: simulated + against the real trace, 144 edges were consumed where 152 were requested, + so the samples slid ~0.5 ETU early by the parity bit -- in 513/513 CHKSUM + cases the "parity" sample read the last data bit. It now projects n + periods ahead of the edge it is sitting on, skips to (target - half a + period) and takes one edge: exact for any period, non-accumulating, one + wait() per call. Without a period estimate it waits each edge exactly. +- **CONCAT detector precision.** The Alcor GET RESPONSE payload (a + secured-packet byte run "40 70 70 61 15" + an SW-like byte 21 later) + false-hit the embedded-exchange scan through the structural 0x4x CLA that + `plausible_cla()` tolerates for resync robustness. The detector's CLA set + now excludes 0x4x/0x5x (no CAT traffic uses them; every tuned + true-positive vector uses 0x00/0x01/0x08/0x80), mirrored in + `tools/vs_reader.py` with a regression vector in `tests/test_gsmtap.py`. + +Effect on the suite (baseline regenerated; APDU counts and `RESULT: OK` +unchanged everywhere, 0 CONCAT): + +| trace | CHKSUM lines before | after | +|---|---|---| +| alcor_reader_sample (new fixture) | no framing at all (0 APDUs) | 2 (1 idle artifact), 48 APDUs | +| xiaomi_phone_sample | 7698 | 2 (1 event) | +| 4gmodem_coldboot | 7218 | 0 | +| samsung_S21p_coldboot_new_cable_sim2 | 394 | 0 | +| test_8 / test_7 / others | 0 | 0 | + +The xiaomi/4gmodem/S21p_sim2 counts had been read as dirty-wire parity +noise -- they were this decoder bug. `tools/capture_baseline.sh` gains the +alcor fixture (local-only, like every `examples/` trace). 75/75 unit tests. +VERSION -> 1.9.2. + ### v1.9.1: T=0 NULL is not an SW1 (fixes bogus "60 61" APDU endings) The "scan for SW1" loops accepted any byte with high nibble `0x6` as a @@ -704,7 +753,7 @@ append the measured CLK frequency when known ### Versioning -The decoder version is defined in `pd.py` as `VERSION = '1.9.1'`. +The decoder version is defined in `pd.py` as `VERSION = '1.9.2'`. The version is printed to the log on decoder startup. ### Testing after decoder changes diff --git a/README.md b/README.md index ad9337d..63d7eaf 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,7 @@ re-frame eliminated the last multi-exchange swallow family on | test_7 | constant | ~43 | 372 (standard) | Direct | PC/SC reader, warm resets | | xiaomi | gated/stopped | ~53 | recovered from DATA | Direct | Phone, mid-session | | samsung | gated/stopped | ~72 | 16 (F=512/D=32) | Direct | Phone, non-standard F/D, STK menu | +| alcor_reader | constant | ~53 | 16 (F=512/D=32) | Direct | PC/SC reader, CLK 4.0 MHz at ATR -> 4.8 MHz after PPS | The decoder auto-detects the ETU from the trace and selects the appropriate bit reader (CLK-sync for constant CLK, edge-list for gated/stopped CLK). diff --git a/pd.py b/pd.py index 06ffffb..caf5279 100644 --- a/pd.py +++ b/pd.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.1' +VERSION = '1.9.2' @@ -217,10 +217,15 @@ def is_desynced(packet): # data region, followed by its P3 data bytes and an interior status word. # These constants bound detector precision (empirically tuned on all 10 # example traces: 0 hits on clean traces, every Samsung/xiaomi family hit). -# A swallowed header's CLA is validated with the same structural rule as the -# decoder itself (see plausible_cla): interindustry classes including the -# logical-channel numbers 0x01-0x03 are legitimate first bytes of a command. -EMBEDDED_CLA = frozenset(cla for cla in range(256) if plausible_cla(cla)) +# A swallowed header's CLA is a real command class byte: interindustry +# classes including the logical-channel numbers 0x01-0x03. The structural +# 0x4x/0x5x forms that plausible_cla() tolerates for resync robustness are +# excluded here -- no CAT traffic uses them, and accepting them makes payload +# bytes false-hit (Alcor secured-packet payload: "40 70 70 61 15" + an +# SW-like byte 21 later). +EMBEDDED_CLA = frozenset( + cla for cla in range(256) + if plausible_cla(cla) and (cla & 0xF0) not in (0x40, 0x50)) # Commands that actually occur on these cards' bus (T=0 + STK/CAT). EMBEDDED_INS = frozenset(( 0x12, 0x14, 0x20, 0x44, 0x70, 0x88, @@ -535,6 +540,12 @@ class Decoder(srd.Decoder): # Forces _measure_etu() on the next decode_step() cycle so the # decoder re-locks to the new ETU from the live DATA line. self._pps_speed_changed = False + # Set by handle_pps() on the native path: some readers also change + # the CLK frequency at the PPS speed switch (Alcor reader: 4.0 -> + # 4.8 MHz with FI/DI 512/32), which invalidates the samples-per-clock + # period measured at the ATR. _read_byte_clk() re-measures it inside + # the first post-PPS character, where CLK is guaranteed to be running. + self._clk_remeasure_pending = False # Count of times the ATR hunt failed to find a valid TS byte -- # bounds the "keep hunting" loop so a mid-session sniffer (no ATR # ever present) eventually falls back to T=0 parsing instead of @@ -1208,28 +1219,33 @@ class Decoder(srd.Decoder): '''Wait for n CLK rising edges. Returns False on end-of-capture. CLK runs continuously for the whole of a character (Clock Stop only - happens between characters), so within a byte it is safe to skip most - of the way by samples and then land on the exact rising edge. This - keeps the edge-exactness of CLK-edge timing without paying one wait() - per CLK cycle. Lands within +/-1 CLK cycle of the target edge, which - is far inside the stable region of a bit (a bit is held for the whole - ETU).''' + happens between characters). The n-th edge is found by projecting + n periods ahead of the edge we are sitting on and landing on the + nearest CLK edge: skip to (target - half a period), then take one + edge. The projection keeps the edge count exact for ANY clock + period -- "skip (n - 2) cycles, then take 2 edges" loses ~1 edge per + call when the period is a fraction of a sample (e.g. 3.333 samples + at 4.8 MHz / 16 MHz), and the deficit accumulates across the byte + (the parity sample then reads the last data bit). Without a period + estimate, fall back to waiting each edge exactly.''' if n <= 0: return True - if self._samples_per_clock and n > 2: - # Skip (n - 2) cycles by samples, then take the last 2 edges - # exactly. Undershooting by a cycle is harmless (the bit is - # stable for the full ETU), and the final two edge waits re-align - # us to a genuine CLK edge. - skip = int((n - 2) * self._samples_per_clock) + spc = self._samples_per_clock + if spc: + target = self.samplenum + n * spc + skip = int(target - 0.5 * spc - self.samplenum) if skip > 0: self.wait({'skip': skip}) - n = 2 - for _ in range(n): - pins = self.wait({self.CLK_IDX: 'r'}) - if pins is None: - self._eof = True - return False + else: + for _ in range(n - 1): + pins = self.wait({self.CLK_IDX: 'r'}) + if pins is None: + self._eof = True + return False + pins = self.wait({self.CLK_IDX: 'r'}) + if pins is None: + self._eof = True + return False return True def _read_byte_clk(self): @@ -1257,6 +1273,16 @@ class Decoder(srd.Decoder): bs = self.clock_skip self.bits = [0] # start bit (always low) prev_edges = 0 + if self._clk_remeasure_pending: + # A PPS may also change the CLK frequency (see handle_pps). + # CLK runs for the whole frame here; the measurement consumes + # exactly 16 rising edges, accounted as the first edges of the + # frame so the bit loop stays edge-exact. + self._clk_remeasure_pending = False + self._measure_clock_period() + if not self._eof: + prev_edges = 16 + self.bit_samples = self._compute_bit_samples() for i in range(9): # bit centres at (1.5 + i) ETU from the start fall total = int(round((1.5 + i) * bs)) @@ -2041,28 +2067,17 @@ class Decoder(srd.Decoder): self.di = self.baud_rate.get(int(pps1 & 0x0F), 1) self.clock_skip = max(int(self.fi // self.di), 1) self.log("PPS accepted: FI", self.fi, "DI", self.di, "clock_skip", self.clock_skip) - # PPS changed the bit rate. How we follow it depends on the reader: - # - # - Native CLK-sync reader (_use_clk_sync): clock_skip is the ONLY - # timing input -- the CLK period is unchanged by PPS, so the new - # ETU is exactly clock_skip * samples_per_clock. No DATA - # re-measurement is needed (and none should happen: consuming a - # burst here would silently drop the first post-PPS command). - # Just refresh bit_samples for wait_data_falling / edge fallback. - # - # - Edge-list reader (non-native modes): there is no usable CLK - # reference, so re-measure the ETU from the live DATA line on - # the next decode cycle; that burst is emitted flagged, not - # silently consumed. # PPS changed the bit rate. How we follow it depends on the mode: # - # - Native CLK mode (real CLK channel): after an ACCEPTED PPS the - # card and terminal switch to FI/DI CLK cycles per bit, and the - # CLK period itself is unchanged. So clock_skip = FI/DI is - # exact -- no DATA re-measurement is needed (and none should - # happen: consuming a burst here would silently drop the first - # post-PPS command). Just refresh bit_samples for - # wait_data_falling / the edge-list fallback. + # - Native CLK mode (real CLK channel): clock_skip = FI/DI is + # protocol-defined, so the new ETU is clock_skip * + # samples_per_clock -- no DATA re-measurement is needed (and + # none should happen here: consuming a burst at this point + # would silently drop the first post-PPS command). But some + # readers also change the CLK FREQUENCY at the speed switch + # (Alcor reader: 4.0 -> 4.8 MHz with FI/DI 512/32), which + # invalidates the samples-per-clock measured at the ATR; the + # first post-PPS character re-measures it (see _read_byte_clk). # # - sample_as_clock / detect modes: there is no usable CLK # reference, so re-measure the ETU from the live DATA line on @@ -2073,6 +2088,10 @@ class Decoder(srd.Decoder): self._pps_speed_changed = False # Re-hunt for the next start bit after the PPS response. self._start_fall = None + # Some readers also change the CLK frequency at the switch + # (Alcor 4.0 -> 4.8 MHz with FI/DI 512/32); re-measure it in + # the first post-PPS character, where CLK is running. + self._clk_remeasure_pending = True else: self._pps_speed_changed = True self.bit_samples = None diff --git a/tests/baseline.txt b/tests/baseline.txt index 07292f0..f94db09 100644 --- a/tests/baseline.txt +++ b/tests/baseline.txt @@ -1,7 +1,7 @@ -=== Baseline captured 2026-09-19T00:47:38+03:00 === +=== Baseline captured 2026-09-27T23:45:58+03:00 === === test_8 === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('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.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('etu recovered (samples):', 1236) iso7816-1: RST deasserted ('RST deasserted',) @@ -77,27 +77,11 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === xiaomi_phone_sample === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('etu recovered (samples):', 53) -(11673977, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1]) +(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] -(11695589, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 0, 0, 0, 0, 0, 0] -(11697509, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 1, 1]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 1, 1] -(11698149, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] -(11698789, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 0, 0, 0, 0, 1, 1]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 0, 0, 0, 0, 1, 1] -(11699429, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 1, 1]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 1, 1] -(11701349, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 0, 0, 0, 0, 0, 0] -(11701989, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 1, 0, 0, 1, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 1, 0, 0, 1, 0, 0] -(11709030, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] -CHKSUM ERROR count: 7698 +CHKSUM ERROR count: 2 CONCAT APDU count: 0 reader exchanges : 0 capture ATRs : 0 @@ -118,7 +102,7 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === xiaomi_mi_a1_coldboot === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) @@ -145,27 +129,13 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === 4gmodem_coldboot === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) ('RST deasserted (hunt/ATR already in progress)',) ('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') -(10083201, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] -(10084089, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] -(32207790, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 0, 0, 1, 1, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 0, 0, 1, 1, 0, 0] -(32214835, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 1, 0, 1, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 1, 0, 1, 0, 0, 0] -(32216580, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 0, 0, 0, 0, 1, 1, 1]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 0, 0, 0, 0, 1, 1, 1] -(32219038, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 1, 1]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 1, 1] -(32219857, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] -CHKSUM ERROR count: 7218 +CHKSUM ERROR count: 0 CONCAT APDU count: 0 reader exchanges : 0 capture ATRs : 1 @@ -185,8 +155,41 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) +=== alcor_reader_sample === +('=== iso7816 decoder v1.9.2 ===',) +('RST low at start: expecting cold-boot ATR',) +iso7816-1: RST deasserted +('RST deasserted',) +('RST deasserted (hunt/ATR already in progress)',) +('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') +(26904874, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 1, 1, 1, 1, 1]) +iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 1, 1, 1, 1, 1] +iso7816-1: RST deasserted +('RST deasserted',) +('RST deasserted (hunt/ATR already in progress)',) +('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') +CHKSUM ERROR count: 2 +CONCAT APDU count: 0 +reader exchanges : 0 +capture ATRs : 2 +capture APDUs : 48 +byte-exact matches (LCS) : 0 +reader exchanges not in capture (pre-capture / dropped): 0 +capture APDUs not in reader log: + - proactive/CAT (unlogged by reader SDK): 36 + - valid exchanges (reader retries/dupes): 12 + - GARBAGE (mis-framed/desync): 0 + +Degradation metrics: + - BAD_FCS (desynced by decoder): 0 + - CONCAT (multi-exchange swallowed in one packet): 0 + - payload mismatches (LCS-matched, bytes differ): 0 + - suspicious CLA: 0 + +RESULT: OK - capture fully explained (reader + CAT + valid retries) + === samsung_A55_coldboot_new_cable === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) @@ -213,7 +216,7 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === samsung_A55_coldboot_new_cable_sim2 === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) @@ -240,7 +243,7 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === samsung_S21p_coldboot_new_cable === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) @@ -267,27 +270,13 @@ Degradation metrics: RESULT: OK - capture fully explained (reader + CAT + valid retries) === samsung_S21p_coldboot_new_cable_sim2 === -('=== iso7816 decoder v1.9.1 ===',) +('=== iso7816 decoder v1.9.2 ===',) ('RST low at start: expecting cold-boot ATR',) iso7816-1: RST deasserted ('RST deasserted',) ('RST deasserted (hunt/ATR already in progress)',) ('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') -(72333388, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 1, 0, 1, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 1, 0, 1, 0, 0, 0] -(72394723, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 0, 0, 0, 0, 0, 0] -(72424969, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] -(72429512, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 0, 1, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 0, 1, 0, 0, 0] -(72487445, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] -(72631364, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 0, 1, 0, 0, 0, 0, 0] -(98955516, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) -iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] -CHKSUM ERROR count: 394 +CHKSUM ERROR count: 0 CONCAT APDU count: 0 reader exchanges : 0 capture ATRs : 1 @@ -381,6 +370,7 @@ test_clean_atr_fragment_select (__main__.TestEmbeddedExchanges.test_clean_atr_fr test_clean_channel1_select_after_manage_channel (__main__.TestEmbeddedExchanges.test_clean_channel1_select_after_manage_channel) ... ok test_clean_get_response_fcp (__main__.TestEmbeddedExchanges.test_clean_get_response_fcp) ... ok test_clean_read_record_ff_run (__main__.TestEmbeddedExchanges.test_clean_read_record_ff_run) ... ok +test_clean_secured_packet_payload_alcor (__main__.TestEmbeddedExchanges.test_clean_secured_packet_payload_alcor) ... ok test_clean_select_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... ok test_concat_packets_are_not_is_desynced (__main__.TestEmbeddedExchanges.test_concat_packets_are_not_is_desynced) ... ok @@ -425,6 +415,6 @@ test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok ---------------------------------------------------------------------- -Ran 74 tests in 0.027s +Ran 75 tests in 0.024s OK diff --git a/tests/test_gsmtap.py b/tests/test_gsmtap.py index 953c968..c87e669 100644 --- a/tests/test_gsmtap.py +++ b/tests/test_gsmtap.py @@ -402,6 +402,17 @@ class TestEmbeddedExchanges(unittest.TestCase): # ATR-recovery fragment (header + 3F 00 + 6125) must not hit. self.assertEqual(self._hits('00a40004023f006125'), []) + def test_clean_secured_packet_payload_alcor(self): + # alcor_reader[46] (n=87): GET RESPONSE whose 80-byte payload holds a + # secured-packet run "40 70 70 61 15" 21 bytes before an SW-like byte; + # the reader follows with 00 c0 00 00 19 -- a clean single exchange. + # The structural 0x4x CLA must not turn payload bytes into a hit. + h = ('80c2000050d14e82028381060291978b4440048111227ff64070706115' + '3500350270000030151601252500000018ed15e8476126aa9cacbbe8f4' + '735f962016e3c6e30a26af20c02bad2a34bdf49f152e263bc7390a6119') + self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0]) + self.assertEqual(self._hits(h), []) + def test_a55_u1_concat(self): # A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix. h = ('000402a43f00612501c0000025c062238202782183023f00a5098001' diff --git a/tools/capture_baseline.sh b/tools/capture_baseline.sh index 90c9a8d..dd48d34 100755 --- a/tools/capture_baseline.sh +++ b/tools/capture_baseline.sh @@ -79,6 +79,13 @@ run_test "xiaomi_mi_a1_coldboot" timeout 300 sigrok-cli -i examples/xiaomi_mi_a1 # 4gmodem_coldboot_sample — same pre-power-up capture shape, modem SIM. run_test "4gmodem_coldboot" timeout 300 sigrok-cli -i examples/4gmodem_coldboot_sample.sr -P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:protocol=T=0:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=/tmp/out.pcap -A iso7816 +# alcor_reader_sample — PC/SC reader (Alcor). Regression fixture for the PPS +# clock upshift: CLK is 4.0 MHz at the ATR but 4.8 MHz after PPS (FI/DI +# 512/32 -> 16 CLK/bit = 3.333 samples/CLK at 16 MHz), so the samples-per-clock +# measured at the ATR goes stale; the first post-PPS character must re-measure +# it or no post-PPS APDU frames. +run_test "alcor_reader_sample" timeout 300 sigrok-cli -i examples/alcor_reader_sample.sr -P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:rst_detect=true:gsmtap_enable=false:pcap_file=/tmp/out.pcap -A iso7816 + # samsung_A55_coldboot_new_cable — Samsung A55 with the new short cable # (30 mm wires, external pull resistors GND->VCC and VCC->DATA), named # channels DATA/VCC/RST/CLK on bits 0/2/4/5. diff --git a/tools/vs_reader.py b/tools/vs_reader.py index 122b3b1..c3f7d60 100644 --- a/tools/vs_reader.py +++ b/tools/vs_reader.py @@ -65,7 +65,10 @@ MAX_TPDU_LEN = 271 # data region, followed by its P3 data bytes and an interior status word. # These constants bound detector precision (empirically tuned on all 10 # example traces: 0 hits on clean traces, every Samsung/xiaomi family hit). -EMBEDDED_CLA = VALID_CLA +# Mirror of pd.py: real command classes only -- the structural 0x4x/0x5x +# forms that _plausible_cla() tolerates only add payload false hits. +EMBEDDED_CLA = frozenset( + cla for cla in VALID_CLA if (cla & 0xF0) not in (0x40, 0x50)) # Commands that actually occur on these cards' bus (T=0 + STK/CAT). EMBEDDED_INS = frozenset(( 0x12, 0x14, 0x20, 0x44, 0x70, 0x88,