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.
This commit is contained in:
catarrh committed 2026-09-27 23:52:40 +03:00
1 parent 18ee720fda
commit 3bf5cee222
7 files changed
+184 -104

No files matched your search

+50 -1
View File
@@ -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 \ for trace in test_8_raw16 test_7_raw16 xiaomi_phone_sample \
samsung_phone_sample \ samsung_phone_sample \
xiaomi_mi_a1_coldboot_sample 4gmodem_coldboot_sample \ xiaomi_mi_a1_coldboot_sample 4gmodem_coldboot_sample \
alcor_reader_sample \
samsung_A55_coldboot_new_cable samsung_A55_coldboot_new_cable_sim2 \ samsung_A55_coldboot_new_cable samsung_A55_coldboot_new_cable_sim2 \
samsung_S21p_coldboot_new_cable samsung_S21p_coldboot_new_cable_sim2; do samsung_S21p_coldboot_new_cable samsung_S21p_coldboot_new_cable_sim2; do
# Run sigrok-cli and vs_reader as shown below # 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 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. `.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) ### 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 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 ### 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. The version is printed to the log on decoder startup.
### Testing after decoder changes ### Testing after decoder changes
+1
View File
@@ -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 | | test_7 | constant | ~43 | 372 (standard) | Direct | PC/SC reader, warm resets |
| xiaomi | gated/stopped | ~53 | recovered from DATA | Direct | Phone, mid-session | | 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 | | 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 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). bit reader (CLK-sync for constant CLK, edge-list for gated/stopped CLK).
+62 -43
View File
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT, GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
GSMTAP_FLAG_BAD_FCS) 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. # data region, followed by its P3 data bytes and an interior status word.
# These constants bound detector precision (empirically tuned on all 10 # These constants bound detector precision (empirically tuned on all 10
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit). # 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 # A swallowed header's CLA is a real command class byte: interindustry
# decoder itself (see plausible_cla): interindustry classes including the # classes including the logical-channel numbers 0x01-0x03. The structural
# logical-channel numbers 0x01-0x03 are legitimate first bytes of a command. # 0x4x/0x5x forms that plausible_cla() tolerates for resync robustness are
EMBEDDED_CLA = frozenset(cla for cla in range(256) if plausible_cla(cla)) # 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). # Commands that actually occur on these cards' bus (T=0 + STK/CAT).
EMBEDDED_INS = frozenset(( EMBEDDED_INS = frozenset((
0x12, 0x14, 0x20, 0x44, 0x70, 0x88, 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 # Forces _measure_etu() on the next decode_step() cycle so the
# decoder re-locks to the new ETU from the live DATA line. # decoder re-locks to the new ETU from the live DATA line.
self._pps_speed_changed = False 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 -- # 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 # bounds the "keep hunting" loop so a mid-session sniffer (no ATR
# ever present) eventually falls back to T=0 parsing instead of # 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. '''Wait for n CLK rising edges. Returns False on end-of-capture.
CLK runs continuously for the whole of a character (Clock Stop only 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 happens between characters). The n-th edge is found by projecting
of the way by samples and then land on the exact rising edge. This n periods ahead of the edge we are sitting on and landing on the
keeps the edge-exactness of CLK-edge timing without paying one wait() nearest CLK edge: skip to (target - half a period), then take one
per CLK cycle. Lands within +/-1 CLK cycle of the target edge, which edge. The projection keeps the edge count exact for ANY clock
is far inside the stable region of a bit (a bit is held for the whole period -- "skip (n - 2) cycles, then take 2 edges" loses ~1 edge per
ETU).''' 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: if n <= 0:
return True return True
if self._samples_per_clock and n > 2: spc = self._samples_per_clock
# Skip (n - 2) cycles by samples, then take the last 2 edges if spc:
# exactly. Undershooting by a cycle is harmless (the bit is target = self.samplenum + n * spc
# stable for the full ETU), and the final two edge waits re-align skip = int(target - 0.5 * spc - self.samplenum)
# us to a genuine CLK edge.
skip = int((n - 2) * self._samples_per_clock)
if skip > 0: if skip > 0:
self.wait({'skip': skip}) self.wait({'skip': skip})
n = 2 else:
for _ in range(n): for _ in range(n - 1):
pins = self.wait({self.CLK_IDX: 'r'}) pins = self.wait({self.CLK_IDX: 'r'})
if pins is None: if pins is None:
self._eof = True self._eof = True
return False return False
pins = self.wait({self.CLK_IDX: 'r'})
if pins is None:
self._eof = True
return False
return True return True
def _read_byte_clk(self): def _read_byte_clk(self):
@@ -1257,6 +1273,16 @@ class Decoder(srd.Decoder):
bs = self.clock_skip bs = self.clock_skip
self.bits = [0] # start bit (always low) self.bits = [0] # start bit (always low)
prev_edges = 0 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): for i in range(9):
# bit centres at (1.5 + i) ETU from the start fall # bit centres at (1.5 + i) ETU from the start fall
total = int(round((1.5 + i) * bs)) 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.di = self.baud_rate.get(int(pps1 & 0x0F), 1)
self.clock_skip = max(int(self.fi // self.di), 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) 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: # PPS changed the bit rate. How we follow it depends on the mode:
# #
# - Native CLK mode (real CLK channel): after an ACCEPTED PPS the # - Native CLK mode (real CLK channel): clock_skip = FI/DI is
# card and terminal switch to FI/DI CLK cycles per bit, and the # protocol-defined, so the new ETU is clock_skip *
# CLK period itself is unchanged. So clock_skip = FI/DI is # samples_per_clock -- no DATA re-measurement is needed (and
# exact -- no DATA re-measurement is needed (and none should # none should happen here: consuming a burst at this point
# happen: consuming a burst here would silently drop the first # would silently drop the first post-PPS command). But some
# post-PPS command). Just refresh bit_samples for # readers also change the CLK FREQUENCY at the speed switch
# wait_data_falling / the edge-list fallback. # (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 # - sample_as_clock / detect modes: there is no usable CLK
# reference, so re-measure the ETU from the live DATA line on # 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 self._pps_speed_changed = False
# Re-hunt for the next start bit after the PPS response. # Re-hunt for the next start bit after the PPS response.
self._start_fall = None 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: else:
self._pps_speed_changed = True self._pps_speed_changed = True
self.bit_samples = None self.bit_samples = None
+49 -59
View File
@@ -1,7 +1,7 @@
=== Baseline captured 2026-09-19T00:47:38+03:00 === === Baseline captured 2026-09-27T23:45:58+03:00 ===
=== test_8 === === test_8 ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('etu recovered (samples):', 1237) ('etu recovered (samples):', 1237)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -36,7 +36,7 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== test_7 === === test_7 ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('etu recovered (samples):', 1236) ('etu recovered (samples):', 1236)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -77,27 +77,11 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== xiaomi_phone_sample === === xiaomi_phone_sample ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('etu recovered (samples):', 53) ('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] 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]) CHKSUM ERROR count: 2
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
CONCAT APDU count: 0 CONCAT APDU count: 0
reader exchanges : 0 reader exchanges : 0
capture ATRs : 0 capture ATRs : 0
@@ -118,7 +102,7 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== xiaomi_mi_a1_coldboot === === xiaomi_mi_a1_coldboot ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -145,27 +129,13 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== 4gmodem_coldboot === === 4gmodem_coldboot ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
('RST deasserted (hunt/ATR already in progress)',) ('RST deasserted (hunt/ATR already in progress)',)
('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') ('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e')
(10083201, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) CHKSUM ERROR count: 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
CONCAT APDU count: 0 CONCAT APDU count: 0
reader exchanges : 0 reader exchanges : 0
capture ATRs : 1 capture ATRs : 1
@@ -185,8 +155,41 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) 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 === === samsung_A55_coldboot_new_cable ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -213,7 +216,7 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== samsung_A55_coldboot_new_cable_sim2 === === samsung_A55_coldboot_new_cable_sim2 ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -240,7 +243,7 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== samsung_S21p_coldboot_new_cable === === samsung_S21p_coldboot_new_cable ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
@@ -267,27 +270,13 @@ Degradation metrics:
RESULT: OK - capture fully explained (reader + CAT + valid retries) RESULT: OK - capture fully explained (reader + CAT + valid retries)
=== samsung_S21p_coldboot_new_cable_sim2 === === samsung_S21p_coldboot_new_cable_sim2 ===
('=== iso7816 decoder v1.9.1 ===',) ('=== iso7816 decoder v1.9.2 ===',)
('RST low at start: expecting cold-boot ATR',) ('RST low at start: expecting cold-boot ATR',)
iso7816-1: RST deasserted iso7816-1: RST deasserted
('RST deasserted',) ('RST deasserted',)
('RST deasserted (hunt/ATR already in progress)',) ('RST deasserted (hunt/ATR already in progress)',)
('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e') ('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e')
(72333388, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 1, 0, 1, 0, 0, 0]) CHKSUM ERROR count: 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
CONCAT APDU count: 0 CONCAT APDU count: 0
reader exchanges : 0 reader exchanges : 0
capture ATRs : 1 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_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_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_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_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok
test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... 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 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 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 OK
+11
View File
@@ -402,6 +402,17 @@ class TestEmbeddedExchanges(unittest.TestCase):
# ATR-recovery fragment (header + 3F 00 + 6125) must not hit. # ATR-recovery fragment (header + 3F 00 + 6125) must not hit.
self.assertEqual(self._hits('00a40004023f006125'), []) 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): def test_a55_u1_concat(self):
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix. # A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001' h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
+7
View File
@@ -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. # 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 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 # samsung_A55_coldboot_new_cable — Samsung A55 with the new short cable
# (30 mm wires, external pull resistors GND->VCC and VCC->DATA), named # (30 mm wires, external pull resistors GND->VCC and VCC->DATA), named
# channels DATA/VCC/RST/CLK on bits 0/2/4/5. # channels DATA/VCC/RST/CLK on bits 0/2/4/5.
+4 -1
View File
@@ -65,7 +65,10 @@ MAX_TPDU_LEN = 271
# data region, followed by its P3 data bytes and an interior status word. # data region, followed by its P3 data bytes and an interior status word.
# These constants bound detector precision (empirically tuned on all 10 # These constants bound detector precision (empirically tuned on all 10
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit). # 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). # Commands that actually occur on these cards' bus (T=0 + STK/CAT).
EMBEDDED_INS = frozenset(( EMBEDDED_INS = frozenset((
0x12, 0x14, 0x20, 0x44, 0x70, 0x88, 0x12, 0x14, 0x20, 0x44, 0x70, 0x88,