v1.5.0: invalid-INS one-byte re-frame + explicit discard logging
Residual CONCAT mis-frames (4gmodem/A55_sim2/S21p_sim2) traced to a silent 5-byte drop at an invalid-INS T=0 header (0x0f 00 a4 08 04). Drop only the stray leading byte and re-frame the remaining four via _replay so the real command (00 a4 08 04 02 2f 00 -> 61 1c) is recovered. - _note_discard()/_consume_first(): every consumed-but-unemitted byte is now logged; idle 0x00/0xFF discards are rate-limited, real data always gets an ANN_WARN annotation. - All read-until-SW loops log skipped non-SW bytes. - All traces: CONCAT 0; BAD_FCS/GARBAGE 0; test_8/test_7 RESULT OK; 47/47 unit tests; baseline byte-identical. History rewritten to a single root commit so no example trace is stored in the repo (fixtures are local-only, gitignored). README keeps only generic <trace>.sr placeholders; AGENTS.md retains the fixture names.
No files matched your search
@@ -0,0 +1,5 @@
|
||||
# Batch scripts require CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
# Keep shell scripts LF
|
||||
*.sh text eol=lf
|
||||
@@ -0,0 +1,6 @@
|
||||
*.pcap
|
||||
*.log
|
||||
__pycache__/
|
||||
|
||||
# Trace fixtures are local-only (large, machine-specific); never tracked.
|
||||
examples/
|
||||
@@ -0,0 +1,879 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Policy: decoder changes must be validated against ALL example traces
|
||||
|
||||
Every change to `pd.py` must be followed by running the full test suite:
|
||||
|
||||
1. **Clear bytecode cache first** - stale `.pyc` files cause false results:
|
||||
```bash
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} +
|
||||
find . -name "*.pyc" -delete 2>/dev/null
|
||||
```
|
||||
|
||||
2. **Run all traces in mid-session mode** (the decoder always runs this way;
|
||||
captures
|
||||
all APDUs, including cold-boot ATRs):
|
||||
```bash
|
||||
# Run decoder for each trace
|
||||
for trace in test_8_raw16 test_7_raw16 xiaomi_phone_sample \
|
||||
samsung_phone_sample \
|
||||
xiaomi_mi_a1_coldboot_sample 4gmodem_coldboot_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
|
||||
# Capture APDUs, GARBAGE count, and RESULT
|
||||
done
|
||||
```
|
||||
|
||||
3. **Acceptance criteria** (must ALL pass):
|
||||
- **test_8 mid-session**: 56 APDUs, 28/37 reader exchanges (9 pre-capture), 0 GARBAGE, RESULT: OK
|
||||
- **test_7 mid-session**: 65 APDUs, 37/37 reader exchanges, 0 GARBAGE, RESULT: OK
|
||||
- **All smoke-test traces**: >0 APDUs in mid-session mode
|
||||
- **No regressions**: GARBAGE count must not increase, no new BAD_FCS/payload mismatches
|
||||
- **Unit tests**: 30/30 pass (`python3 tests/test_gsmtap.py -v`)
|
||||
|
||||
### Important: CLK signal behavior matters
|
||||
|
||||
- **Constant CLK** (test_7, test_8): Card reader keeps CLK running continuously
|
||||
- Decoder can reliably sample DATA on each CLK edge
|
||||
- Cold-boot re-arm handles warm resets
|
||||
|
||||
- **Gated CLK** (phone traces): Phone stops CLK when idle, restarts before next APDU set
|
||||
- Decoder must handle CLK gaps and restart properly
|
||||
- ETU auto-detection from DATA edges is critical
|
||||
- This is the decoder's primary use case
|
||||
|
||||
- **Cold boot captured pre-power-up** (`*_coldboot_sample.sr`): VCC/RST are LOW
|
||||
at t=0, capture spans the power-on (~17 s at 16 MHz), and the card emits its
|
||||
ATR once RST deasserts
|
||||
- Decoder must arm the ATR hunt when RST is still low at capture start
|
||||
(see "Cold-boot re-arm") and not lock an ETU from pre-ATR power-up traffic
|
||||
- It must decode the ATR `3b9f96801f...001e`
|
||||
- `rst_detect=true`, `vcc_detect=true` for these fixtures
|
||||
|
||||
```bash
|
||||
# --- test_8 ---
|
||||
sigrok-cli -i examples/test_8_raw16.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 >/dev/null
|
||||
python3 tools/vs_reader.py examples/test_8_reader.log /tmp/out.pcap
|
||||
|
||||
# --- test_7 ---
|
||||
sigrok-cli -i examples/test_7_raw16.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 >/dev/null
|
||||
python3 tools/vs_reader.py examples/test_7_reader.log /tmp/out.pcap
|
||||
|
||||
# --- xiaomi phone (gated CLK) ---
|
||||
sigrok-cli -i examples/xiaomi_phone_sample.sr \
|
||||
-P iso7816:clk=CLK:data=DATA:rst=RST:clock_option=native:protocol=T=0:gsmtap_enable=false:pcap_file=/tmp/out.pcap \
|
||||
-A iso7816 >/dev/null
|
||||
|
||||
# --- samsung phone 1 (gated CLK, non-default F/D) ---
|
||||
sigrok-cli -i examples/samsung_phone_sample.sr \
|
||||
-P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:protocol=T=0:gsmtap_enable=false:pcap_file=/tmp/out.pcap \
|
||||
-A iso7816 >/dev/null
|
||||
|
||||
# --- xiaomi_mi_a1_coldboot (starts pre-power-up) ---
|
||||
# rst_detect/vcc_detect=true are required for these fixtures.
|
||||
sigrok-cli -i examples/xiaomi_mi_a1_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 >/dev/null
|
||||
|
||||
# --- 4gmodem_coldboot ---
|
||||
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 >/dev/null
|
||||
|
||||
# --- unit tests ---
|
||||
python3 tests/test_gsmtap.py -v
|
||||
```
|
||||
|
||||
**Always clear the Python bytecode cache before testing** — stale `.pyc`
|
||||
files can cause the decoder to use old code, producing false results:
|
||||
```bash
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} +
|
||||
```
|
||||
|
||||
The acceptance criteria are:
|
||||
|
||||
**The decoder is NOT considered fully functional until ALL of these criteria are met.**
|
||||
|
||||
#### Ground truth
|
||||
|
||||
`test_7_reader.log` and `test_8_reader.log` are **trusted, complete APDU logs**
|
||||
captured by the PC/SC reader SDK during the same recording session as the
|
||||
sigrok trace. They contain every command the reader sent and every response
|
||||
the card returned. They are the authoritative reference — the decoder output
|
||||
must match them.
|
||||
|
||||
#### Success criteria (all must pass)
|
||||
|
||||
1. **test_8 and test_7 (reader-log traces):**
|
||||
- **Every reader exchange must appear in the decoder output.**
|
||||
For test_8: 28/37 reader exchanges are present in the pcap
|
||||
(9 missing because they occurred before the capture started —
|
||||
a trace-level limitation, not a decoder bug). For test_7: all
|
||||
37/37 are present after the v1.1.10 deglitch fix (previously
|
||||
only 10/37 due to ATR resets fragmenting the session).
|
||||
- **0 garbage APDUs** in mid-session mode.
|
||||
- **`RESULT: OK`** from `vs_reader.py`.
|
||||
|
||||
2. **All traces (including smoke-test):**
|
||||
- **>0 APDUs** in mid-session mode for smoke-test traces.
|
||||
- No increase in `CHKSUM ERROR`, `BAD_FCS`, payload mismatches, or
|
||||
suspicious CLA counts.
|
||||
|
||||
3. **Every trace must be tested** (the decoder always runs mid-session — it
|
||||
captures all APDUs, including cold-boot ATRs).
|
||||
|
||||
#### Known issues (must be fixed to meet criteria)
|
||||
|
||||
**T=0 framing produces incorrect APDUs.** The decoder's output now matches
|
||||
the reader log for all reader exchanges:
|
||||
- test_8: 28/37 reader exchanges are byte-identical in the pcap
|
||||
(9 missing because they occurred before the capture started),
|
||||
plus 17 extra CAT/proactive commands and 11 valid retries,
|
||||
total 56 APDUs, 0 garbage.
|
||||
- test_7: All 37/37 reader exchanges are byte-identical in the pcap,
|
||||
plus 17 CAT/proactive commands, 8 ATRs, and 11 valid retries,
|
||||
total 65 APDUs, 0 garbage.
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
branch and got appended as command data. Fixed by skipping NULLs
|
||||
before the ACK check.
|
||||
|
||||
2. **"Read until SW" truncates GET RESPONSE (FIXED).** The ACK handler
|
||||
read until it saw a byte in 0x6x/0x9x range, but FCP tag 0x62
|
||||
matched this pattern, truncating GET RESPONSE to 1-2 bytes. Fixed
|
||||
by using P3-bounded reads (read exactly P3 bytes then SW1 SW2) for
|
||||
case-2 commands where P3 = Le.
|
||||
|
||||
#### CLA validation note
|
||||
|
||||
All example traces (`test_7`, `test_8`, `xiaomi`, `samsung`)
|
||||
contain only CLA `0x00` (interindustry) and `0x8x` (USIM/CAT, mostly `0x80`
|
||||
with occasional `0x81` for logical channel 1).
|
||||
The decoder must **not** mandate specific CLA values — other CLA values
|
||||
(e.g. `0x04`, `0x08`, `0x84`, `0xA0`, `0xB0`) are valid per ISO 7816-4 / 3GPP
|
||||
and must be accepted. However, during test runs against the example traces,
|
||||
any CLA value outside `0x00-0x0F` and `0x80-0x8F` is an error flag indicating
|
||||
the decoder mis-framed the byte stream.
|
||||
|
||||
#### ISO 7816 APDU validation (enforced from v1.1.7)
|
||||
|
||||
The decoder validates every reassembled T=0 APDU with `validate_t0_apdu()`
|
||||
before emitting it. Invalid APDUs are still packed into gsmtap but are
|
||||
flagged with `GSMTAP_FLAG_BAD_FCS`.
|
||||
|
||||
Validation is purely structural (no content scanning of command/response
|
||||
data), so the false-positive rate is very low. The checks are:
|
||||
|
||||
1. Length is at least 7 bytes (header + SW) and at most `MAX_TPDU_LEN` (271).
|
||||
2. CLA byte is in the recognized set (`0x00`, `0x04`, `0x08`, `0x80`,
|
||||
`0x84`, `0xA0`, `0xB0`).
|
||||
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
||||
collision), and the least-significant bit is `0`.
|
||||
4. The penultimate byte (SW1) is in `0x6x` or `0x9x`.
|
||||
|
||||
The current validation deliberately does **not** enforce P3-based length
|
||||
constraints, because a card may reject a command and send the status word
|
||||
immediately even when P3 > 0.
|
||||
|
||||
#### v1.4.0: CONCAT detection (embedded exchanges, issue #1 gate)
|
||||
|
||||
A byte-drop mis-frame swallows several real T=0 exchanges into one
|
||||
structurally "valid" packet: each swallowed header survives verbatim in the
|
||||
data region, followed by its P3 data bytes and an interior status word.
|
||||
Such packets pass `validate_t0_apdu()` but produce wrong APDUs downstream.
|
||||
`embedded_exchanges()` (pd.py) scans the data region (`[5, n-9]`) for
|
||||
embedded complete exchanges:
|
||||
|
||||
- header `CLA INS P1 P2 P3` where `INS ∈ EMBEDDED_INS` (commands that
|
||||
actually occur on these cards: 12/14/20/44/70/88/A2/A4/B0/B2/B6/C0/C2/
|
||||
CA/CB/D6/DC/F2) and `P3 ≥ 1`
|
||||
- CLA valid, or the phantom byte-drop artifact `0x01` (corrupt `0x00`),
|
||||
with a one-byte prefix slip tolerated when byte `0x01` precedes a real
|
||||
header
|
||||
- SW1 accepted in a 3-byte slack window after the nominal data end
|
||||
(`d1 = s + 5 + p3`), from `EMBEDDED_SW1` (real status bytes that are not
|
||||
also frequent data bytes), and the SW must be interior (`sw < n-2`, i.e.
|
||||
more exchange data must follow it)
|
||||
|
||||
On a hit the decoder flags the packet `GSMTAP_FLAG_BAD_FCS` and logs
|
||||
`CONCAT APDU: <k> embedded exchange(s) at <offsets>; packet <hex>`, so the
|
||||
gate works live and offline. The detector is precision-tuned against all
|
||||
10 example traces: 0 hits on clean traces (test_8/test_7/4gmodem), and it
|
||||
flags every known corrupted family:
|
||||
|
||||
| Trace | APDUs | CONCAT hits |
|
||||
|-------|-------|-------------|
|
||||
| test_8 | 56 | 0 |
|
||||
| test_7 | 65 | 0 |
|
||||
| xiaomi_phone | 620 | 4 |
|
||||
| samsung_phone | 924 | 5 |
|
||||
| xiaomi_mi_a1_coldboot | 459 | 3 |
|
||||
| 4gmodem_coldboot | 327 | 0 |
|
||||
| A55_coldboot_new_cable | 386 | 3 |
|
||||
| A55_coldboot_new_cable_sim2 | 375 | 2 |
|
||||
| S21p_coldboot_new_cable | 380 | 3 |
|
||||
| S21p_coldboot_new_cable_sim2 | 356 | 2 |
|
||||
|
||||
The concat packets are BAD_FCS-flagged on the phone traces, so their BAD_FCS
|
||||
counts now equal the CONCAT counts (deliberate: those APDUs are genuinely
|
||||
desynced). `vs_reader.py` reports the same detector; `capture_baseline.sh`
|
||||
prints `CONCAT APDU count: N` per trace (grep of the `CONCAT APDU:` log line
|
||||
only, so one line per packet). Acceptance: test_8/test_7/4gmodem must stay
|
||||
0; phone-trace counts must not increase.
|
||||
|
||||
### v1.4.0: CONCAT gate — embedded-exchange detection (issue #1)
|
||||
|
||||
The decoder previously emitted byte-drop-mis-framed APDUs that swallowed
|
||||
several real exchanges into one "valid" packet (the Samsung/xiaomi
|
||||
"wrong APDU" family). Fragmentation is not representable in the
|
||||
assembled bytes, so this is fixed as a detection gate, not a reframe:
|
||||
|
||||
- `embedded_exchanges()` scans the data region for embedded complete
|
||||
exchanges (see "v1.4.0: CONCAT detection" under APDU validation).
|
||||
- Hits are flagged `GSMTAP_FLAG_BAD_FCS` + logged `CONCAT APDU: ...`.
|
||||
- `vs_reader.py` / `capture_baseline.sh` gain a `CONCAT` metric
|
||||
(packet counts match the log lines exactly).
|
||||
- Unit tests cover the tuned vectors (clean FCP/READ-RECORD/SELECT never
|
||||
hit; U1/U4/record-burst/xiaomi swallows are caught).
|
||||
- VERSION → 1.4.0. Baseline regenerated with the new CONCAT columns.
|
||||
|
||||
A proper *reframing* (re-splitting those packets and byte-dropping the
|
||||
mis-frame at the source) is the next decoder step; until then the gate
|
||||
makes the corruption visible in live and offline captures instead of
|
||||
silently producing wrong APDUs.
|
||||
|
||||
### v1.5.0: invalid-INS one-byte re-frame (residual CONCATs → 0)
|
||||
|
||||
The last CONCAT family (4gmodem/A55_sim2/S21p_sim2) is now **reframed at
|
||||
the source**, not just gated. All 10 traces report 0 CONCAT:
|
||||
|
||||
| Trace | APDUs | CONCAT hits |
|
||||
|-------|-------|-------------|
|
||||
| test_8 | 56 | 0 |
|
||||
| test_7 | 65 | 0 |
|
||||
| xiaomi_phone | 640 | 0 |
|
||||
| samsung_phone | 924 | 0 |
|
||||
| xiaomi_mi_a1_coldboot | 473 | 0 |
|
||||
| 4gmodem_coldboot | 327 | 0 |
|
||||
| A55_coldboot_new_cable | 407 | 0 |
|
||||
| A55_coldboot_new_cable_sim2 | 395 | 0 |
|
||||
| S21p_coldboot_new_cable | 399 | 0 |
|
||||
| S21p_coldboot_new_cable_sim2 | 375 | 0 |
|
||||
|
||||
1. **One-byte re-frame on invalid INS.** When a 5-byte header window passes
|
||||
`plausible_cla()` but the INS is unusable (`0x00`, `0x6x`/`0x9x`, or odd
|
||||
LSB — ISO 7816-4 §12.1.1), the window is almost always shifted by one
|
||||
stray leading byte (e.g. a leftover SW2 read after a PACKETEND). The old
|
||||
code silently dropped all 5 bytes plus resync (`_resync = True`), and when
|
||||
`resync_idle()` gave up on a busy burst the next byte became a bogus CLA
|
||||
whose P3 swallowed the whole dialog into one CONCAT packet. Now the
|
||||
decoder drops **only** the leading byte (explicitly logged via
|
||||
`_note_discard`), re-frames the remaining four via `_replay`, and lets the
|
||||
next decode step start a fresh header at the old INS. Example (4gmodem):
|
||||
the stray `0f` after the READ BINARY SW1 `91` re-frames `0f 00 a4 08 04`
|
||||
→ `00 a4 08 04 02`, recovering the true SELECT `00 a4 08 04 02 2f 00 →
|
||||
61 1c` verbatim.
|
||||
2. **No-silent-discard invariant.** Every byte the decoder consumes without
|
||||
emitting is now explicitly logged via `_note_discard(value, reason,
|
||||
idle=...)`. `idle=True` discards (0x00/0xFF idle stretches, NULLs, stuck
|
||||
low) are rate-limited to the 1st + every 256th; `idle=False` discards
|
||||
(suspicious CLA, invalid-INS re-frame byte, non-SW bytes in read-until-SW
|
||||
scans) always put an `ANN_WARN` and the reason. Idle-line noise,
|
||||
suspicious-CLA, stuck-low, and idle-byte-in-header consumers all go
|
||||
through `_consume_first()` (which pops `_replay` before `peeked_byte`) so
|
||||
re-framed bytes cannot infinite-loop.
|
||||
3. **`_consume_first()` semantics.** `read_byte()` pops `_replay.popleft()`
|
||||
first and only falls through to the wire; `peek_byte()` returns
|
||||
`_replay[0]` first. Neither updates `peeked_samplenum`, so the re-frame
|
||||
branch explicitly restores `self.peeked_samplenum = es` (the first byte's
|
||||
sample) before pushing the 4 remaining bytes — the next header annotates
|
||||
from a valid sample number.
|
||||
4. **Counters.** `reset()` initializes `_discard_total` / `_idle_discard_total`; every `_note_discard` line carries "discards so far: N" for session-level triage.
|
||||
|
||||
The CONCAT detector (`embedded_exchanges()`, v1.4.0) stays as a regression
|
||||
gate and now reports 0 on every trace. VERSION → 1.5.0. Baseline
|
||||
regenerated.
|
||||
|
||||
**Phase B status (v1.5.0).** Two edge-reader hardening fixes shipped, both
|
||||
verified byte-neutral on all 10 traces (stored baseline unchanged):
|
||||
|
||||
- `_next_start_fall()` now rejects sub-ETU phantom falls (a genuine start
|
||||
fall must stay LOW for >= half an ETU) — B2.
|
||||
- `_read_byte_edges()` re-anchors forward on a spurious latch instead of
|
||||
dropping the byte (a drop fabricates a 0x00 / shifts the boundary) — B1.
|
||||
|
||||
Root cause of the Samsung/xiaomi phone-trace concatenation (the B1/B2 family)
|
||||
is fixed at the source by the v1.5.0 invalid-INS re-frame — the A55 burst
|
||||
cases decode clean with 0 CONCAT. The residual 4gmodem/A55_sim2/S21p_sim2
|
||||
family (leftover-SW2-shifted header) is also gone. The planned B3 (strict
|
||||
positional genuine-start anchor in the no-idle resync path) directly
|
||||
conflicts with the documented 0.5-vs-1.5-ETU guard trade-off for fast phone
|
||||
ETUs and risks the Samsung de-concatenation; it needs a dedicated design +
|
||||
live validation, not an incremental patch.
|
||||
|
||||
### v1.3.0: `starts_with_atr` option removed
|
||||
|
||||
The entire ATR-included mode was redundant: its only mode-specific logic was a
|
||||
start-state bootstrap (`FIND START` vs `DATA`) and an early trust hint for
|
||||
`clock_skip=372`, both of which mid-session decoding reproduces via the
|
||||
warm-reset / cold-boot re-arms already handled in `state == 'DATA'`
|
||||
(starting the hunt instead of a fixed initial state). Every trace in the
|
||||
suite produces identical APDU/GARBAGE/CHKSUM results with the option removed.
|
||||
Removed:
|
||||
|
||||
- The `starts_with_atr` decoder option (schema + the 6 branching sites in
|
||||
`pd.py`: initial state, `_clock_skip_confident` bootstrap, the
|
||||
`handle_atr` samplenum save/restore, the cold-boot re-arm guard, and the
|
||||
ETU re-measure condition — now `self.bit_samples is None or
|
||||
self._pps_speed_changed`).
|
||||
- `--starts-with-atr` from `start.sh` (now warned as obsolete).
|
||||
|
||||
The decoder always starts in `DATA` and re-arms the ATR hunt as needed.
|
||||
|
||||
### v1.2.1: cold-boot capture support
|
||||
|
||||
A capture that starts pre-power-up (VCC/RST low at t=0) previously locked a
|
||||
bogus ETU (`bit_samples=1548`, `clock_skip=369`) from pre-ATR power-up DATA
|
||||
transitions in mid-session mode, so the real ATR never synced. Fixed in
|
||||
`241b7ed`:
|
||||
|
||||
- **Priming.** First `decode_step` reads the initial RST/VCC levels instead of
|
||||
defaulting `last_rst`/`last_vcc` to 0 (which fabricated a reset edge).
|
||||
- **Cold-boot re-arm.** RST low at capture start → arm the hunt at the
|
||||
spec-default rate (FIND START, `clock_skip=372`, no ETU lock).
|
||||
- **Guarded deassert re-arm.** The 10 ms-hysteresis-confirmed deassert lands
|
||||
mid-ATR on warm resets; it only re-arms when `state == 'DATA'` so it no
|
||||
longer garbles the in-flight ATR read (test_8 mid-session CHKSUM 18→0,
|
||||
test_7 ATR-included 2→0).
|
||||
|
||||
New fixtures `xiaomi_mi_a1_coldboot_sample.sr` / `4gmodem_coldboot_sample.sr`
|
||||
decode their full ATR in both modes with 0 GARBAGE.
|
||||
|
||||
### v1.2.0: start-bit deglitch (Samsung de-concatenation)
|
||||
|
||||
Samsung phone traces with gated CLK and non-default F/D (16 CLK/ETU)
|
||||
produced concatenated APDUs: multiple T=0 exchanges merged into a single
|
||||
GSMTAP packet. The v1.1.9 fixes (turnaround idle handling, edge-list
|
||||
deglitch) improved other phone traces but the Samsung trace still showed
|
||||
concatenation because the CLK-synchronous reader's start-bit detector
|
||||
could not use the edge-list deglitch filter.
|
||||
|
||||
Fix applied (v1.2.0):
|
||||
- Add a 2-sample deglitch in `wait_data_falling()`: after the 1.5-ETU
|
||||
minimum-high guard passes, skip 2 additional samples and re-check DATA.
|
||||
If DATA returns to idle, the start bit was a glitch → continue hunting.
|
||||
This eliminates 1-sample (62.5 ns) LOW glitches in inter-byte guard
|
||||
times that caused byte-level misalignment in the CLK-sync reader.
|
||||
|
||||
Results:
|
||||
- Samsung mid-session: 52 → 924 APDUs (de-concatenated), all valid ISO 7816
|
||||
commands (SELECT, GET RESPONSE, READ BINARY, STATUS, STK envelope), 0 GARBAGE.
|
||||
- test_7 ATR: 10 → 65 APDUs, all 37/37 reader exchanges now captured
|
||||
(was 10/37 in v1.1.9 due to ATR resets fragmenting the session).
|
||||
- test_8 ATR: 54 → 56 APDUs, 28/37 reader exchanges (unchanged — 9 missing
|
||||
are pre-capture, not a decoder issue).
|
||||
- Xiaomi mid-session: 620 APDUs (unchanged, no regression).
|
||||
|
||||
**Glitch rejection measurements** (`-l 5` session log):
|
||||
|
||||
| Trace | Glitches rejected | Notes |
|
||||
|-------|-------------------|-------|
|
||||
| test_8 (reader) | 6 | Constant CLK, minor noise/turnaround artifacts |
|
||||
| test_7 (reader) | 14 | Constant CLK, more activity than test_8 |
|
||||
| xiaomi (phone) | **0** | Edge-list `_deglitch_edges()` filters before `wait_data_falling` |
|
||||
| samsung (phone) | **1041** | CLK-sync reader has no upstream deglitch, confirms FX2LP pull-up hypothesis |
|
||||
|
||||
Xiaomi shows 0 because the edge-list reader's `_deglitch_edges()` (0.2×ETU
|
||||
threshold) removes sub-ETU pulses before they reach `wait_data_falling()`.
|
||||
The CLK-sync reader has no equivalent filter, so Samsung catches every
|
||||
glitch. The 6–14 glitches on reader traces are minor noise or turnaround
|
||||
artifacts in the constant-CLK captures.
|
||||
|
||||
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.
|
||||
|
||||
**Logging.** Each rejected start-bit glitch is logged with a running
|
||||
count (`start-bit glitch rejected (#N)`) visible in the session log.
|
||||
This helps diagnose whether the deglitch is too aggressive on a
|
||||
particular capture — if the count is unexpectedly high, the 2-sample
|
||||
window may be filtering real start bits.
|
||||
|
||||
### Proven data rates (all at 16 MHz sample rate)
|
||||
|
||||
| Trace | ETU (samples) | CLK/ETU | Mode |
|
||||
|-------|---------------|---------|------|
|
||||
| test_8 | ~43 | 372 (standard) | constant CLK, ATR+PPS |
|
||||
| test_7 | ~43 | 372 (standard) | constant CLK, warm resets |
|
||||
| xiaomi | ~53 | recovered from DATA | gated CLK, mid-session |
|
||||
| samsung | ~72 | 16 (F=512/D=32) | gated CLK, non-standard F/D |
|
||||
|
||||
##### T=0 Procedure Bytes
|
||||
|
||||
| Byte | Meaning |
|
||||
|------|---------|
|
||||
| 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 |
|
||||
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
||||
| SW1 (0x6x / 0x9x) | Status word: exchange complete |
|
||||
|
||||
##### CLA (Class) Byte — Interindustry Structure
|
||||
|
||||
```
|
||||
Bit: 8 7 6 5 4 3 2 1
|
||||
b8 b7 b6 b5 SM SM ch ch
|
||||
```
|
||||
|
||||
- Bits 8–7: `00` or `01` for interindustry; `1x` reserved for proprietary
|
||||
- Bit 6: `0` for interindustry; `1` for proprietary structural formats
|
||||
- Bits 4–3: Secure Messaging indicator (`00` = no SM)
|
||||
- Bits 2–1: Logical channel (0–3 for interindustry)
|
||||
|
||||
Valid interindustry CLA digits: `0x`, `1x`, `4x`, `5x`.
|
||||
|
||||
Examples: `0x00` (standard), `0x0C` (secure messaging).
|
||||
|
||||
##### INS (Instruction) Byte — Valid Ranges
|
||||
|
||||
- Low nibble cannot be `X0` if high nibble is even
|
||||
- Low nibble cannot be `XF` under any circumstances
|
||||
- `0x6x` and `0x7x` ranges are invalid (reserved for SW1 / protocol bytes)
|
||||
|
||||
Standard interindustry INS values: `A4` (SELECT), `B0` (READ BINARY),
|
||||
`D6` (UPDATE BINARY), `B2` (READ RECORD), `E2` (APPEND RECORD),
|
||||
`20` (VERIFY), `82` (EXTERNAL AUTHENTICATE), `88` (INTERNAL AUTHENTICATE).
|
||||
|
||||
##### Quick Validation Checklist
|
||||
|
||||
1. CLA high nibble is `0`, `1`, `4`, or `5` (for interindustry)
|
||||
2. INS low nibble is not `F`
|
||||
3. INS is outside `0x6x`–`0x7x` range
|
||||
|
||||
**Current smoke-test status:**
|
||||
|
||||
| Trace | APDUs |
|
||||
|-------|-------|
|
||||
| test_8 | 56 |
|
||||
| test_7 | 65 |
|
||||
| xiaomi_phone | 640 |
|
||||
| samsung_phone | 924 |
|
||||
| xiaomi_mi_a1_coldboot | 473 |
|
||||
| 4gmodem_coldboot | 327 |
|
||||
| A55_coldboot_new_cable | 407 |
|
||||
| A55_coldboot_new_cable_sim2 | 395 |
|
||||
| S21p_coldboot_new_cable | 380 |
|
||||
| S21p_coldboot_new_cable_sim2 | 356 |
|
||||
|
||||
The baseline runs every trace through the decoder once
|
||||
(intentional since v1.3.0: the `starts_with_atr` option was removed and the
|
||||
decoder always runs mid-session — it captures all APDUs, including cold-boot
|
||||
ATRs, and is the only mode that works on gated-CLK phone traces).
|
||||
|
||||
The four `samsung_*_coldboot_new_cable*_sim2` fixtures were captured 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 at 16 MHz. All four decode
|
||||
their full cold-boot ATR (`3b9f9580…` on SIM1, `3b9f9680…` on SIM2) with
|
||||
0 GARBAGE / 0 BAD_FCS. S21p SIM2 carries 197 CHKSUM (dirty-wire parity
|
||||
noise, BAD_FCS still 0); A55 SIM2 is clean (0 CHKSUM). The `_sim2` fixtures
|
||||
are cropped to the power-on window (the 17–21 s of pre-power idle was
|
||||
removed).
|
||||
|
||||
Cold-boot fixtures start **before power-up**
|
||||
(VCC/RST low at t=0); the decoder must arm the ATR hunt when RST is still low
|
||||
at capture start and decode the full ATR once the card wakes (~17 s in).
|
||||
`l8star_coldboot_sample.sr` was captured in the same session but had a
|
||||
dirty-wire tail (GARBAGE=1 / RESULT UNCLEAN) and was dropped from the suite.
|
||||
|
||||
**Note on v1.1.3 fix:** The decoder measures CLK period as an average instead of using the minimum recurring period. This correctly handles traces where CLK periods alternate (e.g., 4,3,3 pattern for ~5.33MHz CLK at 16MHz sample rate). The fix changed `_measure_clock_period()` to use `sum(spacings) / len(spacings)` instead of `_robust_min(spacings)`, giving accurate `spc` values like 3.333... instead of 3. This corrected the `_wait_clk_rising()` skip calculation, fixing the post-ATR byte alignment issue.
|
||||
|
||||
### GSMTAP wire-format extensions
|
||||
|
||||
The GSMTAP-SIM output deviates from libosmocore's gsmtap.h in three
|
||||
documented ways (see README.md "GSMTAP events" and the comments in
|
||||
`gsmtap_stream.py`): sub_type 0x02 (`GSMTAP_SIM_PPS`) carries the PPS
|
||||
request + response combined under the spec's `PPS_REQ` value (the spec
|
||||
splits into `PPS_REQ` 0x02 / `PPS_RSP` 0x03; 0x03 is unused here);
|
||||
0x10/0x11 (`RST_EVENT`/`VCC_EVENT`) are custom line-event subtypes; and
|
||||
`GSMTAP_FLAG_BAD_FCS` writes flags into the header `res` byte, which
|
||||
official gsmtap.h reserves — a simtrace2-sniff convention.
|
||||
|
||||
### Versioning
|
||||
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.5.0'`.
|
||||
The version is printed to the log on decoder startup.
|
||||
|
||||
### Testing after decoder changes
|
||||
|
||||
After any change to `pd.py`:
|
||||
|
||||
1. Clear bytecode cache:
|
||||
```bash
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} +
|
||||
```
|
||||
|
||||
2. Run all tests against all traces:
|
||||
```bash
|
||||
./tools/capture_baseline.sh > tests/baseline.txt
|
||||
```
|
||||
|
||||
3. Compare against saved baseline:
|
||||
```bash
|
||||
./tools/compare_baseline.sh
|
||||
```
|
||||
|
||||
4. If differences found, review:
|
||||
- Capture APDU count must stay >0 for any trace that previously had APDUs
|
||||
- GARBAGE count must stay 0 for test_8 (mid-session)
|
||||
- RESULT must stay OK for test_8 (mid-session)
|
||||
- CHKSUM ERROR count must not increase
|
||||
- BAD_FCS / payload mismatches / suspicious CLA must not increase
|
||||
- ETU values must match for mid-session traces
|
||||
- No new INVALID Procedure Byte errors
|
||||
|
||||
5. Run unit tests:
|
||||
```bash
|
||||
python3 tests/test_gsmtap.py -v
|
||||
```
|
||||
|
||||
6. Update `tests/baseline.txt` with new baseline if changes are intentional.
|
||||
|
||||
## What happened: the decoder rewrite attempt
|
||||
|
||||
We attempted a major rewrite of the ISO 7816 decoder (`pd.py`) with these
|
||||
features:
|
||||
|
||||
1. **Edge-counting always** — removed the `native_fast` sample-skip bit
|
||||
reader, making all bit reads count CLK edges per bit instead of skipping
|
||||
`clock_skip * samples_per_edge` samples. The idea was drift-immunity.
|
||||
|
||||
2. **Signal degradation recovery** — a `degraded` flag tracking rolling
|
||||
parity errors; when ≥ 8 of 64 frames fail parity, the decoder re-arms
|
||||
the ATR hunt and flags APDUs with `GSMTAP_FLAG_BAD_FCS`.
|
||||
|
||||
3. **Clock-drift monitor** — every 128 bytes the decoder re-measures the
|
||||
CLK period and re-anchors `samples_per_edge` if the median drifts > 2%.
|
||||
|
||||
4. **Fatal TCK check** — the ATR's TCK checksum mismatch was made fatal
|
||||
(reject the ATR) instead of being logged as a warning.
|
||||
|
||||
5. **Synthetic ATR fallback** — after 8 failed ATR hunts, commit a
|
||||
minimal T=0 ATR `[0x3B, 0x00]` and begin decoding, for mid-session
|
||||
captures.
|
||||
|
||||
## Outcome
|
||||
|
||||
The rewrite introduced a regression: **35 garbage APDUs** on `test_8`
|
||||
where the baseline had **0 garbage**. The root causes:
|
||||
|
||||
1. **Fatal TCK check** — the ATR in the example traces has a wrong TCK
|
||||
(`got=f7 expected=80`) due to a bit error in the capture. The fatal
|
||||
check rejected this real ATR, preventing the decoder from ever
|
||||
reaching the DATA state.
|
||||
|
||||
2. **Signal degradation ATR re-arm** — the `degraded` flag triggered on
|
||||
the capture's natural bit errors, re-arming the ATR hunt mid-session
|
||||
and fragmenting the APDU stream.
|
||||
|
||||
3. **Stale `.pyc` cache** — the Python bytecode cache was not
|
||||
regenerated after code changes, causing the running decoder to use
|
||||
old code. This masked the real regression for several test runs.
|
||||
|
||||
The edge-counting change itself may also have contributed subtle
|
||||
bit-sampling differences, but the TCK check and signal degradation were
|
||||
the primary causes.
|
||||
|
||||
## Resolution
|
||||
|
||||
We reverted to the known-good baseline (`2055d1c`, the VCC/RST hysteresis
|
||||
commit) and applied only three essential, minimal fixes on top:
|
||||
|
||||
1. **`hasT0`/`hasT1` init in `start()`** — initialize both to `False` for
|
||||
`protocol=auto`, preventing `AttributeError` when `starts_with_atr=false`
|
||||
enters DATA state without an ATR.
|
||||
|
||||
2. **`hasT0=False` guard in `decode_step` DATA state** — when no protocol
|
||||
is known, skip byte reading and wait for a warm reset to trigger the
|
||||
ATR hunt.
|
||||
|
||||
3. **Synthetic T=0 ATR fallback** — after 8 failed ATR hunts, commit
|
||||
`[0x3B, 0x00]` and set `hasT0=True` so the decoder can begin
|
||||
processing APDUs in mid-session captures.
|
||||
|
||||
The `native_fast` sample-skip, signal degradation, clock monitor, and
|
||||
fatal TCK check were all discarded. The decoder is now the original
|
||||
`2055d1c` code plus 24 lines of mid-session support.
|
||||
|
||||
## Current architecture (v1.1.1, post-ATR desync fixes)
|
||||
|
||||
The decoder uses a **CLK-synchronous bit reader** for native-CLK captures and
|
||||
an edge-list DATA reader for the non-native clock modes. Bit timing comes
|
||||
from **counting real CLK edges**, not from a sample-count estimate.
|
||||
|
||||
### Two bit readers (selected per burst)
|
||||
|
||||
| Reader | Timing source | Used when |
|
||||
|--------|---------------|-----------|
|
||||
| `_read_byte_clk()` | count `clock_skip` CLK rising edges per ETU, sample DATA at bit centres | native clock mode **and** `clock_skip` known (`_clock_skip_confident`) |
|
||||
| `_read_byte_edges()` | DATA-edge reconstruction from `bit_samples` | `sample_as_clock`/`detect`, or mid-session before `clock_skip` is recovered |
|
||||
|
||||
### Key changes
|
||||
|
||||
1. **CLK-synchronous reader.** `_read_byte_clk()` counts `clock_skip` CLK
|
||||
rising edges per ETU and samples DATA at the bit centres (1.5 … 9.5 ETU).
|
||||
Exact for `0x00`/`0xff` (too few DATA transitions for the edge reader) and
|
||||
deterministic, because `clock_skip` is protocol-defined (372 for the ATR,
|
||||
FI/DI after PPS). Safe across Clock Stop: CLK never stops mid-character
|
||||
and resumes before the next start bit. `_wait_clk_rising()` skips most of
|
||||
each bit by samples then lands on the exact edge, so it is fast.
|
||||
|
||||
2. **No synthetic ATR.** The old `[3B, 00]` fallback fabricated a 2-byte ATR
|
||||
from a stray `0x3B`/`0x3F` byte in command traffic (e.g. the FID `3F00` of
|
||||
a SELECT) and destroyed the command. The hunt is bounded and never emits
|
||||
a fake ATR.
|
||||
|
||||
3. **Command detection during ATR hunt.** A plausible CLA + valid INS is
|
||||
replayed to the DATA state and decoded as a command instead of skipped as
|
||||
"invalid TS" — this recovers a SELECT when the preceding ATR was missed.
|
||||
|
||||
4. **RST resets `clock_skip` to 372** on RST assert (a reset makes the card
|
||||
send ATR at the default rate).
|
||||
|
||||
5. **PPS on the native path** just updates `clock_skip` from FI/DI (CLK
|
||||
period is unchanged, so the new ETU is exact). No DATA re-measurement
|
||||
burst is consumed — previously that silently dropped the first post-PPS
|
||||
command.
|
||||
|
||||
6. **Mid-session `clock_skip` recovery.** `_measure_etu()` recovers
|
||||
`bit_samples`; `_measure_clock_period()` recovers `_samples_per_clock`;
|
||||
`_derive_clock_skip_from_etu()` sets `clock_skip = bit_samples /
|
||||
_samples_per_clock` and marks it confident. If the CLK period cannot be
|
||||
measured (fully gated CLK), the edge-list reader is used instead.
|
||||
|
||||
7. **Data handling.** Valid APDUs are emitted clean; ambiguous frames are
|
||||
emitted flagged `GSMTAP_FLAG_BAD_FCS` (never silently dropped); genuine
|
||||
idle-low all-zero noise remains suppressed.
|
||||
|
||||
### Testing
|
||||
|
||||
- `test_8` / `test_7`: **NOT YET PASSING.** 0 garbage, but decoder output
|
||||
does not match reader log (T=0 framing groups exchanges incorrectly).
|
||||
See success criteria above.
|
||||
|
||||
## Mid-session phone decode (legacy note, still applies)
|
||||
|
||||
For a live mid-session phone capture (`protocol=T=0`),
|
||||
the decoder now:
|
||||
|
||||
- Recovers the ETU from the live DATA line via `_measure_etu()` (edge-list,
|
||||
locked once) so a gated/stopped CLK at a fast ETU (e.g. ~53 samples) is
|
||||
decoded instead of mis-read at the 372-clock default.
|
||||
- Decodes bytes with the edge-list reader `_read_byte_edges` (the only
|
||||
reader now), which is drift-immune.
|
||||
- Runs the normal T=0 framing so each command+response exchange is emitted as
|
||||
one APDU.
|
||||
|
||||
Validation: `test_8`/`test_7` 0 garbage in both modes; 12 unit tests pass;
|
||||
phone 16M/24M decode to coherent APDUs with the STK text fully readable and
|
||||
no `0xff`/`0xfc` garbage.
|
||||
|
||||
### T=0 case 3/4 command-data (RESOLVED)
|
||||
|
||||
Commands that carry command-data (e.g. `80 12 … d0`, P3=208) send those
|
||||
P3 bytes on the wire **before** the card's first procedure byte. The
|
||||
framing now distinguishes the cases by the card's first procedure byte:
|
||||
|
||||
- If the first post-header byte **is the ACK (== INS)** → case 2/4: the
|
||||
terminal sent no command data; read `P3` response bytes (+SW).
|
||||
- Otherwise the byte is terminal command data → case 3/4: read `P3`
|
||||
command-data bytes, then the card's procedure byte (ACK/SW/`9E`/`9F`).
|
||||
On ACK (case 4) the response is read `P3` response bytes (+SW).
|
||||
|
||||
This consolidates case-3/4 exchanges (e.g. the `80 12`/`80 14` STK envelope
|
||||
commands) into one C-APDU = header + command data, R-APDU = response + SW
|
||||
APDU, instead of fragmenting them across frames. It is gated to
|
||||
`_edge_read` (mid-session / gated-CLK captures) so ATR-based captures
|
||||
(`test_8` / `test_7`) keep the original framing unchanged and stay at
|
||||
0 garbage.
|
||||
|
||||
All paths use P3-bounded reads (v1.1.1 reverted the read-until-SW
|
||||
experiment from v1.1.0 which mis-identified FCP `0x62` as SW1).
|
||||
|
||||
## Decoder methods tried — reliability registry
|
||||
|
||||
A running log of decoder approaches, so future work does not re-litigate
|
||||
discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
|
||||
### Proven reliable (kept)
|
||||
|
||||
- **Sample-skip bit reader (`native_fast`).** Original reader; advances bit
|
||||
timing with single sample-skip waits. Reliable for ATR-based captures
|
||||
(`test_8`/`test_7`, 0 garbage). Keep for those.
|
||||
- **ATR TS validation + resync.** Validate TS (`0x3B`/`0x3F`); resync on the
|
||||
next fall if bogus. Reliable; stops a phantom start bit shifting the ATR
|
||||
off-by-one.
|
||||
- **Warm-reset re-arm (guarded).** RST deassert → re-arm the ATR hunt
|
||||
(`FIND START`, `clock_skip` 372, cleared ETU) **only when `state == 'DATA'`**
|
||||
(hunt genuinely idle). Deasserts that arrive while `FIND START`/`ATR` is in
|
||||
progress just log "RST deasserted (hunt/ATR already in progress)" and leave
|
||||
state alone — an unguarded mid-ATR re-arm corrupted the in-flight ATR read
|
||||
(test_8 mid-session CHKSUM 18→0, test_7 ATR-included 2→0 after the guard).
|
||||
Reliable; re-ATRs parse at the default rate.
|
||||
- **Cold-boot priming.** One-shot on the first `decode_step`: `wait({'skip': 0})`
|
||||
reads the *initial* VCC/RST levels instead of defaulting `last_rst`/`last_vcc`
|
||||
to `0`. Without this, a capture that starts pre-power-up fabricated a spurious
|
||||
reset at sample 0.
|
||||
- **Cold-boot re-arm.** When the capture starts with RST **low** (VCC not yet
|
||||
on), arm the hunt immediately regardless of start conditions: state
|
||||
`FIND START`,
|
||||
`_atr_hunt_count=0`, `clock_skip=372`, `bit_samples=None` (no ETU lock), so the
|
||||
pre-ATR power-up DATA transitions cannot lock a bogus ETU (`bit_samples=1548`,
|
||||
`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.
|
||||
- **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.
|
||||
- **Synthetic T=0 ATR fallback `[0x3B, 0x00]`.** After 8 failed ATR hunts,
|
||||
commit a minimal ATR and set `hasT0=True`. Reliable for mid-session starts
|
||||
(no ATR in the trace).
|
||||
- **ETU auto-detect from DATA via edge counting (`_measure_etu`).** Edge-waits
|
||||
to the first DATA start-bit fall, measures ETU from DATA-edge spacing, locks
|
||||
it **once**. Reliable for gated/stopped CLK (phone, ~53 samples). Locking
|
||||
once (not re-measuring) is drift-immune and avoids glitch re-anchors.
|
||||
- **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`.
|
||||
- **`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
|
||||
reading until a protocol is known. Reliable.
|
||||
- **T=0 case-3/4 ACK==INS discriminator.** First post-header byte == INS ⇒
|
||||
case 2/4 (response bounded by `P3`); else command data (read `P3`, then
|
||||
the procedure byte). Reliable for consolidating STK envelope commands.
|
||||
Gated to `_edge_read`. 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).
|
||||
- **Inverse-convention ATR detection in DATA state.** `firstByte == 0x3f`
|
||||
alongside `0x3b` at line 1656, so inverse-convention ATRs (TS=0x3F) are
|
||||
parsed by the default state. Sets `clock_skip=372` correctly
|
||||
before PPS (v1.1.1 fix).
|
||||
- **`_resync = True` after PPS acceptance (native CLK path).** Forces
|
||||
`resync_idle()` before the first post-PPS command, preventing the decoder
|
||||
from latching onto a mid-byte transition at the new speed (v1.1.1 fix).
|
||||
|
||||
### Tried and rejected (do not re-add)
|
||||
|
||||
- **Edge-counting as the *sole* bit reader (remove `native_fast`
|
||||
entirely).** Subtle per-bit sampling differences regressed `test_8` (35
|
||||
garbage). Edge-counting is fine **as a second reader** (`_read_byte_edges`)
|
||||
used only in mid-session mode; it is not a drop-in replacement for the whole
|
||||
pipeline.
|
||||
- **Signal-degradation ATR re-arm (`degraded` flag on ≥8/64 parity errors).**
|
||||
Triggered on the capture's natural bit errors, re-armed the ATR hunt
|
||||
mid-session, and fragmented the stream. Rejected.
|
||||
- **Clock-drift monitor (re-measure CLK every 128 bytes, re-anchor
|
||||
`samples_per_edge`).** Unneeded: locking the ETU once is sufficient and
|
||||
safer. Rejected.
|
||||
- **Fatal TCK check.** The example ATRs carry a wrong TCK (capture bit error,
|
||||
`got=f7 expected=80`); fatal rejection blocked the DATA state entirely.
|
||||
TCK must stay a non-fatal warning. Rejected.
|
||||
- **CLA/P1P2 heuristic routing to T=1.** Misread real T=0 traffic as T=1.
|
||||
Rejected.
|
||||
- **`parse_packet` heuristic / APDU-boundary guessing from CLA/P1P2 + Le/ALG
|
||||
tables.** Produced false splits and mis-framed exchanges. Rejected in favor
|
||||
of proper T=0/T=1 transport state machines.
|
||||
- **`ignore_rst` / `ignore_vcc` options.** Superseded by channel assignment +
|
||||
hysteresis. Rejected.
|
||||
- **Generic "read response until a `6x`/`9x` status word" for T=0 framing.**
|
||||
Any `0x6x`/`0x9x` byte (e.g. command-data `0x6c`, FCP `0x6f`) was taken as
|
||||
SW1, truncating/fragmenting exchanges and dropping most of the capture.
|
||||
Rejected; replaced by the ACK==INS discriminator (case 2/4 bounded by
|
||||
`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.
|
||||
|
||||
### Open residual limitations
|
||||
|
||||
- Mid-session captures that begin inside an exchange cannot frame the first
|
||||
command perfectly (expected; the rest of the stream is clean).
|
||||
- **Samsung phone — gated CLK, non-default F/D** (`samsung_phone_sample.sr`).
|
||||
Uses F=512/D=32 (16 CLK cycles/bit). `_measure_etu` correctly measures
|
||||
the ETU from DATA edge spacings (after glitch filtering). Now passes
|
||||
with 0 GARBAGE and de-concatenated APDUs after v1.1.10 deglitch fix.
|
||||
|
||||
### v1.1.6 fix: bounded T=0 loops and explicit EOF handling
|
||||
|
||||
Tightened the edge-read T=0 procedure-byte handlers to prevent runaway
|
||||
reads and to surface end-of-capture explicitly.
|
||||
|
||||
1. **Bounded all "read until SW" / turnaround-skip loops.** Replaced the
|
||||
unbounded ACK-handler `while` and the open-ended `range(512)` loops with
|
||||
`for _ in range(MAX_TPDU_LEN)`. Added EOF checks so the decoder returns
|
||||
cleanly at end-of-capture instead of consuming zeros/turnaround bytes
|
||||
indefinitely.
|
||||
|
||||
2. **`read_byte()` / `peek_byte()` return `None` on EOF.** Previously both
|
||||
methods returned `0x00` when the sample stream ended, making it
|
||||
impossible for callers to distinguish real data from padding. They now
|
||||
return `None`; the T=0 DATA-state header and procedure-byte loops check
|
||||
for `None` and exit the current exchange.
|
||||
|
||||
3. **Skip turnaround idle bytes before appending.** The P3=0 and case-4
|
||||
ACK "read until SW" paths now mirror the v1.1.5 unexpected-byte fix:
|
||||
non-SW bytes (including 0xFF turnaround artifacts) are skipped before
|
||||
the status word is appended, rather than being added to the packet.
|
||||
|
||||
### v1.1.5 fix: turnaround idle bytes in edge-read path
|
||||
|
||||
Samsung phone traces with gated CLK had 5 garbage APDUs (BAD_FCS) in
|
||||
mid-session mode. Root cause: two code paths in the edge-read T=0
|
||||
procedure-byte handler did not skip turnaround idle bytes (0xFF) on the
|
||||
bidirectional DATA line.
|
||||
|
||||
1. **ACK handler (case 2/4):** After reading P3 response bytes, the code
|
||||
read 2 raw bytes as SW1 SW2. For case-3 commands (SELECT with data),
|
||||
the terminal sends command data after ACK, then the DATA line idles
|
||||
(0xFF) before the card sends SW. The idle 0xFF was mis-identified as
|
||||
SW1. Fixed by adding a `while` loop that skips non-SW bytes before
|
||||
reading SW1.
|
||||
|
||||
2. **"Unexpected byte" handler (procedure byte loop):** Non-SW bytes
|
||||
(e.g. 0xFF turnaround) were appended to the packet before scanning for
|
||||
SW, corrupting the SW position. Fixed by moving `packet.append(pb)`
|
||||
inside the SW1 check so non-SW bytes are skipped.
|
||||
@@ -0,0 +1,506 @@
|
||||
# sigrok_iso7816_stream
|
||||
|
||||
Fork of [svenso/sigrok_iso7816](https://github.com/svenso/sigrok_iso7816) with
|
||||
**live GSMTAP UDP streaming**, making a cheap FX2 logic analyzer (Saleae clone)
|
||||
behave like a SIMtrace2 sniffer:
|
||||
|
||||
```
|
||||
SIM card ⇄ reader (passive tap on the contacts)
|
||||
→ FX2 logic analyzer (fx2lafw, 8ch, 24MHz max)
|
||||
→ sigrok-cli + iso7816 decoder
|
||||
→ GSMTAP UDP :4729 → simtrace2-pysniff / Wireshark
|
||||
```
|
||||
|
||||
## The tap in practice
|
||||
|
||||

|
||||
*Components used in the build*
|
||||
|
||||

|
||||
*The rig tapping a phone SIM session*
|
||||
|
||||

|
||||
*The rig tapping a card-reader session*
|
||||
|
||||

|
||||
*The SIM-tap adapter*
|
||||
|
||||

|
||||
*Tap wiring on the SIM contacts*
|
||||
|
||||

|
||||
*The short-cable tap — reduces loop area and rise-time artifacts*
|
||||
|
||||

|
||||
*Short-cable wiring detail*
|
||||
|
||||

|
||||
*Short cable with the breakout gear*
|
||||
|
||||

|
||||
*Short cable with external pull resistors — 10 kΩ to GND on VCC (pulls low when off), 47 kΩ to VCC on DATA (faster rise to high)*
|
||||
|
||||
## What was added on top of upstream
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Live GSMTAP stream | Every decoded event is sent as a GSMTAP-SIM UDP packet (default on, mimics simtrace2-sniff) |
|
||||
| Correct sub_types | ATR=0x01 and PPS=0x02 now tagged properly in PCAP output (upstream hardcoded everything to APDU=0x00) |
|
||||
| ATR/PPS in PCAP | ATR and combined PPS request+response are emitted as PCAP packets |
|
||||
| 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 |
|
||||
|
||||
## Decoder options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `clock_option` | `native` | `native`, `detect`, `sample_as_clock` (see upstream FAQ) |
|
||||
| `protocol` | `auto` | `auto`, `T=0`, `T=1` |
|
||||
| `gsmtap_enable` | `true` | Live GSMTAP UDP streaming |
|
||||
| `gsmtap_host` | `127.0.0.1` | GSMTAP destination host |
|
||||
| `gsmtap_port` | `4729` | GSMTAP destination port |
|
||||
| `pcap_file` | *(empty)* | Write PCAP to this file in addition to binary output |
|
||||
| `rst_detect` | `false` | Track RST pin events (requires the `rst` channel to be assigned) |
|
||||
| `vcc_detect` | `false` | Track VCC pin events (requires the `vcc` channel to be assigned) |
|
||||
|
||||
Channels: `clk`, `data` (required); `rst`, `vcc` (optional — assign only when
|
||||
the corresponding wire is physically connected).
|
||||
|
||||
## GSMTAP events
|
||||
|
||||
GSMTAP header: version 2, type SIM (0x04), UDP port 4729.
|
||||
|
||||
| sub_type | Name | Payload |
|
||||
|----------|------|---------|
|
||||
| 0x00 | APDU | raw T=0/T=1 packet bytes |
|
||||
| 0x01 | ATR | raw ATR bytes |
|
||||
| 0x02 | PPS | PPS request + response bytes concatenated (deviation, see below) |
|
||||
| 0x10 | RST event (custom) | `[direction, level, 0x00]` |
|
||||
| 0x11 | VCC event (custom) | `[direction, level, 0x00]` |
|
||||
|
||||
Event payload byte semantics:
|
||||
- RST: `direction` 1 = reset asserted (high→low), 0 = deasserted (low→high);
|
||||
`level` = line level after the transition.
|
||||
- VCC: `direction` 1 = power applied (low→high), 0 = removed (high→low);
|
||||
`level` = line level after the transition.
|
||||
|
||||
0x00/0x01 match libosmocore's gsmtap.h: standard values and payloads.
|
||||
0x02 reuses the spec's `GSMTAP_SIM_PPS_REQ` value but carries the request
|
||||
and response bytes concatenated in one packet; the spec splits PPS into
|
||||
`PPS_REQ` (0x02) / `PPS_RSP` (0x03), so Wireshark dissects these are
|
||||
"PPS request" and `PPS_RSP` (0x03) is never sent. 0x10/0x11 are custom
|
||||
extensions not defined in libosmocore's gsmtap.h.
|
||||
|
||||
Header extensions: flags such as `GSMTAP_FLAG_BAD_FCS` are written into
|
||||
the header `res` byte, which official gsmtap.h marks reserved (RFU) —
|
||||
flag usage there follows the simtrace2-sniff convention and is not part
|
||||
of the libosmocore spec.
|
||||
|
||||
## Usage
|
||||
|
||||
Quick start after cloning (see Installation below):
|
||||
|
||||
```bash
|
||||
./setup.sh # check sigrok-cli, register decoder symlink
|
||||
./start.sh # capture + GSMTAP stream to 127.0.0.1:4729
|
||||
```
|
||||
|
||||
`start.sh` defaults: pins D5=CLK, D0=I/O, D4=RST, D2=VCC, 16 MHz samplerate.
|
||||
Override via args:
|
||||
|
||||
```bash
|
||||
./start.sh --clk=D1 --data=D6 # different wiring
|
||||
./start.sh --no-rst --no-vcc # CLK+I/O only
|
||||
./start.sh --host=10.0.28.7 # stream to a remote analyser
|
||||
./start.sh --pcap=session.pcap # also record decoded events to PCAP
|
||||
./start.sh --debug # verbose libsigrokdecode logging
|
||||
```
|
||||
|
||||
### Detecting the wiring (`tools/detect_pins.py`)
|
||||
|
||||
Not sure which FX2 channel carries which SIM signal? The probe loops
|
||||
short captures until the wiring map is complete:
|
||||
|
||||
```bash
|
||||
python3 tools/detect_pins.py # live probe (default): trigger a
|
||||
# SIM reset when prompted
|
||||
python3 tools/detect_pins.py capture.sr # or analyze an existing capture
|
||||
```
|
||||
|
||||
Each segment fingerprints every channel (toggle rate, idle level, median
|
||||
inter-transition gap, first-rise ordering across VCC -> RST -> ATR).
|
||||
Ambiguous static lines are reported as such instead of guessed; the run
|
||||
ends with the detected mapping and a ready-to-paste `start.sh` command
|
||||
line -- partial mappings still print usable flags for the known channels.
|
||||
Requires numpy.
|
||||
|
||||
> **GND is not detectable.** It carries no signal to fingerprint, so the
|
||||
> tool cannot find or verify it. Plug it first and check it manually
|
||||
> (continuity meter or known-good contact) -- a missing or loose ground
|
||||
> masquerades as every other fault at once: floating lines, phantom
|
||||
> start bits, all-zero frames.
|
||||
|
||||
### Robustness against noisy signals
|
||||
|
||||
Live captures can glitch (a phantom start bit during power-up used to
|
||||
shift the whole ATR parse off-by-one). The decoder now:
|
||||
|
||||
- validates the ATR's TS byte (`0x3B`/`0x3F` per TS 102 221 §5.2.2) and
|
||||
resyncs on the next real falling edge if it is bogus;
|
||||
- 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.
|
||||
|
||||
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.
|
||||
512/16 = 32), matching both sides' new UART rate. Live capture
|
||||
confirmed that keeping 372 after PPS aliases all subsequent traffic.
|
||||
|
||||
A **warm reset** (the reader re-asserts RST every ~1.6 s) makes the card
|
||||
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
|
||||
`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.
|
||||
|
||||
### Troubleshooting live capture
|
||||
|
||||
`start.sh` runs a single `--continuous` capture until you press **Ctrl+C**
|
||||
(`rc=130`). stdin is detached from sigrok-cli, so pressing keys/Enter in
|
||||
the terminal does **not** stop it (sigrok's "press any key to stop
|
||||
acquisition" prompt is disabled) — that keypress was previously
|
||||
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
|
||||
debounce, T=0 exchange reassembly). A clean capture now decodes to zero
|
||||
garbage — see the `vs_reader.py` validation below.
|
||||
|
||||
If a capture *does* end on its own (`rc=0`), the device genuinely stopped
|
||||
streaming; the FX2's USB bandwidth ceiling at sustained high sample rates
|
||||
is the usual suspect.
|
||||
|
||||
```bash
|
||||
./start.sh --pcap=s.pcap --log=s.log # also record decoded events, log rc=
|
||||
./start.sh ... --samplerate=24M # more timing headroom if needed
|
||||
./start.sh ... --klog # also record kernel journal
|
||||
./start.sh ... --debug # libsigrok internals at -l 4
|
||||
```
|
||||
|
||||
`start.sh` captures a continuous stream at the configured rate (default 16
|
||||
MHz) until Ctrl+C (`rc=130`). A healthy capture never ends by itself; if the
|
||||
FX2 drops the USB transfer mid-session (a flaky device, `rc=0` "device
|
||||
stopped streaming"), pass `--loop` to auto-restart — it re-launches the
|
||||
capture on every `rc=0` until you press Ctrl+C or hit `--max-restarts=N`
|
||||
(default 20; `0` = unlimited), with a `--restart-delay=S` gap (default 2 s).
|
||||
|
||||
- `--loop` auto-restarts the capture when it ends on its own (`rc=0`).
|
||||
- `--max-restarts=N` caps restarts (default 20; `0` = unlimited).
|
||||
- `--restart-delay=S` seconds to wait between restarts (default 2).
|
||||
- `--log` captures the `rc=` verdict line for the session end.
|
||||
- The decoder logs `SIGNAL INTEGRITY WARNING` when >= 8 of the last
|
||||
64 frames fail parity.
|
||||
- Desynced TPDUs (mis-framed exchanges) are kept but flagged with
|
||||
`GSMTAP_FLAG_BAD_FCS` in the GSMTAP header `res` byte — visible in
|
||||
Wireshark as a BAD-FCS warning.
|
||||
- A T=0 header with a structurally invalid INS (ISO 7816-4 §12.1.1:
|
||||
`0x00`, `0x6x`/`0x9x`, or odd LSB) is almost always a one-byte shift from
|
||||
a stray leading byte (e.g. a leftover SW2 read after a PACKETEND).
|
||||
Instead of silently dropping the whole 5-byte window (which let the next
|
||||
byte become a bogus CLA and swallow the remaining dialog into one CONCAT
|
||||
packet), the decoder drops only the leading byte and re-frames the rest,
|
||||
recovering the real command. Every consumed-but-unemitted byte is
|
||||
explicitly logged (`discarded byte 0x…: reason`); idle 0x00/0xFF discards
|
||||
are rate-limited, non-idle discards always produce a decoder annotation.
|
||||
|
||||
Performance: in native CLK mode the decoder calibrates samples per CLK
|
||||
edge once from the ATR's first frame, then advances bit timing with
|
||||
single sample-skip waits instead of one wait per CLK edge. The PPS speed
|
||||
switch is followed via `clock_skip = F/D`; there is no post-PPS or
|
||||
parity-triggered recalibration.
|
||||
|
||||
### Mode comparison on FX2 hardware (A/B result)
|
||||
|
||||
- `native` decoded real traffic (ATR, PPS switch, 100+ valid APDUs in a
|
||||
12 s window); the one persistent artifact was the VCC sense line
|
||||
"bouncing" — the ~1 kHz ringing the 10 ms debounce now suppresses.
|
||||
- `detect` never produced a usable session on this rig: it missed the
|
||||
PPS speed switch entirely (no PPS handling fired), aliasing all
|
||||
post-PPS traffic; a second attempt caught a dead I/O line.
|
||||
|
||||
`native` stays the default. Use `--clock=detect` only for offline
|
||||
experiments without PPS. A run full of all-zero frames means the tap
|
||||
lost contact with a line.
|
||||
|
||||
Parity is **even** per TS 102 221 ("an even number of bits set to '1'
|
||||
including the parity bit"); frames failing the check are flagged as
|
||||
`CHKSUM ERROR` but still decoded.
|
||||
|
||||
Equivalent raw sigrok-cli commands:
|
||||
|
||||
```bash
|
||||
# Minimal: CLK+I/O only, live stream to localhost:4729
|
||||
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
|
||||
-P iso7816:clk=D0:data=D1:clock_option=detect -A iso7816
|
||||
|
||||
# Full wiring: CLK + I/O + RST + VCC with line event tracking
|
||||
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
|
||||
-P iso7816:clk=D5:data=D0:rst=D4:vcc=D2:clock_option=native:rst_detect=true:vcc_detect=true \
|
||||
-A iso7816
|
||||
|
||||
# Stream to a remote analyser / disable streaming / write PCAP file
|
||||
... -P iso7816:...:gsmtap_host=10.0.28.7 ...
|
||||
... -P iso7816:...:gsmtap_enable=false ...
|
||||
... -P iso7816:...:pcap_file=trace.pcap ...
|
||||
|
||||
# Classic upstream mode: binary PCAP to stdout
|
||||
sigrok-cli -i capture.sr -P iso7816:clk=2:data=0 -B iso7816=pcap > out.pcap
|
||||
```
|
||||
|
||||
### Pin mapping
|
||||
|
||||
The FX2 clone channels are assigned via `-P iso7816:clk=N:data=N:rst=N:vcc=N`
|
||||
(no re-plugging needed when wires are mixed up). Default tap wiring used by
|
||||
`start.sh`:
|
||||
|
||||
| FX2 channel | SIM contact | Signal |
|
||||
|-------------|-------------|--------|
|
||||
| D0 | C7 | I/O |
|
||||
| D2 | C1 | VCC (digital level only!) |
|
||||
| D4 | C2 | RST |
|
||||
| D5 | C3 | CLK |
|
||||
| GND | C5 | GND |
|
||||
|
||||
FX2 input threshold is TTL-ish (~1.4V): 1.8V-class cards may read unreliably;
|
||||
3V/5V classes are fine.
|
||||
|
||||
**Samplerate and 8-bit mode (important):** the FX2's nominal 24 MHz is only
|
||||
available when sampling in 8-bit mode, i.e. when **only channels D0–D7 are
|
||||
enabled**. Enabling the 9th channel (A0/analog on CWAV AX clones) or any
|
||||
analog input forces 16-bit collection, which libsigrok caps at 12 MHz.
|
||||
`start.sh`/`start.bat` therefore pass `-C D0,D1,D2,D3,D4,D5,D6,D7`
|
||||
explicitly and default to **16 MHz** (comfortably under the 24 MHz USB
|
||||
bandwidth ceiling; ~3.3 samples per 4.8 MHz clock cycle). `setup.sh`/
|
||||
`setup.bat` probe and report your hardware's actual maximum under the
|
||||
same restriction.
|
||||
|
||||
## Installation
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||
cd sigrok_iso7816_stream
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
`setup.sh` verifies sigrok-cli and the FX2 firmware package
|
||||
(`sigrok-firmware-fx2lafw`) are installed, symlinks the repo into
|
||||
`~/.local/share/libsigrokdecode/decoders/iso7816` (user-local decoders shadow
|
||||
system-wide ones), and probes the connected device for its maximum supported
|
||||
samplerate.
|
||||
|
||||
### Windows
|
||||
|
||||
Prerequisites:
|
||||
|
||||
1. **Python 3.8+** from python.org ("Add to PATH" during install), then:
|
||||
`pip install numpy`
|
||||
2. **sigrok-cli** Windows installer (0.7.2 x86_64) from
|
||||
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads).
|
||||
If it fails to start with error `0xc0150002`, install the
|
||||
[Visual C++ 2010 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=26999)
|
||||
(`msvcr100.dll` is required by the bundled Python runtime).
|
||||
3. fx2lafw firmware ships bundled with the installer — no separate download.
|
||||
|
||||
Driver setup (FX2 dongles need WinUSB, the vendor driver won't work):
|
||||
|
||||
- Run **Zadig**, which ships *inside* the sigrok-cli installation directory
|
||||
(also reachable from the Start menu). Select the FX2 device in its dropdown
|
||||
and install the **WinUSB** driver.
|
||||
- After firmware upload the device re-enumerates with a different VID/PID —
|
||||
if it still isn't found, run Zadig a **second time** for the re-enumerated
|
||||
entry.
|
||||
- Windows binds drivers per USB port when no serial number is present:
|
||||
moving the dongle to another port may require repeating Zadig.
|
||||
|
||||
Then:
|
||||
|
||||
```bat
|
||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||
cd sigrok_iso7816_stream
|
||||
setup.bat
|
||||
```
|
||||
|
||||
`setup.bat` verifies sigrok-cli and bundled firmware, registers this repo's
|
||||
decoder via a junction into `%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816`
|
||||
(confirmed by `sigrok-cli -L` listing `iso7816`), checks Python/numpy for
|
||||
`tools\detect_pins.py`, and probes the device's accepted samplerates.
|
||||
|
||||
Capture works the same as on Linux:
|
||||
|
||||
```bat
|
||||
tools\detect_pins.py rem find the wiring (loops segments until done)
|
||||
start.bat rem capture + GSMTAP stream
|
||||
```
|
||||
|
||||
Note: batch files need CRLF line endings; the repo enforces this via
|
||||
`.gitattributes`, so a fresh clone on Windows is already correct.
|
||||
|
||||
## RST/VCC detection notes and limitations
|
||||
|
||||
- Events are detected **asynchronously before the first ATR** (power-on /
|
||||
reset release are captured once the level is stable) and **at packet
|
||||
boundaries** between T=0/T=1 packets afterwards.
|
||||
- Edges occurring entirely *within* a packet transfer are observed as a net
|
||||
level change at the next boundary; short pulses inside a packet can be missed.
|
||||
- VCC is digital-only: the decoder sees powered/not-powered, never the
|
||||
voltage class (1.8/3/5V).
|
||||
- **RST and VCC use level-stability hysteresis at 10 ms** (samplerate-scaled):
|
||||
a new level is reported only once it has been continuously stable for ≥ 10 ms
|
||||
— either because it persists that long, or because it ends after being stable
|
||||
that long. This makes power-up and power-down *symmetric*: a VCC sense line
|
||||
that **rings on the rising edge** (noisy/floating tap) no longer loses its
|
||||
"VCC on" event — it fires once the line settles HIGH, instead of being eaten
|
||||
by edge debounce. RST uses the same scheme, so a brief RST glitch no longer
|
||||
triggers a phantom warm-reset re-arm.
|
||||
- **VCC events are generally unreliable — prefer not to track VCC.** A floating
|
||||
or unconnected VCC sense pin reads HIGH at all times (logic-high with no load),
|
||||
so the decoder sees "powered" constantly and emits spurious power events on
|
||||
every bounce. Unless your tap derives VCC from a clean, loaded source, it is
|
||||
better to **leave the `vcc` channel unassigned** (do not pass `--vcc`) and keep
|
||||
`vcc_detect=false`; RST tracking is usually enough to see resets. Only enable
|
||||
VCC tracking on a tap with a solid VCC sense (and a solid GND — see wiring notes).
|
||||
- Unassigned optional channels are ignored even if `rst_detect`/`vcc_detect`
|
||||
is enabled (guarded via `has_channel()`).
|
||||
|
||||
## Testing
|
||||
|
||||
Verified against the upstream example captures:
|
||||
|
||||
```bash
|
||||
# ATR+PPS+54 APDUs from the test_8 reader capture (~19s decode)
|
||||
# GSMTAP stream verified with tshark (ATR/APDU dissected natively),
|
||||
# custom 0x10/0x11 events verified with a UDP listener, and end-to-end
|
||||
# into simtrace2-pysniff server (POST /api/capture/start → decode → stop).
|
||||
```
|
||||
|
||||
A synthetic VCD generator covering RST/VCC edge cases lives in the project
|
||||
notes (`projects/SIGROK_ISO7816.md` in the parent workspace).
|
||||
|
||||
### Validation against the reader SDK (`tools/vs_reader.py`)
|
||||
|
||||
On a controlled rig (PCSC reader driven by software) the reference APDU log
|
||||
from the reader SDK can be diffed against the decoded PCAP:
|
||||
|
||||
```bash
|
||||
sigrok-cli -i <trace>.sr \
|
||||
-P iso7816:clk=D5:data=D0:rst=D4:vcc=D2:clock_option=native:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=out.pcap \
|
||||
-A iso7816
|
||||
python3 tools/vs_reader.py <reader-log>.log out.pcap
|
||||
```
|
||||
|
||||
`vs_reader.py` aligns each reader-log exchange to the capture APDUs
|
||||
(byte-exact LCS match) and classifies the leftovers as proactive/CAT
|
||||
(unseen by the reader SDK), pre-capture/dropped, or garbage.
|
||||
The mid-session capture whose warm resets previously produced 4 garbage
|
||||
events — with the re-arm + start-bit guard it decodes to **0 garbage**
|
||||
(`RESULT: OK - capture fully explained`).
|
||||
|
||||
test_7 (4 ATR resets fragmenting the session) now captures all 37/37 reader
|
||||
exchanges (up from 10/37 before the deglitch fix). Samsung phone traces with
|
||||
16 CLK/ETU (F=512/D=32) now de-concatenate to 924 individual T=0 exchanges
|
||||
(previously merged into 52 oversized APDUs).
|
||||
|
||||
Replayable trace fixtures (kept locally, outside the repo) cover
|
||||
controlled-rig reader sessions (with their reader-SDK ground-truth logs),
|
||||
phone captures with gated CLK, and **cold-boot captures** that start
|
||||
*before* power-up (VCC/RST low at t=0). The four Samsung fixtures are
|
||||
cold boots taken with a 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; the `sim2` variants are cropped to the power-on window.
|
||||
|
||||
### Cold-boot captures (capture starts before power-up)
|
||||
|
||||
When the capture starts pre-power-up, the decoder must arm the ATR
|
||||
hunt because RST is still low at sample 0: it primes the initial
|
||||
VCC/RST levels on the first step (instead of defaulting them to deasserted),
|
||||
and on an RST-low-at-start it enters `FIND START` with a cleared ETU, so the
|
||||
pre-ATR power-up DATA transitions cannot lock a bogus bit rate. It then
|
||||
decodes the full ATR (`3b9f96801fc78031e073fe2117574a3382033339001e`) with
|
||||
0 garbage:
|
||||
|
||||
```bash
|
||||
sigrok-cli -i <coldboot>.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=out.pcap \
|
||||
-A iso7816
|
||||
```
|
||||
|
||||
An RST deassert that lands while an ATR parse is already in progress no longer
|
||||
re-arms the hunt mid-read — it logs `RST deasserted (hunt/ATR already in
|
||||
progress)` and leaves the in-flight ATR alone (test_8/test_7 CHKSUM 18→0).
|
||||
|
||||
### Capture scenarios: reader (constant CLK) and phone (Clock Stop Mode)
|
||||
|
||||
The decoder handles the two physically different SIM sniffing setups:
|
||||
|
||||
- **Reader / constant-CLK capture** — the reader drives a steady CLK for the
|
||||
whole session. In mid-session mode the capture
|
||||
decodes to **0 garbage** (`RESULT: OK`) against the reader-SDK
|
||||
ground truth (`tools/vs_reader.py <reader-log>.log out.pcap`).
|
||||
Warm-reset reader captures use the synthetic-T=0-ATR fallback.
|
||||
|
||||
- **Phone / stopped-CLK capture** — the phone gates or stops CLK between
|
||||
transfers (Clock Stop Mode), so the ETU is not the 372-clock default and
|
||||
cannot be inferred from CLK. Run with `protocol=T=0`; the decoder recovers the ETU from the DATA line via edge
|
||||
counting, locks it, and decodes with the drift-immune edge-list bit reader.
|
||||
T=0 case 3/4 commands (e.g. `80 12`/`80 14` STK envelope) are consolidated
|
||||
into a single command→response APDU. A 2-sample start-bit deglitch eliminates
|
||||
sub-ETU LOW glitches caused by FX2LP internal pull-up artifacts, preventing
|
||||
byte misalignment on fast phone traces.
|
||||
|
||||
### Proven data rates and ETUs
|
||||
|
||||
All traces decoded at **16 MHz sample rate**, 0 garbage, RESULT: OK.
|
||||
Since v1.5.0 every example trace also reports **0 CONCAT** (the invalid-INS
|
||||
re-frame eliminated the last multi-exchange swallow family on
|
||||
4gmodem/A55_sim2/S21p_sim2).
|
||||
|
||||
| Trace | CLK mode | ETU (samples) | CLK/ETU | Convention | Notes |
|
||||
|-------|----------|---------------|---------|------------|-------|
|
||||
| test_8 | constant | ~43 | 372 (standard) | Direct | PC/SC reader, ATR+PPS+APDUs |
|
||||
| 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 |
|
||||
|
||||
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).
|
||||
No manual configuration is needed beyond `clock_option=native` and
|
||||
`protocol=T=0` for phone captures.
|
||||
|
||||
Recommended invocations:
|
||||
|
||||
```bash
|
||||
# Reader capture (controlled rig, CLK always running)
|
||||
sigrok-cli -i <trace>.sr \
|
||||
-P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=out.pcap \
|
||||
-A iso7816
|
||||
python3 tools/vs_reader.py <reader-log>.log out.pcap
|
||||
|
||||
# Phone capture (mid-session, gated/stopped CLK)
|
||||
sigrok-cli -i <phone>.sr \
|
||||
-P iso7816:clk=CLK:data=DATA:rst=RST:clock_option=native:rst_detect=true:vcc_detect=false:protocol=T=0:gsmtap_enable=false:pcap_file=out.pcap \
|
||||
-A iso7816
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0+, same as upstream.
|
||||
@@ -0,0 +1,25 @@
|
||||
##
|
||||
## This file is part of the libsigrokdecode project.
|
||||
##
|
||||
## Copyright (C) 2012 Uwe Hermann <uwe@hermann-uwe.de>
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License
|
||||
## along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
##
|
||||
|
||||
'''
|
||||
ISO7816 is a bidirectional, multi-master
|
||||
bus using two signals (CLK = serial clock line, DATA = IO data line).
|
||||
'''
|
||||
|
||||
from .pd import Decoder
|
||||
@@ -0,0 +1,99 @@
|
||||
##
|
||||
## GSMTAP live streaming for the ISO 7816 decoder.
|
||||
##
|
||||
## Sends decoded events as GSMTAP-SIM UDP packets, compatible with
|
||||
## simtrace2-sniff, simtrace2-pysniff and the GSMTAP dissector in
|
||||
## Wireshark.
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
|
||||
import socket
|
||||
import struct
|
||||
|
||||
GSMTAP_VERSION = 0x02
|
||||
GSMTAP_HDR_LEN = 4 # in 32-bit words (16 bytes)
|
||||
GSMTAP_TYPE_SIM = 0x04
|
||||
|
||||
# sub_type values. 0x00/0x01 are standard GSMTAP-SIM types with standard
|
||||
# payloads (libosmocore gsmtap.h). 0x02 uses the spec's GSMTAP_SIM_PPS_REQ
|
||||
# value but carries request + response bytes concatenated in one packet;
|
||||
# the spec splits PPS into PPS_REQ (0x02) / PPS_RSP (0x03), so Wireshark
|
||||
# dissects 0x02 as "PPS request" and GSMTAP_SIM_PPS_RSP (0x03) is
|
||||
# intentionally unused. 0x10/0x11 are custom extensions (not in the spec).
|
||||
GSMTAP_SIM_APDU = 0x00
|
||||
GSMTAP_SIM_ATR = 0x01
|
||||
GSMTAP_SIM_PPS = 0x02 # request + response combined (this decoder)
|
||||
GSMTAP_SIM_RST_EVENT = 0x10 # payload: [direction, level, reserved]
|
||||
GSMTAP_SIM_VCC_EVENT = 0x11 # payload: [direction, level, reserved]
|
||||
|
||||
# RST/VCC event payload bytes:
|
||||
# byte 0 (direction):
|
||||
# RST: 1 = reset asserted (high->low), 0 = reset deasserted (low->high)
|
||||
# VCC: 1 = power applied (low->high), 0 = power removed (high->low)
|
||||
# byte 1 (level): line level after the transition (0 = low, 1 = high)
|
||||
# byte 2: reserved (0x00)
|
||||
|
||||
GSMTAP_UDP_PORT = 4729
|
||||
|
||||
# libosmocore gsmtap.h: per-packet flags live in the header `res` byte,
|
||||
# which the spec marks reserved (RFU). Flag usage here is a documented
|
||||
# extension following the simtrace2-sniff convention, not part of gsmtap.h.
|
||||
GSMTAP_FLAG_BAD_FCS = 0x01 # "Any data checksum is wrong" -- reused here to
|
||||
# mark decoder-desynced TPDUs (see README.md,
|
||||
# "GSMTAP events").
|
||||
|
||||
_HDR_FMT = '!BBBBHBBIBBBB' # 16 bytes, big-endian
|
||||
|
||||
|
||||
def build_packet(sub_type, data, flags=0):
|
||||
'''Build a complete GSMTAP packet (header + payload) as bytes.
|
||||
|
||||
`flags` is written into the header `res` byte (GSMTAP_FLAG_*).'''
|
||||
hdr = struct.pack(
|
||||
_HDR_FMT,
|
||||
GSMTAP_VERSION, # version
|
||||
GSMTAP_HDR_LEN, # hdr_len (in 32-bit words)
|
||||
GSMTAP_TYPE_SIM, # type
|
||||
0, # timeslot
|
||||
0, # arfcn
|
||||
0, # noise_db
|
||||
0, # signal_db
|
||||
0, # frame_number
|
||||
sub_type, # sub_type
|
||||
0, # antenna_nr
|
||||
0, # sub_slot
|
||||
flags, # res (GSMTAP_FLAG_*)
|
||||
)
|
||||
return hdr + bytes(data)
|
||||
|
||||
|
||||
class GsmtapStreamSender:
|
||||
'''Fire-and-forget UDP sender.
|
||||
|
||||
Never raises on send: with no listener present, datagrams are
|
||||
silently dropped by the OS (same behavior as simtrace2-sniff).
|
||||
'''
|
||||
|
||||
def __init__(self, host='127.0.0.1', port=GSMTAP_UDP_PORT):
|
||||
self._addr = (host, int(port))
|
||||
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# UDP to localhost sendto is non-blocking anyway; make it explicit so
|
||||
# a full socket buffer can never stall the decode loop.
|
||||
self._sock.setblocking(False)
|
||||
|
||||
def send(self, sub_type, data, flags=0):
|
||||
try:
|
||||
self._sock.sendto(build_packet(sub_type, data, flags),
|
||||
self._addr)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 447 KiB |
|
After Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,129 @@
|
||||
@echo off
|
||||
rem sigrok_iso7816_stream setup (Windows): verify sigrok-cli, register decoder.
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
set "REPO_DIR=%~dp0"
|
||||
if "%REPO_DIR:~-1%"=="\" set "REPO_DIR=%REPO_DIR:~0,-1%"
|
||||
set "DEC_DIR=%LOCALAPPDATA%\libsigrokdecode\decoders"
|
||||
set "LINK=%DEC_DIR%\iso7816"
|
||||
|
||||
echo sigrok_iso7816_stream setup (Windows)
|
||||
echo.
|
||||
|
||||
rem ---- sigrok-cli -------------------------------------------------------
|
||||
echo Checking sigrok-cli...
|
||||
set "SIGROK_EXE="
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles%\sigrok\sigrok-cli\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles%\sigrok\sigrok-cli\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles(x86)%\sigrok\sigrok-cli\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles(x86)%\sigrok\sigrok-cli\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles%\sigrok\PulseView\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles%\sigrok\PulseView\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles(x86)%\sigrok\PulseView\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles(x86)%\sigrok\PulseView\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE where sigrok-cli >nul 2>&1 && for /f "delims=" %%i in ('where sigrok-cli') do set "SIGROK_EXE=%%i"
|
||||
if not defined SIGROK_EXE goto :notfound
|
||||
for %%i in ("!SIGROK_EXE!") do set "SIGROK_DIR=%%~dpi"
|
||||
if "!SIGROK_DIR:~-1!"=="\" set "SIGROK_DIR=!SIGROK_DIR:~0,-1!"
|
||||
echo [ok] !SIGROK_EXE!
|
||||
rem Add sigrok-cli dir to PATH so subsequent calls work even if only
|
||||
rem discovered at the default install location.
|
||||
set "PATH=!SIGROK_DIR!;!PATH!"
|
||||
sigrok-cli --version 2>nul | findstr /r "^"
|
||||
goto :sigrok_ok
|
||||
|
||||
:notfound
|
||||
echo [X] sigrok-cli not found.
|
||||
echo Searched:
|
||||
echo %ProgramFiles%\sigrok\sigrok-cli
|
||||
echo %ProgramFiles(x86)%\sigrok\sigrok-cli
|
||||
echo %ProgramFiles%\sigrok\PulseView
|
||||
echo %ProgramFiles(x86)%\sigrok\PulseView
|
||||
echo PATH
|
||||
echo Install it from https://sigrok.org/wiki/Downloads
|
||||
echo ^(sigrok-cli-0.7.2-x86_64-installer.exe^), then re-open this
|
||||
echo window so PATH changes take effect and re-run.
|
||||
exit /b 1
|
||||
|
||||
:sigrok_ok
|
||||
|
||||
rem If sigrok-cli fails to start with error 0xc0150002, install the
|
||||
rem Microsoft Visual C++ 2010 Redistributable (msvcr100.dll).
|
||||
|
||||
rem ---- firmware ---------------------------------------------------------
|
||||
echo Checking fx2lafw firmware...
|
||||
set "FW_FOUND="
|
||||
if not defined FW_FOUND if exist "!SIGROK_DIR!\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=!SIGROK_DIR!\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "!SIGROK_DIR!\..\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=!SIGROK_DIR!\..\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%ProgramFiles%\sigrok\sigrok-cli\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%ProgramFiles%\sigrok\sigrok-cli\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%ProgramFiles(x86)%\sigrok\sigrok-cli\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%ProgramFiles(x86)%\sigrok\sigrok-cli\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%ProgramFiles%\sigrok\PulseView\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%ProgramFiles%\sigrok\PulseView\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%ProgramFiles(x86)%\sigrok\PulseView\share\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%ProgramFiles(x86)%\sigrok\PulseView\share\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%LOCALAPPDATA%\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%LOCALAPPDATA%\sigrok-firmware"
|
||||
if not defined FW_FOUND if exist "%ProgramData%\sigrok-firmware\fx2lafw-cypress-fx2.fw" set "FW_FOUND=%ProgramData%\sigrok-firmware"
|
||||
if defined FW_FOUND (
|
||||
echo [ok] !FW_FOUND!
|
||||
) else (
|
||||
echo [!] bundled firmware directory not found - continuing anyway;
|
||||
echo the installer normally ships fx2lafw firmware, and libsigrok
|
||||
echo also searches %LOCALAPPDATA%\sigrok-firmware
|
||||
)
|
||||
|
||||
rem ---- decoder registration --------------------------------------------
|
||||
echo Registering decoder...
|
||||
if not exist "%DEC_DIR%" mkdir "%DEC_DIR%"
|
||||
if exist "%LINK%\pd.py" goto :link_ok
|
||||
if exist "%LINK%" rmdir "%LINK%"
|
||||
mklink /J "%LINK%" "%REPO_DIR%" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [X] could not create junction %LINK%
|
||||
exit /b 1
|
||||
)
|
||||
:link_ok
|
||||
sigrok-cli -L 2>nul | findstr /i "iso7816" >nul
|
||||
if errorlevel 1 (
|
||||
echo [X] decoder registered but NOT listed by sigrok-cli.
|
||||
echo Expected junction: %LINK%
|
||||
exit /b 1
|
||||
)
|
||||
echo [ok] %LINK% -^> listed by sigrok-cli
|
||||
|
||||
rem ---- python / numpy (for tools\detect_pins.py) ------------------------
|
||||
echo Checking Python + numpy...
|
||||
set "PY="
|
||||
py -3 --version >nul 2>&1 && set "PY=py -3"
|
||||
if not defined PY (
|
||||
python --version >nul 2>&1 && set "PY=python"
|
||||
)
|
||||
if defined PY (
|
||||
for /f "delims=" %%v in ('!PY! --version 2^>^&1') do echo [ok] %%v
|
||||
!PY! -c "import numpy" >nul 2>&1 || echo [!] numpy missing: run '!PY! -m pip install numpy'
|
||||
) else (
|
||||
echo [!] no python launcher found; detect_pins.py needs Python 3.8+ and numpy
|
||||
)
|
||||
|
||||
rem ---- hardware samplerate probe ---------------------------------------
|
||||
echo Probing device samplerates...
|
||||
set "MAX_RATE="
|
||||
for %%r in (1M 2M 4M 8M 12M 16M 20M 24M) do (
|
||||
sigrok-cli -d fx2lafw --config samplerate=%%r --continuous --time 20 ^
|
||||
-P "iso7816:clk=D0:data=D1" >"%TEMP%\iso7816-probe.txt" 2>&1
|
||||
type "%TEMP%\iso7816-probe.txt" | findstr /c:"Unable to claim" >nul && (
|
||||
echo blocked - another program holds the device
|
||||
goto :probedone
|
||||
)
|
||||
type "%TEMP%\iso7816-probe.txt" | findstr /c:"Failed to open" /c:"No devices found" >nul && (
|
||||
echo skipped - no FX2 device connected
|
||||
goto :probedone
|
||||
)
|
||||
type "%TEMP%\iso7816-probe.txt" | findstr /r /c:"invalid argument" /c:"Unable to sample" /c:"Could not start" >nul || set MAX_RATE=%%r
|
||||
)
|
||||
:probedone
|
||||
if defined MAX_RATE (
|
||||
echo Highest accepted samplerate: !MAX_RATE!
|
||||
) else if not defined MAX_RATE if not defined DEVICE_MSG (
|
||||
echo unknown - device present but no rate accepted
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Done. Next steps:
|
||||
echo 1. Plug GND first and verify contact manually (see README).
|
||||
echo 2. tools\detect_pins.py -- find the wiring
|
||||
echo 3. start.bat -- capture + GSMTAP stream
|
||||
endlocal
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# sigrok_iso7816_stream setup: verify sigrok-cli and register the decoder.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DECODERS_DIR="${HOME}/.local/share/libsigrokdecode/decoders"
|
||||
LINK="${DECODERS_DIR}/iso7816"
|
||||
FIRMWARE_DIRS=(
|
||||
/usr/share/sigrok-firmware
|
||||
/usr/share/sigrok/firmware
|
||||
/usr/local/share/sigrok-firmware
|
||||
)
|
||||
|
||||
printf 'Checking sigrok-cli... '
|
||||
if ! command -v sigrok-cli >/dev/null 2>&1; then
|
||||
echo "NOT FOUND"
|
||||
echo " Install it first, e.g.: sudo apt install sigrok-cli"
|
||||
exit 1
|
||||
fi
|
||||
echo "found ($(command -v sigrok-cli))"
|
||||
sigrok-cli --version 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
printf 'Checking FX2 firmware... '
|
||||
FW_FOUND=""
|
||||
for dir in "${FIRMWARE_DIRS[@]}"; do
|
||||
if compgen -G "${dir}/fx2lafw-*.fw" >/dev/null; then
|
||||
FW_FOUND="${dir}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${FW_FOUND}" ]; then
|
||||
echo "NOT FOUND"
|
||||
echo " fx2lafw firmware files are missing."
|
||||
echo " Arch/CachyOS: sudo pacman -S sigrok-firmware-fx2lafw"
|
||||
echo " Debian/Ubuntu: sudo apt install sigrok-firmware-fx2lafw"
|
||||
exit 1
|
||||
fi
|
||||
echo "ok (${FW_FOUND})"
|
||||
|
||||
printf 'Checking decoder directory... '
|
||||
mkdir -p "${DECODERS_DIR}"
|
||||
echo "ok (${DECODERS_DIR})"
|
||||
|
||||
printf 'Installing decoder symlink... '
|
||||
if [ -L "${LINK}" ]; then
|
||||
rm "${LINK}"
|
||||
elif [ -e "${LINK}" ]; then
|
||||
echo ""
|
||||
echo " ${LINK} exists and is not a symlink; refusing to overwrite." >&2
|
||||
exit 1
|
||||
fi
|
||||
ln -s "${REPO_DIR}" "${LINK}"
|
||||
echo "ok"
|
||||
|
||||
printf 'Verifying... '
|
||||
target="$(readlink -f "${LINK}")"
|
||||
if [ "${target}" = "${REPO_DIR}" ] && [ -f "${target}/pd.py" ]; then
|
||||
echo "ok (${LINK} -> ${target})"
|
||||
else
|
||||
echo "FAILED" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'Probing hardware max samplerate... '
|
||||
MAX_RATE=""
|
||||
DEVICE_MSG=""
|
||||
for rate in 1M 2M 4M 8M 12M 16M 20M 24M; do
|
||||
result="$(sigrok-cli -d fx2lafw --config "samplerate=${rate}" --continuous \
|
||||
--time 20 -P 'iso7816:clk=D0:data=D1' 2>&1 || true)"
|
||||
if echo "${result}" | grep -q "Unable to claim"; then
|
||||
DEVICE_MSG="blocked (another program holds the device; stop it and re-run)"
|
||||
break
|
||||
fi
|
||||
if echo "${result}" | grep -q "Failed to open device\|No devices found"; then
|
||||
DEVICE_MSG="skipped (no FX2 device connected)"
|
||||
break
|
||||
fi
|
||||
if ! echo "${result}" | grep -qE "invalid argument|Unable to sample|Could not start"; then
|
||||
MAX_RATE="${rate}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${DEVICE_MSG}" ]; then
|
||||
echo "${DEVICE_MSG}"
|
||||
elif [ -n "${MAX_RATE}" ]; then
|
||||
echo "${MAX_RATE}"
|
||||
if [ "${MAX_RATE}" != "24M" ]; then
|
||||
echo " Device rejects rates above ${MAX_RATE}. Rates up to 24 MHz need"
|
||||
echo " 8-bit sampling: only channels D0-D7 may be enabled (enabling"
|
||||
echo " A0/analog forces 16-bit collection, capped at 12 MHz)."
|
||||
echo " start.sh restricts channels accordingly."
|
||||
fi
|
||||
else
|
||||
echo "unknown (device present but no rate accepted)"
|
||||
fi
|
||||
|
||||
echo "Done. Run ./start.sh to begin capture."
|
||||
@@ -0,0 +1,104 @@
|
||||
@echo off
|
||||
rem sigrok_iso7816_stream launcher (Windows): capture ISO 7816 and stream
|
||||
rem GSMTAP to :4729. Flag-for-flag equivalent of start.sh.
|
||||
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
|
||||
set "CLK=D5"
|
||||
set "DATA=D0"
|
||||
set "RST=D4"
|
||||
set "VCC=D2"
|
||||
set "SAMPLERATE=16M"
|
||||
set "CLOCK=native"
|
||||
set "HOST=127.0.0.1"
|
||||
set "PORT=4729"
|
||||
set "PCAP="
|
||||
set "DEBUG="
|
||||
|
||||
:argloop
|
||||
if "%~1"=="" goto :argsdone
|
||||
if /i "%~1"=="--clk" ( set "CLK=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--data" ( set "DATA=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--rst" ( set "RST=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--vcc" ( set "VCC=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--samplerate" ( set "SAMPLERATE=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--clock" ( set "CLOCK=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--host" ( set "HOST=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--port" ( set "PORT=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--pcap" ( set "PCAP=%~2" & shift & shift & goto :argloop )
|
||||
if /i "%~1"=="--debug" ( set "DEBUG=1" & shift & goto :argloop )
|
||||
if /i "%~1"=="--no-rst" ( set "RST=" & shift & goto :argloop )
|
||||
if /i "%~1"=="--no-vcc" ( set "VCC=" & shift & goto :argloop )
|
||||
if /i "%~1"=="-h" ( goto :usage )
|
||||
if /i "%~1"=="--help" ( goto :usage )
|
||||
echo Unknown option: %~1
|
||||
exit /b 1
|
||||
|
||||
:usage
|
||||
echo Usage: %~n0 [OPTIONS]
|
||||
echo --clk=CH CLK channel default: %CLK%
|
||||
echo --data=CH I/O channel default: %DATA%
|
||||
echo --rst=CH RST channel default: %RST%
|
||||
echo --vcc=CH VCC channel default: %VCC%
|
||||
echo --samplerate=SR Sample rate default: %SAMPLERATE%
|
||||
echo --clock=MODE native/detect/sample_as_clock
|
||||
echo --host=IP GSMTAP destination default: 127.0.0.1
|
||||
echo --port=N GSMTAP port default: 4729
|
||||
echo --pcap=FILE Also write decoded events to FILE (no spaces in path)
|
||||
echo --debug Verbose libsigrokdecode logging (-l 4)
|
||||
echo --no-rst Disable RST tracking
|
||||
echo --no-vcc Disable VCC tracking
|
||||
echo.
|
||||
echo To log output on Windows use redirection: %~n0 ... ^> capture.log 2^>^&1
|
||||
exit /b 0
|
||||
|
||||
:argsdone
|
||||
set "SIGROK_EXE="
|
||||
rem Try known install locations (order: 64-bit, 32-bit, then PATH)
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles%\sigrok\sigrok-cli\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles%\sigrok\sigrok-cli\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles(x86)%\sigrok\sigrok-cli\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles(x86)%\sigrok\sigrok-cli\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles%\sigrok\PulseView\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles%\sigrok\PulseView\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE if exist "%ProgramFiles(x86)%\sigrok\PulseView\sigrok-cli.exe" set "SIGROK_EXE=%ProgramFiles(x86)%\sigrok\PulseView\sigrok-cli.exe"
|
||||
if not defined SIGROK_EXE where sigrok-cli >nul 2>&1 && for /f "delims=" %%i in ('where sigrok-cli') do set "SIGROK_EXE=%%i"
|
||||
if not defined SIGROK_EXE goto :notfound
|
||||
for %%i in ("!SIGROK_EXE!") do set "SIGROK_DIR=%%~dpi"
|
||||
if "!SIGROK_DIR:~-1!"=="\" set "SIGROK_DIR=!SIGROK_DIR:~0,-1!"
|
||||
set "PATH=!SIGROK_DIR!;!PATH!"
|
||||
goto :found
|
||||
|
||||
:notfound
|
||||
echo [X] sigrok-cli not found.
|
||||
echo Searched:
|
||||
echo %ProgramFiles%\sigrok\sigrok-cli
|
||||
echo %ProgramFiles(x86)%\sigrok\sigrok-cli
|
||||
echo %ProgramFiles%\sigrok\PulseView
|
||||
echo %ProgramFiles(x86)%\sigrok\PulseView
|
||||
echo PATH
|
||||
echo Install from https://sigrok.org/wiki/Downloads
|
||||
exit /b 1
|
||||
|
||||
:found
|
||||
|
||||
set "OPTS=clk=%CLK%:data=%DATA%:clock_option=%CLOCK%"
|
||||
set "OPTS=%OPTS%:gsmtap_host=%HOST%:gsmtap_port=%PORT%"
|
||||
if defined RST set "OPTS=%OPTS%:rst=%RST%:rst_detect=true"
|
||||
if defined VCC set "OPTS=%OPTS%:vcc=%VCC%:vcc_detect=true"
|
||||
if defined PCAP set "OPTS=%OPTS%:pcap_file=%PCAP%"
|
||||
|
||||
set "CMDLINE=sigrok-cli -d fx2lafw --config samplerate=%SAMPLERATE% --continuous -C D0,D1,D2,D3,D4,D5,D6,D7"
|
||||
if defined DEBUG set "CMDLINE=%CMDLINE% -l 4"
|
||||
set "CMDLINE=%CMDLINE% -P iso7816:%OPTS% -A iso7816"
|
||||
|
||||
echo ISO 7816 streamer - clk=%CLK% data=%DATA% clock=%CLOCK%
|
||||
if defined RST echo RST tracking on channel %RST%
|
||||
if defined VCC echo VCC tracking on channel %VCC%
|
||||
if defined PCAP echo PCAP output: %PCAP%
|
||||
echo Samplerate: %SAMPLERATE% ^| GSMTAP -^> %HOST%:%PORT%
|
||||
echo Capture runs until you press Ctrl+C.
|
||||
echo.
|
||||
|
||||
rem <NUL detaches stdin (best-effort to disable sigrok's "press any key
|
||||
rem to stop acquisition" prompt); not verified on Windows, where the
|
||||
rem prompt may read the console rather than stdin.
|
||||
%CMDLINE% <NUL
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,318 @@
|
||||
#!/bin/bash
|
||||
# sigrok_iso7816_stream launcher: capture ISO 7816 and stream GSMTAP to :4729.
|
||||
set -euo pipefail
|
||||
|
||||
CLK=D5
|
||||
DATA=D0
|
||||
RST=D4
|
||||
VCC=D2
|
||||
SAMPLERATE=16M
|
||||
CLOCK=native
|
||||
HOST=127.0.0.1
|
||||
PORT=4729
|
||||
PCAP=""
|
||||
DEBUG=""
|
||||
LOG=""
|
||||
KLOG=""
|
||||
LOOP=0 # --loop: restart on rc=0 (device stopped streaming)
|
||||
MAX_RESTARTS=20 # --max-restarts=N (0 = unlimited)
|
||||
RESTART_DELAY=2 # --restart-delay=S seconds between restarts
|
||||
PROTOCOL="T=0" # --protocol: T=0 (default; live phone captures start
|
||||
# mid-session, so we recover the ETU from the live line
|
||||
# and frame full command+response APDU exchanges), T=1,
|
||||
# or auto
|
||||
# Records which channel pins the user explicitly named via --clk/--data/
|
||||
# --rst/--vcc. When ANY of these is given, defaults are NOT injected for the
|
||||
# remaining channels (a default RST/VCC pin could clash with a user pin), so
|
||||
# only the named channels are used. --no-rst/--no-vcc are toggles and do not
|
||||
# trigger this (they keep the other defaults).
|
||||
CLK_SET=""; DATA_SET=""; RST_SET=""; VCC_SET=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--clk=CH CLK channel (default: ${CLK})
|
||||
--data=CH I/O channel (default: ${DATA})
|
||||
--rst=CH RST channel (default: ${RST})
|
||||
--vcc=CH VCC channel (default: ${VCC})
|
||||
Note: naming any of --clk/--data/--rst/--vcc disables the other channels'
|
||||
defaults -- only the channels you name are used (no injected pins to clash).
|
||||
Use --no-rst/--no-vcc to drop RST/VCC while keeping the remaining defaults.
|
||||
--samplerate=SR Sample rate (default: ${SAMPLERATE}). Override to
|
||||
24M if you need the extra timing headroom; 16M is the
|
||||
FX2's comfortable sustained USB rate.
|
||||
--clock=MODE clock_option (default: ${CLOCK})
|
||||
native spec timing, counts CLK edges; wants
|
||||
~2+ samples per CLK period
|
||||
detect measures cycles/bit from the ATR;
|
||||
upstream calls it "most stable"
|
||||
sample_as_clock experimental
|
||||
--host=IP GSMTAP destination (default: ${HOST})
|
||||
--port=N GSMTAP port (default: ${PORT})
|
||||
--pcap=FILE Also write decoded events to FILE (offline analysis)
|
||||
--debug Verbose libsigrokdecode logging (-l 4)
|
||||
--log=FILE Tee all output into FILE (postmortem / exit diagnosis)
|
||||
--klog[=FILE] Also record the kernel journal (journalctl -kf) to
|
||||
FILE (default: klog.log) -- needs journal read
|
||||
permission; captures xHCI/USB errors at exit moments.
|
||||
--no-rst Disable RST tracking
|
||||
--no-vcc Disable VCC tracking
|
||||
--protocol=MODE ISO 7816 protocol for mid-session decode: T=0 (default),
|
||||
T=1, or auto. Live phone captures start mid-session, so a
|
||||
fixed protocol lets the decoder emit raw bursts instead of
|
||||
hunting for a (non-existent) ATR.
|
||||
--loop Auto-restart when the capture ends on its own (rc=0, device
|
||||
stopped streaming). Loops until you press Ctrl+C (rc=130)
|
||||
or a real error. Useful for a flaky FX2 that drops the USB
|
||||
transfer mid-session.
|
||||
--max-restarts=N Cap --loop restarts (default: 20; 0 = unlimited).
|
||||
--restart-delay=S Seconds to wait between restarts (default: 2).
|
||||
-h, --help Show this help
|
||||
|
||||
GSMTAP stream defaults to 127.0.0.1:4729 (simtrace2-pysniff compatible).
|
||||
|
||||
Capture runs continuously until Ctrl+C. stdin is detached from sigrok-cli
|
||||
so pressing keys/Enter in the terminal does NOT stop the capture (sigrok's
|
||||
"press any key to stop acquisition" prompt is disabled).
|
||||
|
||||
NOTE: clock_option=native counts CLK edges, so the samplerate must stay
|
||||
above ~2 samples per CLK period (>= 8 MHz for a 3.579 MHz card clock).
|
||||
Sustained 12 MHz capture can overrun FX2 USB buffers -> dropped sample
|
||||
chunks appear as torn/fragmented packets; lower rates reduce that risk.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
norm_proto() {
|
||||
# Normalize a --protocol value to the decoder's accepted form (T=0 / T=1 / auto).
|
||||
case "$1" in
|
||||
T=0|T0|t0) echo "T=0" ;;
|
||||
T=1|T1|t1) echo "T=1" ;;
|
||||
auto|AUTO) echo "auto" ;;
|
||||
*) echo "auto" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--clk=*) CLK="${arg#*=}"; CLK_SET=1 ;;
|
||||
--data=*) DATA="${arg#*=}"; DATA_SET=1 ;;
|
||||
--rst=*) RST="${arg#*=}"; RST_SET=1 ;;
|
||||
--vcc=*) VCC="${arg#*=}"; VCC_SET=1 ;;
|
||||
--samplerate=*) SAMPLERATE="${arg#*=}" ;;
|
||||
--clock=*) CLOCK="${arg#*=}" ;;
|
||||
--host=*) HOST="${arg#*=}" ;;
|
||||
--port=*) PORT="${arg#*=}" ;;
|
||||
--pcap=*) PCAP="${arg#*=}" ;;
|
||||
--debug) DEBUG=1 ;;
|
||||
--log=*) LOG="${arg#*=}" ;;
|
||||
--klog) KLOG="klog.log" ;;
|
||||
--klog=*) KLOG="${arg#*=}" ;;
|
||||
--no-rst) RST="" ;;
|
||||
--no-vcc) VCC="" ;;
|
||||
--protocol=*) PROTOCOL="$(norm_proto "${arg#*=}")" ;;
|
||||
--starts-with-atr=*)
|
||||
echo "WARNING: --starts-with-atr is obsolete (removed in v1.3.0); the decoder always runs mid-session" >&2 ;;
|
||||
--loop) LOOP=1 ;;
|
||||
--max-restarts=*) MAX_RESTARTS="${arg#*=}" ;;
|
||||
--restart-delay=*) RESTART_DELAY="${arg#*=}" ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: ${arg} (see --help)" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If the user named any channel pin, do NOT inject defaults for the channels
|
||||
# they didn't name -- a default RST/VCC pin (D0/D4) can clash with a user
|
||||
# pin (e.g. --data=D4), and the decoder should only use the named channels.
|
||||
if [ -n "${CLK_SET}${DATA_SET}${RST_SET}${VCC_SET}" ]; then
|
||||
[ -z "${CLK_SET}" ] && CLK=""
|
||||
[ -z "${DATA_SET}" ] && DATA=""
|
||||
[ -z "${RST_SET}" ] && RST=""
|
||||
[ -z "${VCC_SET}" ] && VCC=""
|
||||
fi
|
||||
|
||||
LINK="${HOME}/.local/share/libsigrokdecode/decoders/iso7816"
|
||||
if [ ! -e "${LINK}/pd.py" ]; then
|
||||
echo "Decoder not installed (${LINK} missing or broken)." >&2
|
||||
echo "Run ./setup.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Preflight: confirm the FX2 device is present and that every channel we
|
||||
# are about to assign actually exists on it. A wrong/missing channel name
|
||||
# would otherwise surface later as a confusing "device stopped streaming"
|
||||
# (rc=0) or a sigrok-cli abort; fail loudly here instead.
|
||||
SHOW_OUT="$(sigrok-cli -d fx2lafw --show 2>&1)" || SHOW_OUT=""
|
||||
if ! echo "${SHOW_OUT}" | grep -q 'channels:'; then
|
||||
echo "ERROR: FX2 device not found or firmware not loaded." >&2
|
||||
echo "Connect the analyzer and run ./setup.sh (installs sigrok-firmware-fx2lafw)." >&2
|
||||
exit 1
|
||||
fi
|
||||
DEV_CHANS="$(echo "${SHOW_OUT}" | grep -oP 'channels: \K.*' | tr -s ' ')"
|
||||
validate_chan() {
|
||||
# $1 = role (clk/data/rst/vcc), $2 = channel name
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: '$1' channel is empty. Use --$1=CH (e.g. --$1=D1)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! echo " ${DEV_CHANS} " | grep -qw "$2"; then
|
||||
echo "ERROR: channel '$2' ($1) is not valid on this device." >&2
|
||||
echo "Available channels: ${DEV_CHANS}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
validate_chan clk "${CLK}"
|
||||
validate_chan data "${DATA}"
|
||||
[ -n "${RST}" ] && validate_chan rst "${RST}"
|
||||
[ -n "${VCC}" ] && validate_chan vcc "${VCC}"
|
||||
|
||||
# A physical channel can carry only one signal. Assigning the same channel
|
||||
# to two decoder inputs (e.g. --clk=D0 with the default --rst=D0) makes the
|
||||
# decoder hang/burst after the ATR, because the shared pin drives both sample
|
||||
# reads and line-event detection at once. Reject that before launching.
|
||||
check_dup() {
|
||||
# $1 = role A, $2 = value A, $3 = role B, $4 = value B
|
||||
[ -z "$2" ] || [ -z "$4" ] && return 0
|
||||
if [ "$2" = "$4" ]; then
|
||||
echo "ERROR: '$1' and '$3' are both assigned to channel '$2'." >&2
|
||||
echo "A channel cannot carry two signals at once. Use --no-$3 or set" >&2
|
||||
echo "--$3 to a different pin (see the pinout in the README)." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
check_dup clk "${CLK}" rst "${RST}"
|
||||
check_dup clk "${CLK}" vcc "${VCC}"
|
||||
check_dup data "${DATA}" rst "${RST}"
|
||||
check_dup data "${DATA}" vcc "${VCC}"
|
||||
|
||||
echo "Channels: clk=${CLK} data=${DATA}${RST:+ rst=${RST}}${VCC:+ vcc=${VCC}}"
|
||||
|
||||
OPTS="clk=${CLK}:data=${DATA}:clock_option=${CLOCK}"
|
||||
OPTS+=":gsmtap_host=${HOST}:gsmtap_port=${PORT}"
|
||||
[ -n "${RST}" ] && OPTS+=":rst=${RST}:rst_detect=true"
|
||||
[ -n "${VCC}" ] && OPTS+=":vcc=${VCC}:vcc_detect=true"
|
||||
OPTS+=":protocol=${PROTOCOL}"
|
||||
|
||||
echo "ISO 7816 streamer — clk=${CLK} data=${DATA}${RST:+ rst=${RST}}${VCC:+ vcc=${VCC}} clock=${CLOCK}"
|
||||
echo "Protocol: ${PROTOCOL}"
|
||||
echo "Samplerate: ${SAMPLERATE} | GSMTAP -> ${HOST}:${PORT}${PCAP:+ | pcap: ${PCAP}}${LOG:+ | log: ${LOG}}"
|
||||
echo "Capture runs until you press Ctrl+C (stdin is detached, so keypresses"
|
||||
echo "do NOT stop it)."
|
||||
echo ""
|
||||
|
||||
if [ -n "${LOG}" ]; then
|
||||
exec > >(tee "${LOG}") 2>&1
|
||||
fi
|
||||
|
||||
KLOG_PID=""
|
||||
if [ -n "${KLOG}" ]; then
|
||||
if journalctl -kf > "${KLOG}" 2>/dev/null &
|
||||
then
|
||||
KLOG_PID=$!
|
||||
echo "Kernel journal -> ${KLOG} (pid ${KLOG_PID})"
|
||||
else
|
||||
echo "journalctl -kf unavailable (permissions?); kernel log disabled" >&2
|
||||
KLOG_PID=""
|
||||
fi
|
||||
fi
|
||||
cleanup() {
|
||||
[ -n "${KLOG_PID}" ] && kill "${KLOG_PID}" 2>/dev/null || true
|
||||
[ -n "${ANNOT_TMP}" ] && rm -f "${ANNOT_TMP}" 2>/dev/null || true
|
||||
[ -n "${FIFO_PID}" ] && kill "${FIFO_PID}" 2>/dev/null || true
|
||||
[ -n "${FIFO}" ] && rm -f "${FIFO}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
ANNOT_TMP="$(mktemp -t iso7816_annot.XXXXXX)" || ANNOT_TMP=""
|
||||
FIFO=""
|
||||
FIFO_PID=""
|
||||
|
||||
run_once() {
|
||||
# $1 = samplerate, $2 = pcap file for this session (may be empty)
|
||||
local rate="$1"
|
||||
local pcap_opt=""
|
||||
[ -n "$2" ] && pcap_opt=":pcap_file=$2"
|
||||
# Blocking, keypress-free stdin: sigrok-cli's "press any key to stop"
|
||||
# waits on stdin. /dev/null EOFs instantly and stops the capture (the
|
||||
# bug introduced when detaching stdin), and a real keypress would too.
|
||||
# A background `sleep` holds the pipe open without ever sending a byte,
|
||||
# so the capture runs until the genuine SIGINT (Ctrl+C). We track and
|
||||
# reap the sleep via a FIFO.
|
||||
FIFO="$(mktemp -u -t iso7816_fifo.XXXXXX)"
|
||||
mkfifo "${FIFO}"
|
||||
sleep 1000000 > "${FIFO}" &
|
||||
FIFO_PID=$!
|
||||
sigrok-cli -d fx2lafw \
|
||||
--config samplerate="${rate}" \
|
||||
--continuous \
|
||||
-C 'D0,D1,D2,D3,D4,D5,D6,D7' \
|
||||
${DEBUG:+-l 4} \
|
||||
-P "iso7816:${OPTS}${pcap_opt}" \
|
||||
-A iso7816 < "${FIFO}" | tee "${ANNOT_TMP}"
|
||||
local rc=${PIPESTATUS[0]}
|
||||
kill "${FIFO_PID}" 2>/dev/null || true
|
||||
rm -f "${FIFO}" 2>/dev/null || true
|
||||
return "${rc}"
|
||||
}
|
||||
|
||||
report_exit() {
|
||||
# $1 = session number, $2 = exit code
|
||||
case "$2" in
|
||||
0) reason="capture ended (device stopped streaming on its own)" ;;
|
||||
130) reason="SIGINT (Ctrl+C)" ;;
|
||||
137) reason="SIGKILL (OOM?)" ;;
|
||||
141) reason="SIGPIPE" ;;
|
||||
*) reason="error" ;;
|
||||
esac
|
||||
echo "=== session #$1 ended rc=$2: $reason ($(date +%T))"
|
||||
|
||||
# rc=0 means the device itself stopped streaming -- this is distinct
|
||||
# from an idle line, which with --continuous would keep running until
|
||||
# Ctrl+C (rc=130). An idle capture (started before the card is
|
||||
# powered) is completely normal and must not be treated as a failure.
|
||||
if [ "$2" -eq 0 ]; then
|
||||
local n=0
|
||||
[ -n "${ANNOT_TMP}" ] && [ -f "${ANNOT_TMP}" ] \
|
||||
&& n=$(wc -l < "${ANNOT_TMP}" 2>/dev/null || echo 0)
|
||||
if [ "$n" -eq 0 ]; then
|
||||
echo "NOTE: decoded 0 events. An idle line is normal (e.g. capture" >&2
|
||||
echo "started before the SIM was powered). The device stopping on" >&2
|
||||
echo "its own (rc=0) is a device-level issue, not an idle line:" >&2
|
||||
echo " - FX2 unplugged / lost USB connection mid-capture" >&2
|
||||
echo " - fx2lafw firmware hiccup" >&2
|
||||
echo " - USB bandwidth ceiling (try a lower --samplerate)" >&2
|
||||
else
|
||||
echo "Decoded ${n} events before the device stopped." >&2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
set +e
|
||||
if [ "${LOOP}" -eq 1 ]; then
|
||||
tries=0
|
||||
while true; do
|
||||
tries=$((tries + 1))
|
||||
run_once "${SAMPLERATE}" "${PCAP}"
|
||||
rc=$?
|
||||
report_exit "${tries}" "$rc"
|
||||
# A non-zero rc means a genuine stop: Ctrl+C (130) or an error.
|
||||
# Only rc=0 (device stopped streaming on its own) triggers a restart.
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
break
|
||||
fi
|
||||
if [ "${MAX_RESTARTS}" -gt 0 ] && [ "${tries}" -ge "${MAX_RESTARTS}" ]; then
|
||||
echo "Reached max restarts (${MAX_RESTARTS}); stopping." >&2
|
||||
break
|
||||
fi
|
||||
echo "Device stopped (rc=0); restarting in ${RESTART_DELAY}s ..." >&2
|
||||
sleep "${RESTART_DELAY}"
|
||||
done
|
||||
exit "$rc"
|
||||
else
|
||||
run_once "${SAMPLERATE}" "${PCAP}"
|
||||
rc=$?
|
||||
report_exit "single" "$rc"
|
||||
exit "$rc"
|
||||
fi
|
||||
@@ -0,0 +1,403 @@
|
||||
=== Baseline captured 2026-09-09T14:15:00+03:00 ===
|
||||
|
||||
=== test_8 ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('etu recovered (samples):', 1237)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 37
|
||||
capture ATRs : 3
|
||||
capture APDUs : 56
|
||||
byte-exact matches (LCS) : 28
|
||||
reader exchanges not in capture (pre-capture / dropped): 9
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 17
|
||||
- valid exchanges (reader retries/dupes): 11
|
||||
- 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)
|
||||
|
||||
=== test_7 ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('etu recovered (samples):', 1236)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 37
|
||||
capture ATRs : 8
|
||||
capture APDUs : 65
|
||||
byte-exact matches (LCS) : 37
|
||||
reader exchanges not in capture (pre-capture / dropped): 0
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 17
|
||||
- valid exchanges (reader retries/dupes): 11
|
||||
- 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)
|
||||
|
||||
=== xiaomi_phone_sample ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('etu recovered (samples):', 53)
|
||||
(11673977, '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
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 640
|
||||
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): 20
|
||||
- valid exchanges (reader retries/dupes): 620
|
||||
- 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)
|
||||
|
||||
=== xiaomi_mi_a1_coldboot ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e')
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 473
|
||||
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): 10
|
||||
- valid exchanges (reader retries/dupes): 463
|
||||
- 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)
|
||||
|
||||
=== 4gmodem_coldboot ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('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
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 327
|
||||
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): 9
|
||||
- valid exchanges (reader retries/dupes): 318
|
||||
- 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.5.0 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 407
|
||||
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): 24
|
||||
- valid exchanges (reader retries/dupes): 383
|
||||
- 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_sim2 ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f96801fc78031e073fe2117574a3382033339001e')
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 395
|
||||
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): 24
|
||||
- valid exchanges (reader retries/dupes): 371
|
||||
- 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_S21p_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
('RST deasserted (hunt/ATR already in progress)',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 399
|
||||
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): 24
|
||||
- valid exchanges (reader retries/dupes): 375
|
||||
- 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_S21p_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.5.0 ===',)
|
||||
('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
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 375
|
||||
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): 21
|
||||
- valid exchanges (reader retries/dupes): 354
|
||||
- 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)
|
||||
|
||||
=== signal diagnostics ===
|
||||
--- test_8 ---
|
||||
First activity: sample 1
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 71858170
|
||||
Transitions: 6
|
||||
Transitions: 168
|
||||
Transitions: 7423
|
||||
--- test_7 ---
|
||||
First activity: sample 2
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 71428947
|
||||
Transitions: 16
|
||||
Transitions: 444
|
||||
Transitions: 8998
|
||||
--- xiaomi_phone_sample ---
|
||||
First activity: sample 11497325
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 12833950
|
||||
Transitions: 0
|
||||
Transitions: 78488
|
||||
--- samsung_A55_coldboot_new_cable ---
|
||||
First activity: sample 46045778
|
||||
Frequency: 4.00 MHz
|
||||
Period: 4.0 samples
|
||||
Rising edges: 29683152
|
||||
Transitions: 1
|
||||
Transitions: 1
|
||||
Transitions: 46003
|
||||
--- samsung_S21p_coldboot_new_cable ---
|
||||
First activity: sample 44024869
|
||||
Frequency: 4.00 MHz
|
||||
Period: 4.0 samples
|
||||
Rising edges: 30172749
|
||||
Transitions: 1
|
||||
Transitions: 3
|
||||
Transitions: 47161
|
||||
=== unit tests ===
|
||||
test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_removed) ... ok
|
||||
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
||||
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... ok
|
||||
test_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pulse_start_edge_preserved) ... ok
|
||||
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
||||
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
||||
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
||||
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... ok
|
||||
test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
||||
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
||||
test_plausible_sw_accepts_valid_classes (__main__.TestDesyncHelpers.test_plausible_sw_accepts_valid_classes) ... ok
|
||||
test_plausible_sw_rejects_garbage (__main__.TestDesyncHelpers.test_plausible_sw_rejects_garbage) ... ok
|
||||
test_a55_u1_concat (__main__.TestEmbeddedExchanges.test_a55_u1_concat) ... ok
|
||||
test_a55_u4_mega_concat (__main__.TestEmbeddedExchanges.test_a55_u4_mega_concat) ... ok
|
||||
test_channel1_select_after_manage_channel_swallow (__main__.TestEmbeddedExchanges.test_channel1_select_after_manage_channel_swallow) ... ok
|
||||
test_clean_atr_fragment_select (__main__.TestEmbeddedExchanges.test_clean_atr_fragment_select) ... 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_read_record_ff_run (__main__.TestEmbeddedExchanges.test_clean_read_record_ff_run) ... 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
|
||||
test_samsung_record_burst_concat (__main__.TestEmbeddedExchanges.test_samsung_record_burst_concat) ... ok
|
||||
test_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
||||
test_build_packet_bad_fcs_flag (__main__.TestGsmtapFlags.test_build_packet_bad_fcs_flag) ... ok
|
||||
test_build_packet_payload_passthrough (__main__.TestGsmtapFlags.test_build_packet_payload_passthrough) ... ok
|
||||
test_build_packet_plain_res_zero (__main__.TestGsmtapFlags.test_build_packet_plain_res_zero) ... ok
|
||||
test_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
||||
test_accepts_full_hold_fall (__main__.TestNextStartFall.test_accepts_full_hold_fall) ... ok
|
||||
test_finds_fall_after_threshold (__main__.TestNextStartFall.test_finds_fall_after_threshold) ... ok
|
||||
test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall_exactly_at_threshold)
|
||||
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_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
||||
test_invalid_cla_proprietary_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_proprietary_unseen) ... ok
|
||||
test_invalid_ins_lsb_odd (__main__.TestValidateT0Apdu.test_invalid_ins_lsb_odd) ... ok
|
||||
test_invalid_ins_sw_collision (__main__.TestValidateT0Apdu.test_invalid_ins_sw_collision) ... ok
|
||||
test_invalid_ins_zero (__main__.TestValidateT0Apdu.test_invalid_ins_zero) ... ok
|
||||
test_invalid_oversized (__main__.TestValidateT0Apdu.test_invalid_oversized) ... ok
|
||||
test_invalid_sw1 (__main__.TestValidateT0Apdu.test_invalid_sw1) ... ok
|
||||
test_invalid_too_short (__main__.TestValidateT0Apdu.test_invalid_too_short) ... ok
|
||||
test_valid_case1_direct_sw (__main__.TestValidateT0Apdu.test_valid_case1_direct_sw) ... ok
|
||||
test_valid_case2_with_response (__main__.TestValidateT0Apdu.test_valid_case2_with_response) ... ok
|
||||
test_valid_case3_with_command_data (__main__.TestValidateT0Apdu.test_valid_case3_with_command_data) ... ok
|
||||
test_valid_cla_channel1_select_after_manage_channel (__main__.TestValidateT0Apdu.test_valid_cla_channel1_select_after_manage_channel) ... ok
|
||||
test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla_interindustry_classes) ... ok
|
||||
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 47 tests in 0.017s
|
||||
|
||||
OK
|
||||
@@ -0,0 +1,476 @@
|
||||
=== Baseline captured 2026-09-04T22:55:20+03:00 ===
|
||||
|
||||
=== test_8 ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('Invalid TS byte 0x90 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x90 - skipped
|
||||
('Invalid TS byte 0x00 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x00 - skipped
|
||||
('Invalid TS byte 0xf7 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xf7 - skipped
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
('Invalid TS byte 0x10 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x10 - skipped
|
||||
('Invalid TS byte 0x95 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x95 - skipped
|
||||
('Invalid TS byte 0x7a (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x7a - skipped
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
('Invalid TS byte 0x10 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x10 - skipped
|
||||
('warm reset: re-arming ATR hunt',)
|
||||
CHKSUM ERROR count: 28
|
||||
reader exchanges : 37
|
||||
capture ATRs : 2
|
||||
capture APDUs : 54
|
||||
byte-exact matches (LCS) : 28
|
||||
reader exchanges not in capture (pre-capture / dropped): 9
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 17
|
||||
- valid exchanges (reader retries/dupes): 9
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== test_8 mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('etu recovered (samples):', 1237)
|
||||
('warm reset: re-arming ATR hunt',)
|
||||
('Invalid TS byte 0x95 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x95 - skipped
|
||||
('Invalid TS byte 0x7a (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x7a - skipped
|
||||
(382251, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
|
||||
('Invalid TS byte 0xfc (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xfc - skipped
|
||||
(7362370, '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]
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
(14335815, '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]
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
('Invalid TS byte 0xfb (pre-activation?), resyncing',)
|
||||
CHKSUM ERROR count: 38
|
||||
reader exchanges : 37
|
||||
capture ATRs : 2
|
||||
capture APDUs : 54
|
||||
byte-exact matches (LCS) : 28
|
||||
reader exchanges not in capture (pre-capture / dropped): 9
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 17
|
||||
- valid exchanges (reader retries/dupes): 9
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== test_7 ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('warm reset: re-arming ATR hunt',)
|
||||
('Invalid TS byte 0x07 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x07 - skipped
|
||||
('Invalid TS byte 0x95 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x95 - skipped
|
||||
('Invalid TS byte 0x7a (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x7a - skipped
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
('Invalid TS byte 0x10 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x10 - skipped
|
||||
('Invalid TS byte 0x95 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x95 - skipped
|
||||
('Invalid TS byte 0x7a (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x7a - skipped
|
||||
(23123034, '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]
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
CHKSUM ERROR count: 166
|
||||
reader exchanges : 37
|
||||
capture ATRs : 4
|
||||
capture APDUs : 10
|
||||
byte-exact matches (LCS) : 10
|
||||
reader exchanges not in capture (pre-capture / dropped): 27
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 0
|
||||
- valid exchanges (reader retries/dupes): 0
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== test_7 mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('warm reset: re-arming ATR hunt',)
|
||||
('etu recovered (samples):', 1236)
|
||||
('warm reset: re-arming ATR hunt',)
|
||||
('ENDATR', b'3b9f95801fc78031e073fe21136364035b83079000f7')
|
||||
(45481390, '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]
|
||||
(45636295, '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]
|
||||
('Invalid TS byte 0x9f (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x9f - skipped
|
||||
('Invalid TS byte 0x95 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x95 - skipped
|
||||
('Invalid TS byte 0x80 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x80 - skipped
|
||||
('Invalid TS byte 0x1f (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x1f - skipped
|
||||
('Invalid TS byte 0xc7 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xc7 - skipped
|
||||
('Invalid TS byte 0x80 (pre-activation?), resyncing',)
|
||||
CHKSUM ERROR count: 162
|
||||
reader exchanges : 37
|
||||
capture ATRs : 4
|
||||
capture APDUs : 10
|
||||
byte-exact matches (LCS) : 10
|
||||
reader exchanges not in capture (pre-capture / dropped): 27
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 0
|
||||
- valid exchanges (reader retries/dupes): 0
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_phone ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('Invalid TS byte 0xf4 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xf4 - skipped
|
||||
(11685168, '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]
|
||||
('Invalid TS byte 0xff (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xff - skipped
|
||||
(11703683, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 0, 1, 0, 0, 0, 0, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 0, 1, 0, 0, 0, 0, 0]
|
||||
('Invalid TS byte 0x0b (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x0b - skipped
|
||||
('Invalid TS byte 0xe8 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xe8 - skipped
|
||||
('Invalid TS byte 0xbc (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xbc - skipped
|
||||
(11739309, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 0, 1, 1, 1, 0, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 0, 1, 1, 1, 0, 1, 0]
|
||||
('Invalid TS byte 0xb9 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xb9 - skipped
|
||||
('Invalid TS byte 0x5c (pre-activation?), resyncing',)
|
||||
CHKSUM ERROR count: 142
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 0
|
||||
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): 0
|
||||
- valid exchanges (reader retries/dupes): 0
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_phone mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('etu recovered (samples):', 53)
|
||||
(11673977, '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: 10140
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 620
|
||||
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): 18
|
||||
- valid exchanges (reader retries/dupes): 602
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_phone_sample ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
(3125821, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
|
||||
('Invalid TS byte 0xfc (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xfc - skipped
|
||||
('Invalid TS byte 0x03 (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x03 - skipped
|
||||
(21909783, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 1, 1, 1, 0, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 1, 1, 1, 0, 1, 0]
|
||||
('Invalid TS byte 0xbc (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0xbc - skipped
|
||||
(21952849, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 1, 1, 1, 0, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 1, 1, 1, 0, 0]
|
||||
('Invalid TS byte 0x7f (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x7f - skipped
|
||||
(21969301, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 0, 0, 0, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 0, 0, 0, 1, 0]
|
||||
('Invalid TS byte 0x8f (pre-activation?), resyncing',)
|
||||
iso7816-1: Invalid TS byte 0x8f - skipped
|
||||
('Invalid TS byte 0xfe (pre-activation?), resyncing',)
|
||||
CHKSUM ERROR count: 2062
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 2
|
||||
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): 0
|
||||
- valid exchanges (reader retries/dupes): 2
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_phone_sample mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('etu recovered (samples):', 72)
|
||||
(3272987, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 1, 1, 1, 1, 1, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 1, 1, 1, 1, 1, 1, 0]
|
||||
(3273839, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 1, 0, 1, 1, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 1, 0, 1, 1, 1, 0]
|
||||
('etu recovered (samples):', 71)
|
||||
(21919147, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
|
||||
(22026940, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
|
||||
(22831386, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 1, 0, 0, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 1, 0, 0, 1, 1]
|
||||
(22850665, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 1, 1, 0, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 1, 1, 0, 1, 0]
|
||||
(22853174, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 1, 1, 0, 1, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 1, 1, 0, 1, 1, 0]
|
||||
(22855060, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
|
||||
(22873632, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 1, 1, 1, 1, 1, 1, 0])
|
||||
CHKSUM ERROR count: 1514
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 49
|
||||
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): 0
|
||||
- valid exchanges (reader retries/dupes): 44
|
||||
- GARBAGE (mis-framed/desync): 5
|
||||
GARBAGE: 00a41f04026f38ff61
|
||||
GARBAGE: 00a40004026f07ff61
|
||||
GARBAGE: 00a40004026f78ff61
|
||||
GARBAGE: 00a4000402ff6fdfff
|
||||
GARBAGE: 00a40004026fe3ff61
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 6
|
||||
in GARBAGE: 5, in retries: 1, in CAT: 0, in matched: 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: UNCLEAN - 5 mis-framed capture APDU(s)
|
||||
|
||||
=== sunrise_phone_sample ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('ENDATR', b'3b9f96801fc68031e073fe211b644127910082900037')
|
||||
CHKSUM ERROR count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 1077
|
||||
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): 5
|
||||
- valid exchanges (reader retries/dupes): 1072
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== sunrise_phone_sample mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('etu recovered (samples):', 5723)
|
||||
(88911990, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 0, 0, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 0, 0, 1, 1]
|
||||
(92269756, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(93979892, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(94115907, '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]
|
||||
(95206798, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(95329967, '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]
|
||||
(95981570, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 1, 0, 1, 1, 1, 0])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 1, 0, 1, 1, 1, 0]
|
||||
(99228947, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(99366855, '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]
|
||||
CHKSUM ERROR count: 1834
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 1
|
||||
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): 0
|
||||
- valid exchanges (reader retries/dupes): 1
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== sim_turnon ATR-included ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('ENDATR', b'3b9f96801fc78031e073fe211163444d2183079000e2')
|
||||
CHKSUM ERROR count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 1390
|
||||
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): 5
|
||||
- valid exchanges (reader retries/dupes): 1385
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== sim_turnon mid-session ===
|
||||
('iso7816 decoder v1.1.3',)
|
||||
('etu recovered (samples):', 2855)
|
||||
(108117240, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 0, 0, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 0, 0, 1, 1]
|
||||
(109877662, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(113088360, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
|
||||
(113308472, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
(113339895, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 1, 0, 1, 1, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
|
||||
(113602225, '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]
|
||||
(113954522, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 0, 0, 0, 1, 0, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 0, 0, 0, 1, 0, 1, 1]
|
||||
(113983375, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 1, 1, 0, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 1, 1, 0, 0, 1, 1, 1, 1]
|
||||
(114163147, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 1, 0, 1, 1, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 1, 0, 1, 1, 0, 1, 1, 1, 1]
|
||||
CHKSUM ERROR count: 5044
|
||||
reader exchanges : 0
|
||||
capture ATRs : 0
|
||||
capture APDUs : 9
|
||||
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): 0
|
||||
- valid exchanges (reader retries/dupes): 7
|
||||
- GARBAGE (mis-framed/desync): 2
|
||||
GARBAGE: 00feedc2feedeefeedeafeec00feecffe797edeafeedeafeec00feecffe797edeafeecc0fefc80e7e7e7e7e7e7e767edfafeec00feecffe797edfafeecc2f9feece6edfafeec00feecffe797edfafeecc2f9feec41fbedfafeec08feecffe797edeafeecc2f9feec02feedfafeecc2f9feec83feedfefeecc2f9feecf2edf2feecc0feec3efeedfafeecc0feec4cfbedfefeecc2f9fefc4080808080808080808088808480808080feec41feedfefeecc0feecc0ecfeec02e7e6feedeefeedeafeecc0f9feece7edfafeedeafeedeafeec82f9feedfafeec08feec18e7e7fdedfefeec8af9feedfafeec0af9feedfafeec00feec18e7e7fdedfafeec02f9feedfafeec0af9
|
||||
GARBAGE: 000667f9e7e7e7e7e7e7e7e7e7e767ec100083e7e7e7e7e7e7e7e7e7e7e7e797ec1820e7e7e7e7e7e7e7e7e7e7e7e727ec1800eae7e7e7e7e7e7e7e7e7e7e7e7e7cfedf6feec08feedeafeec0af9feec03ffe727edf6feec00feec52feedeafeec0af9feecffe7e7e7fbedf6feec80feedeafeec02f9feecffe7e7e7fbedf6feec88e6edfefeec00feedfafeec80e4edfafeec88e4edfafeec00feedfafeec08feedfafeec80feedfefeec80feedfefeec08feedfefeec00feedfafeec80e4ece4ece4edfafeec02e7e6feece23dccffec9d95dcf2edf6feec80e4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4edfafeecc2f9feedc2feedfefeedf6feec80e4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4edf2feecc0feedc6feedfafeecc0feedc27dedfeedf6feec88e4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4ece4
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS (desynced by decoder): 5
|
||||
in GARBAGE: 2, in retries: 3, in CAT: 0, in matched: 0
|
||||
- payload mismatches (LCS-matched, bytes differ): 0
|
||||
- suspicious CLA: 0
|
||||
|
||||
RESULT: UNCLEAN - 2 mis-framed capture APDU(s)
|
||||
|
||||
=== signal diagnostics ===
|
||||
--- test_8 ---
|
||||
First activity: sample 1
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 71858170
|
||||
Transitions: 6
|
||||
Transitions: 168
|
||||
Transitions: 7423
|
||||
--- test_7 ---
|
||||
First activity: sample 2
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 71428947
|
||||
Transitions: 16
|
||||
Transitions: 444
|
||||
Transitions: 8998
|
||||
--- xiaomi_phone ---
|
||||
First activity: sample 11497325
|
||||
Frequency: 5.33 MHz
|
||||
Period: 3.0 samples
|
||||
Rising edges: 12833950
|
||||
Transitions: 0
|
||||
Transitions: 78488
|
||||
--- samsung_phone_sample ---
|
||||
First activity: sample 3096536
|
||||
Frequency: 4.00 MHz
|
||||
Period: 4.0 samples
|
||||
Rising edges: 14002767
|
||||
Transitions: 38
|
||||
Transitions: 171694
|
||||
Transitions: 117098
|
||||
--- sunrise_phone_sample ---
|
||||
@@ -0,0 +1,363 @@
|
||||
"""Unit tests for GSMTAP flag plumbing + decoder desync helpers.
|
||||
|
||||
pd.py imports the libsigrokdecode bindings, which are not always present
|
||||
in a bare Python env, so we stub a minimal `sigrokdecode` module and load
|
||||
the real pd.py / gsmtap_stream.py sources under the `iso7816` package
|
||||
name (mirroring how libsigrokdecode loads a decoder directory).
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _load_iso7816():
|
||||
# Minimal sigrokdecode stub.
|
||||
srd = types.ModuleType('sigrokdecode')
|
||||
srd.Decoder = type('Decoder', (object,), {})
|
||||
srd.SRD_CONF_SAMPLERATE = 'samplerate'
|
||||
srd.OUTPUT_PYTHON = 'python'
|
||||
srd.OUTPUT_ANN = 'ann'
|
||||
srd.OUTPUT_BINARY = 'binary'
|
||||
sys.modules.setdefault('sigrokdecode', srd)
|
||||
|
||||
pkg = types.ModuleType('iso7816')
|
||||
pkg.__path__ = [REPO]
|
||||
sys.modules.setdefault('iso7816', pkg)
|
||||
|
||||
def load(name, relpath):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'iso7816.' + name, os.path.join(REPO, relpath))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules['iso7816.' + name] = mod
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
return load('gsmtap_stream', 'gsmtap_stream.py'), \
|
||||
load('pd', 'pd.py')
|
||||
|
||||
|
||||
class TestGsmtapFlags(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def test_build_packet_plain_res_zero(self):
|
||||
p = self.gs.build_packet(0x00, b'\x80\xf2')
|
||||
self.assertEqual(p[12], 0x00)
|
||||
self.assertEqual(p[15], 0x00)
|
||||
|
||||
def test_build_packet_bad_fcs_flag(self):
|
||||
p = self.gs.build_packet(0x00, b'\x80\xf2',
|
||||
flags=self.gs.GSMTAP_FLAG_BAD_FCS)
|
||||
self.assertEqual(p[12], 0x00) # sub_type preserved
|
||||
self.assertEqual(p[15], 0x01) # res = BAD_FCS
|
||||
|
||||
def test_build_packet_payload_passthrough(self):
|
||||
data = b'\x00\xa4\x00\x04\x02'
|
||||
p = self.gs.build_packet(0x00, data)
|
||||
self.assertEqual(p[16:], data)
|
||||
|
||||
def test_flag_constant_value(self):
|
||||
self.assertEqual(self.gs.GSMTAP_FLAG_BAD_FCS, 0x01)
|
||||
|
||||
|
||||
class TestDesyncHelpers(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def test_plausible_sw_accepts_valid_classes(self):
|
||||
for sw1 in list(range(0x60, 0x70)) + list(range(0x90, 0xA0)):
|
||||
self.assertTrue(self.pd.plausible_sw(sw1), hex(sw1))
|
||||
|
||||
def test_plausible_sw_rejects_garbage(self):
|
||||
for sw1 in (0x00, 0x01, 0x3B, 0x3F, 0xA0, 0xFF, 0x88, 0x5F):
|
||||
self.assertFalse(self.pd.plausible_sw(sw1), hex(sw1))
|
||||
|
||||
def test_max_tpdu_len(self):
|
||||
self.assertEqual(self.pd.MAX_TPDU_LEN, 271)
|
||||
|
||||
def test_is_desynced_clean_packets(self):
|
||||
# 7-byte: header(5) + proc(1) + SW2(1) ending in a 9x SW1
|
||||
self.assertFalse(self.pd.is_desynced(b'\x00\xa4\x00\x04\x02\x60\x90'))
|
||||
# ACK-style GET RESPONSE (header + proc + payload + SW 9000)
|
||||
self.assertFalse(self.pd.is_desynced(
|
||||
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
||||
|
||||
def test_is_desynced_short_fragment(self):
|
||||
# 5-byte header-only (INVALID-procedure residue) -- no SW read
|
||||
self.assertTrue(self.pd.is_desynced(b'\xa4\x3f\x00\x61\x25'))
|
||||
|
||||
def test_is_desynced_bad_sw(self):
|
||||
self.assertTrue(self.pd.is_desynced(
|
||||
b'\x00\xa4\x00\x04\x02\x3f\x00\x00\xff'))
|
||||
|
||||
def test_is_desynced_oversized(self):
|
||||
big = bytes([0x00, 0xb2, 0x00, 0x00, 0xff]) + b'\x00' * 300
|
||||
self.assertTrue(self.pd.is_desynced(big))
|
||||
|
||||
def test_is_desynced_all_zero_is_not_desync(self):
|
||||
# all-zero is idle-low (suppressed), not desync
|
||||
self.assertFalse(self.pd.is_desynced(b'\x00' * 7))
|
||||
|
||||
|
||||
class TestValidateT0Apdu(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def _valid(self, packet):
|
||||
valid, reason = self.pd.validate_t0_apdu(packet)
|
||||
self.assertTrue(valid, 'expected valid, got: ' + str(reason))
|
||||
|
||||
def _invalid(self, packet, expected_reason_substring=None):
|
||||
valid, reason = self.pd.validate_t0_apdu(packet)
|
||||
self.assertFalse(valid, 'expected invalid')
|
||||
if expected_reason_substring:
|
||||
self.assertIn(expected_reason_substring, reason)
|
||||
|
||||
def test_valid_case1_direct_sw(self):
|
||||
# P3=0, 7 bytes, direct SW
|
||||
self._valid(b'\x00\xa4\x00\x04\x00\x90\x00')
|
||||
|
||||
def test_valid_direct_sw_with_p3_nonzero(self):
|
||||
# Card can reject a command and send SW immediately, even with P3>0
|
||||
self._valid(b'\x00\xa4\x00\x04\x02\x90\x00')
|
||||
|
||||
def test_valid_case2_with_response(self):
|
||||
# header(5) + 2 response bytes + SW(2)
|
||||
self._valid(b'\x00\xb0\x00\x00\x02\x12\x34\x90\x00')
|
||||
|
||||
def test_valid_case3_with_command_data(self):
|
||||
# header(5) + 4 command bytes + SW(2)
|
||||
self._valid(b'\x00\xd6\x00\x00\x04\x11\x22\x33\x44\x90\x00')
|
||||
|
||||
def test_valid_cla_interindustry_classes(self):
|
||||
# Interindustry CLA digits 0x/1x/4x/5x are structurally valid
|
||||
# (ISO 7816-4): includes logical channels 0x01-0x03, secure
|
||||
# messaging 0x0C, chaining 0x10, and the SM/structural 0x4x/0x5x.
|
||||
for cla in (0x01, 0x02, 0x03, 0x0C, 0x10, 0x14, 0x44, 0x50, 0x54):
|
||||
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||
self._valid(pkt), 'CLA=0x%02x' % cla
|
||||
|
||||
def test_valid_cla_channel1_select_after_manage_channel(self):
|
||||
# The command that follows MANAGE CHANNEL (00 70 00 00 01) opening
|
||||
# logical channel 1 legitimately uses CLA 0x01. This exact type
|
||||
# (SELECT by AID) is what the A55/S21p traces carry.
|
||||
self._valid(b'\x01\xa4\x00\x04\x0c'
|
||||
b'\xa0\x00\x00\x00\x63\x50\x4b\x43\x53\x2d\x31\x35'
|
||||
b'\x6a\x82')
|
||||
|
||||
def test_invalid_cla_proprietary_unseen(self):
|
||||
# 0xC0 is proprietary but not a class these cards emit; stays invalid
|
||||
self._invalid(b'\xc0\xa4\x00\x04\x00\x90\x00', 'invalid CLA')
|
||||
|
||||
def test_invalid_too_short(self):
|
||||
self._invalid(b'\x00\xa4\x00\x04\x00\x90', 'too short')
|
||||
|
||||
def test_invalid_cla(self):
|
||||
self._invalid(b'\xff\xa4\x00\x04\x00\x90\x00', 'invalid CLA')
|
||||
|
||||
def test_invalid_ins_zero(self):
|
||||
self._invalid(b'\x00\x00\x00\x04\x00\x90\x00', 'invalid INS')
|
||||
|
||||
def test_invalid_ins_lsb_odd(self):
|
||||
self._invalid(b'\x00\xa5\x00\x04\x00\x90\x00', 'invalid INS')
|
||||
|
||||
def test_invalid_ins_sw_collision(self):
|
||||
self._invalid(b'\x00\x64\x00\x04\x00\x90\x00', 'invalid INS')
|
||||
|
||||
def test_invalid_sw1(self):
|
||||
self._invalid(b'\x00\xa4\x00\x04\x00\xff\x00', 'invalid SW1')
|
||||
|
||||
def test_invalid_oversized(self):
|
||||
big = bytes([0x00, 0xb2, 0x00, 0x00, 0xff]) + b'\x00' * 300
|
||||
self._invalid(big, 'exceeds T=0 max length')
|
||||
|
||||
|
||||
class TestNextStartFall(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_, pd = _load_iso7816()
|
||||
# _next_start_fall is an instance method but does not use self.
|
||||
self.inst = object.__new__(pd.Decoder)
|
||||
|
||||
def test_finds_fall_after_threshold(self):
|
||||
edges = [(100, 0), (200, 1), (300, 0)]
|
||||
self.assertEqual(self.inst._next_start_fall(edges, 250, 100), 300)
|
||||
|
||||
def test_finds_fall_exactly_at_threshold(self):
|
||||
"""Regression test for Samsung 16 CLK/ETU concatenation.
|
||||
|
||||
When the next byte's start-bit fall lands exactly on the
|
||||
``10 * bs`` boundary, the previous strict ``>`` test missed it.
|
||||
"""
|
||||
edges = [(100, 0), (200, 1), (300, 0)]
|
||||
self.assertEqual(self.inst._next_start_fall(edges, 300, 100), 300)
|
||||
|
||||
def test_returns_none_when_no_fall(self):
|
||||
edges = [(100, 0), (200, 1)]
|
||||
self.assertIsNone(self.inst._next_start_fall(edges, 300, 100))
|
||||
|
||||
def test_rejects_phantom_sub_etu_fall(self):
|
||||
# A sub-ETU low bounce in the stop/guard time (fall at 290, quick
|
||||
# rise at 310) must NOT be accepted as the next start; the scan
|
||||
# skips it and only accepts the genuine start fall at 400.
|
||||
edges = [(100, 0), (290, 0), (310, 1), (400, 0)]
|
||||
self.assertEqual(self.inst._next_start_fall(edges, 250, 100), 400)
|
||||
|
||||
def test_accepts_full_hold_fall(self):
|
||||
# Genuine start fall: held LOW for a full ETU before the rise.
|
||||
edges = [(100, 0), (300, 0), (400, 1)]
|
||||
self.assertEqual(self.inst._next_start_fall(edges, 250, 100), 300)
|
||||
|
||||
|
||||
class TestDeglitchEdges(unittest.TestCase):
|
||||
def setUp(self):
|
||||
_, pd = _load_iso7816()
|
||||
self.inst = object.__new__(pd.Decoder)
|
||||
|
||||
def test_no_edges(self):
|
||||
self.assertEqual(self.inst._deglitch_edges([], 72), [])
|
||||
|
||||
def test_glitch_low_pulse_removed(self):
|
||||
# Real start fall at 0, real rise at 72, 1-sample LOW glitch
|
||||
# at 73-74, real fall at 144 (next byte start). bs=72, threshold=14.
|
||||
edges = [(0, 0), (72, 1), (73, 0), (74, 1), (144, 0)]
|
||||
# After removing the 1-sample LOW pulse (73,0)-(74,1),
|
||||
# the line stays HIGH until the next real edge.
|
||||
result = self.inst._deglitch_edges(edges, 72)
|
||||
self.assertEqual(result, [(0, 0), (72, 1), (144, 0)])
|
||||
|
||||
def test_real_pulses_preserved(self):
|
||||
# 72-sample real LOW pulse; should not be touched.
|
||||
edges = [(0, 0), (72, 1), (144, 0), (216, 1)]
|
||||
result = self.inst._deglitch_edges(edges, 72)
|
||||
self.assertEqual(result, edges)
|
||||
|
||||
def test_short_pulse_start_edge_preserved(self):
|
||||
# Pulse starting at the first edge: start-bit fall is preserved,
|
||||
# only the edge ending the short pulse is removed.
|
||||
edges = [(0, 0), (1, 1), (100, 0)]
|
||||
result = self.inst._deglitch_edges(edges, 72)
|
||||
self.assertEqual(result, [(0, 0), (100, 0)])
|
||||
|
||||
|
||||
class TestEmbeddedExchanges(unittest.TestCase):
|
||||
"""CONCAT detection: structurally valid packets that embed complete T=0
|
||||
exchanges (a byte-drop mis-frame swallowed several real exchanges into
|
||||
one APDU). Vectors are exact bytes from the 10-trace suite."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def _hits(self, hexstr):
|
||||
return self.pd.embedded_exchanges(bytes.fromhex(hexstr))
|
||||
|
||||
def test_clean_get_response_fcp(self):
|
||||
# FCP response with 0x62/0x6f BER-TLV tags + p1=f0 PIN template;
|
||||
# classic false-positive candidate -- must NOT hit.
|
||||
h = ('00c000001ec0621c8202412183026f43a503920100'
|
||||
'8a01058b032f0604800200028800913e')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(self._hits(h), [])
|
||||
|
||||
def test_clean_select_611e(self):
|
||||
# Plain SELECT -> 611E (test_8/7 session) must not hit.
|
||||
self.assertEqual(self._hits('00a4080404a47fff611e'), [])
|
||||
|
||||
def test_clean_select_6a82(self):
|
||||
self.assertEqual(self._hits('00a4080404a47fff6a82'), [])
|
||||
|
||||
def test_clean_read_record_ff_run(self):
|
||||
# READ RECORD whose response is an all-FF fill run + 9000. The FF
|
||||
# data must not be mistaken for embedded headers.
|
||||
h = '00b2020440' + 'ff' * 64 + '9000'
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(self._hits(h), [])
|
||||
|
||||
def test_clean_channel1_select_after_manage_channel(self):
|
||||
# A properly decoded post-MANAGE-CHANNEL exchange (CLA 0x01,
|
||||
# SELECT by AID, 6A 82) is a CLEAN single exchange: it must
|
||||
# validate and must NOT be flagged as CONCAT.
|
||||
h = ('01a400040ca000000063504b43532d31356a82')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(self._hits(h), [])
|
||||
self.assertFalse(self.pd.is_desynced(bytes.fromhex(h)))
|
||||
|
||||
def test_clean_atr_fragment_select(self):
|
||||
# ATR-recovery fragment (header + 3F 00 + 6125) must not hit.
|
||||
self.assertEqual(self._hits('00a40004023f006125'), [])
|
||||
|
||||
def test_a55_u1_concat(self):
|
||||
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
|
||||
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
|
||||
'7183040000eec18a01058b032f0601c6069001008301019000'
|
||||
'01a4000402a42f00612101c0000021621f')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(self._hits(h), [(8, 51), (53, 61)])
|
||||
|
||||
def test_a55_u4_mega_concat(self):
|
||||
# A55[281] (n=167): the CAT ENVELOPE mega-concat (7 embedded).
|
||||
h = ('04040ca4a000000063504b43532d31356a8280f2000c009000007080'
|
||||
'0100900000a4000c02a43f009000801400000c148103012500020282'
|
||||
'81830100910f801200000f12d00d8103010500820281829902031290'
|
||||
'0000a4000c02a43f009000801400000c148103010500020282818301'
|
||||
'00910f801200000f12d00d8103010300820281828402011e90008014'
|
||||
'000010148103010300020282818301000402011e90000070009000')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
hits = self._hits(h)
|
||||
self.assertEqual(len(hits), 7)
|
||||
self.assertEqual(hits[0], (32, 40))
|
||||
|
||||
def test_samsung_record_burst_concat(self):
|
||||
# samsung_phone[826] (n=247): mis-framed READ RECORD chain.
|
||||
h = ('0408b252f0990000fffe01900000b2020408b252f0430000fffe02900'
|
||||
'000b2030408b252f0230000fffe03900000b2040408b252f0450000f'
|
||||
'ffe02900000b2050408b252f0490000fffe04900000b2060408b252f'
|
||||
'0690000fffe05900000b2070408b252f0790000fffe06900000b2080'
|
||||
'408b252f0890000fffe07900000b2090408b2ffffffffffffffff900'
|
||||
'000b20a0408b2ffffffffffffffff900000a4000402a46f40611b00c'
|
||||
'000001bc0621982054221001c0283026f408a01058b036f060980020'
|
||||
'0388800900000b201041cb2fffffffffbfbfbfbfbfbfbfbfbfbfbfbf'
|
||||
'bfbfbfbfbfbfbfbfb830300b202041cb2ffff9000')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(len(self._hits(h)), 15)
|
||||
|
||||
def test_xiaomi_select_getresp_swallow(self):
|
||||
# xiaomi[613] (n=118): stacked SELECT/GET RESPONSE exchanges.
|
||||
h = ('04a47fff6f116a8200a4080404a47fff6fe3611e00c000001ec0621c'
|
||||
'8202412183026fe3a503c001808a01058b036f0609800200128801f0'
|
||||
'900000a4080404a47fff6f136a8200a4080404a47fff6fe3611e00c0'
|
||||
'00001ec0621c8202412183026fe3a503c001808a01058b036f060980'
|
||||
'020012889000')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(len(self._hits(h)), 4)
|
||||
|
||||
def test_channel1_select_after_manage_channel_swallow(self):
|
||||
# Channels: MC opens channel 1 (00 70 00 00 01 -> 01 90 00: the 01
|
||||
# response byte is the opened channel number), then a channel-1
|
||||
# SELECT-by-AID (01 A4 00 04 0C AID -> 6A 82) that a mis-frame
|
||||
# swallowed into the MC packet. The embedded exchange starts on
|
||||
# CLA 0x01 -- a real channel-1 cluster byte, not a phantom: the
|
||||
# detector must find it.
|
||||
h = ('007000000101900001a400040ca000000063504b43532d31356a829000')
|
||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||
self.assertEqual(self._hits(h), [(8, 25)])
|
||||
|
||||
def test_concat_packets_are_not_is_desynced(self):
|
||||
# CONCAT detection is orthogonal: these packets PASS structural
|
||||
# validation (they only look wrong when interior bytes are scanned).
|
||||
a55u1 = ('000402a43f00612501c0000025c062238202782183023f00a5098'
|
||||
'0017183040000eec18a01058b032f0601c6069001008301019000'
|
||||
'01a4000402a42f00612101c0000021621f')
|
||||
self.assertFalse(self.pd.is_desynced(bytes.fromhex(a55u1)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
# Capture baseline measurements for all traces in mid-session mode.
|
||||
# Usage: ./tools/capture_baseline.sh > tests/baseline.txt
|
||||
# Compare with: diff tests/baseline.txt <(./tools/capture_baseline.sh)
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Clear stale bytecode cache — prevents false results from old code.
|
||||
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
run_test() {
|
||||
local label="$1"
|
||||
shift
|
||||
echo "=== $label ==="
|
||||
# Run decoder, capture stderr too
|
||||
local output
|
||||
output=$("$@" 2>&1) || true
|
||||
# Extract key metrics
|
||||
echo "$output" | grep -E "iso7816 decoder v|etu recovered|ENDATR|CHKSUM ERROR|INVALID Procedure|desynced APDU|CONCAT APDU|warm reset|Invalid TS|No valid TS|RST low at start|RST deasserted|expecting cold-boot" | head -20
|
||||
# CHKSUM errors are a key degradation signal.
|
||||
chksum_count=$(echo "$output" | grep -c "CHKSUM ERROR" || true)
|
||||
echo "CHKSUM ERROR count: $chksum_count"
|
||||
# CONCAT: packets with embedded exchanges (mis-frame swallowed several
|
||||
# real exchanges into one "valid" APDU). Must stay 0 for clean traces.
|
||||
# grep the log form (with ':'), not the annotation text, so each packet
|
||||
# is counted once.
|
||||
concat_count=$(echo "$output" | grep -c "CONCAT APDU:" || true)
|
||||
echo "CONCAT APDU count: $concat_count"
|
||||
# Check for vs_reader results if pcap exists
|
||||
if [ -f /tmp/out.pcap ]; then
|
||||
local reader_log=""
|
||||
case "$label" in
|
||||
test_8*) reader_log="examples/test_8_reader.log" ;;
|
||||
test_7*) reader_log="examples/test_7_reader.log" ;;
|
||||
esac
|
||||
if [ -n "$reader_log" ] && [ -f "$reader_log" ]; then
|
||||
python3 tools/vs_reader.py "$reader_log" /tmp/out.pcap 2>&1 || true
|
||||
else
|
||||
# No ground-truth reader log; run pcap-only analysis to surface
|
||||
# BAD_FCS / suspicious CLA / payload issues.
|
||||
python3 tools/vs_reader.py --pcap-only /tmp/out.pcap 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Signal diagnostic: extract key metrics from .sr trace
|
||||
signal_diag() {
|
||||
local sr_file="$1"
|
||||
local clk="$2"
|
||||
local data="$3"
|
||||
local rst_opt=""
|
||||
local vcc_opt=""
|
||||
[ -n "$4" ] && rst_opt="--rst $4"
|
||||
[ -n "$5" ] && vcc_opt="--vcc $5"
|
||||
python3 tools/signal_diagnostic.py "$sr_file" --clk "$clk" --data "$data" $rst_opt $vcc_opt 2>&1 \
|
||||
| grep -E 'Frequency|Period|Rising|Transitions|First activity|stable|noisy' | head -10
|
||||
}
|
||||
|
||||
echo "=== Baseline captured $(date -Iseconds) ==="
|
||||
echo ""
|
||||
|
||||
# test_8
|
||||
run_test "test_8" sigrok-cli -i examples/test_8_raw16.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
|
||||
|
||||
# test_7
|
||||
run_test "test_7" sigrok-cli -i examples/test_7_raw16.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
|
||||
|
||||
# xiaomi_phone_sample
|
||||
run_test "xiaomi_phone_sample" timeout 30 sigrok-cli -i examples/xiaomi_phone_sample.sr -P iso7816:clk=CLK:data=DATA:rst=RST:clock_option=native:protocol=T=0:gsmtap_enable=false:pcap_file=/tmp/out.pcap -A iso7816
|
||||
|
||||
# xiaomi_mi_a1_coldboot_sample — capture starts BEFORE power-up (VCC low at t=0).
|
||||
# Regression fixture: must arm the ATR hunt when RST is still low at start
|
||||
# and start decoding the ATR once the card wakes up.
|
||||
run_test "xiaomi_mi_a1_coldboot" timeout 300 sigrok-cli -i examples/xiaomi_mi_a1_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
|
||||
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
run_test "samsung_A55_coldboot_new_cable" timeout 300 sigrok-cli -i examples/samsung_A55_coldboot_new_cable.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
|
||||
|
||||
# samsung_A55_coldboot_new_cable_sim2 — A55 SIM slot 2, same cable; trace
|
||||
# cropped to the power-on window (~21 s of pre-power idle removed).
|
||||
run_test "samsung_A55_coldboot_new_cable_sim2" timeout 300 sigrok-cli -i examples/samsung_A55_coldboot_new_cable_sim2.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
|
||||
|
||||
# samsung_S21p_coldboot_new_cable — Samsung S21+ with the new short cable.
|
||||
run_test "samsung_S21p_coldboot_new_cable" timeout 300 sigrok-cli -i examples/samsung_S21p_coldboot_new_cable.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
|
||||
|
||||
# samsung_S21p_coldboot_new_cable_sim2 — S21+ SIM slot 2, same cable; trace
|
||||
# cropped to the power-on window (~17 s of pre-power idle removed).
|
||||
run_test "samsung_S21p_coldboot_new_cable_sim2" timeout 300 sigrok-cli -i examples/samsung_S21p_coldboot_new_cable_sim2.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
|
||||
|
||||
# signal diagnostics
|
||||
echo "=== signal diagnostics ==="
|
||||
echo "--- test_8 ---"
|
||||
signal_diag examples/test_8_raw16.sr CLK DATA RST VCC
|
||||
echo "--- test_7 ---"
|
||||
signal_diag examples/test_7_raw16.sr CLK DATA RST VCC
|
||||
echo "--- xiaomi_phone_sample ---"
|
||||
signal_diag examples/xiaomi_phone_sample.sr CLK DATA RST
|
||||
echo "--- samsung_A55_coldboot_new_cable ---"
|
||||
signal_diag examples/samsung_A55_coldboot_new_cable.sr CLK DATA RST VCC
|
||||
echo "--- samsung_S21p_coldboot_new_cable ---"
|
||||
signal_diag examples/samsung_S21p_coldboot_new_cable.sr CLK DATA RST VCC
|
||||
|
||||
# unit tests
|
||||
echo "=== unit tests ==="
|
||||
python3 tests/test_gsmtap.py -v 2>&1
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
# Compare current decoder output against saved baseline.
|
||||
# Usage: ./tools/compare_baseline.sh
|
||||
# Prerequisites: tests/baseline.txt exists (run capture_baseline.sh first)
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
BASELINE="tests/baseline.txt"
|
||||
|
||||
if [ ! -f "$BASELINE" ]; then
|
||||
echo "ERROR: No baseline found at $BASELINE"
|
||||
echo "Run ./tools/capture_baseline.sh > tests/baseline.txt first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Capture current output
|
||||
CURRENT=$(mktemp)
|
||||
trap "rm -f $CURRENT" EXIT
|
||||
./tools/capture_baseline.sh > "$CURRENT" 2>&1
|
||||
|
||||
# Compare
|
||||
echo "=== Comparing against baseline ==="
|
||||
echo ""
|
||||
|
||||
# Strip timestamps and test execution times before comparing
|
||||
BASELINE_CLEAN=$(mktemp)
|
||||
CURRENT_CLEAN=$(mktemp)
|
||||
trap "rm -f $BASELINE_CLEAN $CURRENT_CLEAN" EXIT
|
||||
|
||||
grep -v "^=== Baseline captured" "$BASELINE" | grep -v "^Ran [0-9]* tests in" > "$BASELINE_CLEAN"
|
||||
grep -v "^=== Baseline captured" "$CURRENT" | grep -v "^Ran [0-9]* tests in" > "$CURRENT_CLEAN"
|
||||
|
||||
if diff -u "$BASELINE_CLEAN" "$CURRENT_CLEAN"; then
|
||||
echo "IDENTICAL - no changes detected"
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
echo "DIFFERENCES FOUND - review above"
|
||||
echo ""
|
||||
echo "Key metrics to check:"
|
||||
echo " - GARBAGE count must be 0 for test_8"
|
||||
echo " - RESULT must be OK for test_8"
|
||||
echo " - ETU values must match for all traces"
|
||||
echo " - No new INVALID Procedure Byte errors"
|
||||
echo " - CHKSUM ERROR count must not increase"
|
||||
echo " - BAD_FCS / payload mismatches / suspicious CLA must not increase"
|
||||
echo " - CONCAT count must be 0 for test_8 / test_7 / 4gmodem"
|
||||
echo " - Capture APDU count must be >0 when reader log is present"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lower-level signal quality diagnostics for .sr trace files.
|
||||
|
||||
Reads a sigrok .sr file and reports per-signal measurements:
|
||||
CLK — frequency, duration, frequency changes
|
||||
RST — transitions, stability (noisy = ignore)
|
||||
VCC — transitions, stability (noisy = ignore)
|
||||
DATA — oscillations, correlation with CLK
|
||||
|
||||
Usage:
|
||||
python3 tools/signal_diagnostic.py trace.sr [--clk CLK] [--data DATA] [--rst RST] [--vcc VCC]
|
||||
|
||||
Channel names must match the probe names in the .sr metadata
|
||||
(e.g. CLK, DATA, RST, VCC or D0, D1, etc.).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import statistics
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# .sr file reading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def parse_sr_metadata(path):
|
||||
"""Return dict with 'samplerate', 'probes' (probe_num -> name), 'unitsize'."""
|
||||
with zipfile.ZipFile(path, 'r') as zf:
|
||||
meta = zf.read('metadata').decode()
|
||||
info = {'probes': {}}
|
||||
for line in meta.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('samplerate'):
|
||||
val = line.split('=', 1)[1].strip()
|
||||
if val.endswith(' MHz'):
|
||||
info['samplerate'] = float(val[:-4]) * 1e6
|
||||
elif val.endswith(' kHz'):
|
||||
info['samplerate'] = float(val[:-4]) * 1e3
|
||||
elif val.endswith(' Hz'):
|
||||
info['samplerate'] = float(val[:-3])
|
||||
else:
|
||||
info['samplerate'] = float(val)
|
||||
elif line.startswith('unitsize'):
|
||||
info['unitsize'] = int(line.split('=', 1)[1].strip())
|
||||
elif line.startswith('total probes'):
|
||||
info['total_probes'] = int(line.split('=', 1)[1].strip())
|
||||
elif line.startswith('probe'):
|
||||
parts = line.split('=', 1)
|
||||
num = int(parts[0][5:])
|
||||
info['probes'][num] = parts[1].strip()
|
||||
return info
|
||||
|
||||
|
||||
def read_sr_all(path):
|
||||
"""Read all logic samples into a numpy uint8 array."""
|
||||
with zipfile.ZipFile(path, 'r') as zf:
|
||||
chunk_names = [n for n in zf.namelist() if n.startswith('logic-1-')]
|
||||
chunk_names.sort(key=lambda n: int(n.split('-')[-1]))
|
||||
chunks = [zf.read(c) for c in chunk_names]
|
||||
return np.frombuffer(b''.join(chunks), dtype=np.uint8)
|
||||
|
||||
|
||||
def get_channel_bit(metadata, channel_name):
|
||||
"""Return the bit position (0-indexed) for a channel name, or None."""
|
||||
for probe_num, name in metadata['probes'].items():
|
||||
if name == channel_name:
|
||||
return probe_num - 1 # probe 1 = bit 0, probe 2 = bit 1, ...
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal measurement helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def extract_bit(arr, bit):
|
||||
"""Extract a single bit from a numpy uint8 array -> bool array."""
|
||||
return (arr >> bit) & 1
|
||||
|
||||
|
||||
def measure_clk_fast(clk_arr, sr):
|
||||
"""Measure CLK frequency from rising edges using numpy."""
|
||||
if len(clk_arr) == 0:
|
||||
return {'ok': False, 'reason': 'no data'}
|
||||
|
||||
# Find rising edges: 0->1 transitions
|
||||
diffs = np.diff(clk_arr.astype(np.int8))
|
||||
rising = np.where(diffs == 1)[0] + 1 # +1 because diff shifts by 1
|
||||
|
||||
if len(rising) < 2:
|
||||
return {
|
||||
'ok': False,
|
||||
'reason': f'only {len(rising)} rising edges (need >= 2)',
|
||||
'transitions': len(rising),
|
||||
}
|
||||
|
||||
spacings = np.diff(rising)
|
||||
median_spc = float(np.median(spacings))
|
||||
freq = sr / median_spc if median_spc > 0 else 0
|
||||
|
||||
duration_s = (rising[-1] - rising[0]) / sr
|
||||
|
||||
# Frequency change: compare first and last quarter
|
||||
freq_change = None
|
||||
if len(spacings) >= 20:
|
||||
n = len(spacings)
|
||||
q1 = float(np.median(spacings[:n // 4]))
|
||||
q4 = float(np.median(spacings[3 * n // 4:]))
|
||||
if q1 > 0 and q4 > 0:
|
||||
f1 = sr / q1
|
||||
f4 = sr / q4
|
||||
change_pct = abs(f1 - f4) / max(f1, f4) * 100
|
||||
if change_pct > 10:
|
||||
freq_change = f'{change_pct:.0f}% change ({f1/1e6:.2f} -> {f4/1e6:.2f} MHz)'
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
'rising_edges': len(rising),
|
||||
'frequency_mhz': freq / 1e6,
|
||||
'median_period_samples': median_spc,
|
||||
'duration_ms': duration_s * 1000,
|
||||
'freq_change': freq_change,
|
||||
}
|
||||
|
||||
|
||||
def count_transitions(arr):
|
||||
"""Count transitions in a numpy bool array."""
|
||||
if len(arr) == 0:
|
||||
return 0
|
||||
return int(np.sum(np.diff(arr.astype(np.int8)) != 0))
|
||||
|
||||
|
||||
def measure_data_fast(data_arr, clk_arr, sr):
|
||||
"""Measure DATA signal using numpy."""
|
||||
if len(data_arr) == 0:
|
||||
return {'ok': False, 'reason': 'no data'}
|
||||
|
||||
transitions = count_transitions(data_arr)
|
||||
|
||||
clk_data_corr = None
|
||||
if len(clk_arr) == len(data_arr) and len(clk_arr) > 1:
|
||||
clk_diffs = np.diff(clk_arr.astype(np.int8))
|
||||
data_diffs = np.diff(data_arr.astype(np.int8))
|
||||
clk_edges = np.where(clk_diffs == 1)[0]
|
||||
if len(clk_edges) > 0:
|
||||
matching = int(np.sum(data_diffs[clk_edges] != 0))
|
||||
clk_data_corr = f'{matching}/{len(clk_edges)} ({matching / len(clk_edges) * 100:.0f}%)'
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
'transitions': transitions,
|
||||
'clk_data_correlation': clk_data_corr,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_analysis(path, clk_name, data_name, rst_name, vcc_name):
|
||||
"""Run full signal analysis on a .sr file. Returns results dict."""
|
||||
meta = parse_sr_metadata(path)
|
||||
sr = meta['samplerate']
|
||||
|
||||
clk_bit = get_channel_bit(meta, clk_name)
|
||||
data_bit = get_channel_bit(meta, data_name)
|
||||
rst_bit = get_channel_bit(meta, rst_name)
|
||||
vcc_bit = get_channel_bit(meta, vcc_name)
|
||||
|
||||
raw = read_sr_all(path)
|
||||
total_samples = len(raw)
|
||||
duration_s = total_samples / sr
|
||||
|
||||
# Find first CLK activity
|
||||
first_clk_sample = None
|
||||
if clk_bit is not None:
|
||||
clk_arr = extract_bit(raw, clk_bit)
|
||||
clk_edges = np.where(np.diff(clk_arr.astype(np.int8)) == 1)[0]
|
||||
if len(clk_edges) > 0:
|
||||
first_clk_sample = int(clk_edges[0])
|
||||
|
||||
results = {
|
||||
'samplerate_mhz': sr / 1e6,
|
||||
'total_samples': total_samples,
|
||||
'duration_ms': duration_s * 1000,
|
||||
'channels_found': {
|
||||
'CLK': clk_name if clk_bit is not None else None,
|
||||
'DATA': data_name if data_bit is not None else None,
|
||||
'RST': rst_name if rst_bit is not None else None,
|
||||
'VCC': vcc_name if vcc_bit is not None else None,
|
||||
},
|
||||
'first_activity_sample': first_clk_sample or 0,
|
||||
}
|
||||
|
||||
# CLK
|
||||
if clk_bit is not None:
|
||||
results['CLK'] = measure_clk_fast(extract_bit(raw, clk_bit), sr)
|
||||
else:
|
||||
results['CLK'] = {'ok': False, 'reason': f'channel "{clk_name}" not found'}
|
||||
|
||||
# RST
|
||||
if rst_bit is not None:
|
||||
rst_arr = extract_bit(raw, rst_bit)
|
||||
rst_t = count_transitions(rst_arr)
|
||||
results['RST'] = {
|
||||
'ok': True, 'transitions': rst_t,
|
||||
'stable_pct': (1 - rst_t / max(total_samples, 1)) * 100,
|
||||
'noisy': rst_t > total_samples * 0.01,
|
||||
}
|
||||
else:
|
||||
results['RST'] = {'ok': False, 'reason': f'channel "{rst_name}" not found'}
|
||||
|
||||
# VCC
|
||||
if vcc_bit is not None:
|
||||
vcc_arr = extract_bit(raw, vcc_bit)
|
||||
vcc_t = count_transitions(vcc_arr)
|
||||
results['VCC'] = {
|
||||
'ok': True, 'transitions': vcc_t,
|
||||
'stable_pct': (1 - vcc_t / max(total_samples, 1)) * 100,
|
||||
'noisy': vcc_t > total_samples * 0.01,
|
||||
}
|
||||
else:
|
||||
results['VCC'] = {'ok': False, 'reason': f'channel "{vcc_name}" not found'}
|
||||
|
||||
# DATA
|
||||
if data_bit is not None:
|
||||
data_arr = extract_bit(raw, data_bit)
|
||||
clk_arr = extract_bit(raw, clk_bit) if clk_bit is not None else np.array([], dtype=np.uint8)
|
||||
results['DATA'] = measure_data_fast(data_arr, clk_arr, sr)
|
||||
else:
|
||||
results['DATA'] = {'ok': False, 'reason': f'channel "{data_name}" not found'}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def print_results(results):
|
||||
"""Pretty-print signal diagnostic results."""
|
||||
print(f" Sample rate: {results['samplerate_mhz']:.1f} MHz")
|
||||
print(f" Samples: {results['total_samples']}")
|
||||
print(f" Duration: {results['duration_ms']:.1f} ms")
|
||||
found = results['channels_found']
|
||||
print(f" Channels: CLK={found['CLK']} DATA={found['DATA']} RST={found['RST']} VCC={found['VCC']}")
|
||||
if results.get('first_activity_sample', 0) > 0:
|
||||
print(f" First activity: sample {results['first_activity_sample']}")
|
||||
print()
|
||||
|
||||
# CLK
|
||||
clk = results['CLK']
|
||||
print(" CLK:")
|
||||
if clk.get('ok'):
|
||||
print(f" Frequency: {clk['frequency_mhz']:.2f} MHz")
|
||||
print(f" Period: {clk['median_period_samples']:.1f} samples")
|
||||
print(f" Rising edges: {clk['rising_edges']}")
|
||||
print(f" Duration: {clk['duration_ms']:.1f} ms")
|
||||
if clk.get('freq_change'):
|
||||
print(f" WARNING: {clk['freq_change']}")
|
||||
else:
|
||||
print(f" FAIL: {clk.get('reason', 'unknown')}")
|
||||
|
||||
# RST
|
||||
rst = results['RST']
|
||||
print(" RST:")
|
||||
if rst.get('ok'):
|
||||
print(f" Transitions: {rst['transitions']}")
|
||||
print(f" Stable: {rst['stable_pct']:.1f}%")
|
||||
if rst['noisy']:
|
||||
print(f" WARNING: noisy (>{1}% transitions) — consider --rst=ignore")
|
||||
else:
|
||||
print(f" Not present: {rst.get('reason', 'n/a')}")
|
||||
|
||||
# VCC
|
||||
vcc = results['VCC']
|
||||
print(" VCC:")
|
||||
if vcc.get('ok'):
|
||||
print(f" Transitions: {vcc['transitions']}")
|
||||
print(f" Stable: {vcc['stable_pct']:.1f}%")
|
||||
if vcc['noisy']:
|
||||
print(f" WARNING: noisy (>{1}% transitions) — consider --vcc=ignore")
|
||||
else:
|
||||
print(f" Not present: {vcc.get('reason', 'n/a')}")
|
||||
|
||||
# DATA
|
||||
data = results['DATA']
|
||||
print(" DATA:")
|
||||
if data.get('ok'):
|
||||
print(f" Transitions: {data['transitions']}")
|
||||
if data.get('clk_data_correlation'):
|
||||
print(f" CLK corr: {data['clk_data_correlation']}")
|
||||
else:
|
||||
print(f" FAIL: {data.get('reason', 'unknown')}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Signal quality diagnostic for .sr traces')
|
||||
parser.add_argument('sr_file', help='Path to .sr file')
|
||||
parser.add_argument('--clk', default='CLK', help='CLK channel name (default: CLK)')
|
||||
parser.add_argument('--data', default='DATA', help='DATA channel name (default: DATA)')
|
||||
parser.add_argument('--rst', default='RST', help='RST channel name (default: RST)')
|
||||
parser.add_argument('--vcc', default='VCC', help='VCC channel name (default: VCC)')
|
||||
args = parser.parse_args()
|
||||
|
||||
results = run_analysis(args.sr_file, args.clk, args.data, args.rst, args.vcc)
|
||||
print(f"Signal diagnostic: {args.sr_file}")
|
||||
print_results(results)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare a decoded GSMTAP pcap against a PC/SC reader APDU log.
|
||||
|
||||
Both describe the same ISO 7816-4 exchange stream: the reader log gives
|
||||
one line per exchange as `C-APDU => R-APDU`, and the decoder emits each
|
||||
exchange as one GSMTAP event carrying concatenated `C-APDU + R-APDU`.
|
||||
|
||||
The capture is usually a TIME-SLICE of the reader session (capture may
|
||||
start mid-session), and the reader SDK does NOT surface CAT/proactive
|
||||
traffic (TERMINAL PROFILE, FETCH, STATUS, ENVELOPE, ...) -- so the pcap
|
||||
is the reader log's exchange sequence, interspersed with extra
|
||||
proactive/polling exchanges. We therefore align via longest-common-
|
||||
subsequence, not greedy prefix matching.
|
||||
|
||||
Degradation metrics:
|
||||
- BAD_FCS: decoder-flagged desynced APDUs (GSMTAP_FLAG_BAD_FCS)
|
||||
- CONCAT: packets that embed complete exchanges (mis-frame swallowed
|
||||
several real exchanges into one)
|
||||
- payload mismatches: LCS-matched pairs where bytes differ
|
||||
- suspicious CLA: APDUs with unexpected CLA values
|
||||
|
||||
Usage:
|
||||
python3 tools/vs_reader.py <reader.log> <capture.pcap>
|
||||
python3 tools/vs_reader.py --pcap-only <capture.pcap> (no reader log)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# CAT commands the PC/SC reader SDK handles internally and does not log.
|
||||
CAT_INS = {
|
||||
0x10, # TERMINAL PROFILE
|
||||
0x12, # FETCH
|
||||
0x14, # TERMINAL RESPONSE
|
||||
0x24, # GET INPUT
|
||||
0x26, # PROVIDE LOCAL INFO
|
||||
0xC2, # ENVELOPE
|
||||
0xF2, # STATUS
|
||||
}
|
||||
|
||||
# GSMTAP_FLAG_BAD_FCS from gsmtap_stream.py
|
||||
GSMTAP_FLAG_BAD_FCS = 0x01
|
||||
|
||||
# Valid CLA values for ISO 7816 / 3GPP. Mirror of plausible_cla in pd.py:
|
||||
# interindustry structures (high nibble 0x0/0x1/0x4/0x5), which covers the
|
||||
# logical-channel classes 0x01-0x03 (e.g. the command after MANAGE CHANNEL
|
||||
# opens channel 1), plus the proprietary SIM/UICC CAT classes.
|
||||
def _plausible_cla(cla):
|
||||
if not (cla & 0x80):
|
||||
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
||||
return cla in (0x80, 0x84, 0xA0, 0xB0)
|
||||
|
||||
|
||||
VALID_CLA = frozenset(cla for cla in range(256) if _plausible_cla(cla))
|
||||
|
||||
# ISO 7816-3 T=0 max TPDU length: header(5) + procedure bytes + 255 data + SW(2)
|
||||
MAX_TPDU_LEN = 271
|
||||
|
||||
# --- embedded-exchange (CONCAT) detector ---
|
||||
# A byte-drop mis-frame can swallow several real T=0 exchanges into one
|
||||
# structurally valid packet: each swallowed header survives verbatim in the
|
||||
# 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
|
||||
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
||||
EMBEDDED_INS = frozenset((
|
||||
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
||||
0xA2, 0xA4, 0xB0, 0xB2, 0xB6,
|
||||
0xC0, 0xC2, 0xCA, 0xCB, 0xD6, 0xDC, 0xF2,
|
||||
))
|
||||
# Real SW1 values that are not also frequent data bytes (BER-TLV tags).
|
||||
EMBEDDED_SW1 = frozenset((
|
||||
0x60, 0x61, 0x62, 0x63, 0x67, 0x68, 0x69,
|
||||
0x6A, 0x6B, 0x6D, 0x6E, 0x6F, 0x90, 0x91,
|
||||
0x93, 0x98, 0x99,
|
||||
))
|
||||
EMBEDDED_SW1_SLACK = 3 # SW1 search window after the nominal data end
|
||||
|
||||
|
||||
def embedded_exchanges(buf):
|
||||
"""Return [(header_start, sw1_index), ...] for complete T=0 exchanges
|
||||
embedded inside a packet's data region (index 5 onward).
|
||||
|
||||
Pattern: header (CLA INS P1 P2 P3) + P3 data bytes + interior SW1/SW2
|
||||
before the packet's own final status word. CLA must be structurally
|
||||
valid (same rule as _plausible_cla); INS must be a real command byte;
|
||||
SW1 is accepted within a small slack window after the nominal data end
|
||||
and must be interior (trailing bytes follow it).
|
||||
"""
|
||||
n = len(buf)
|
||||
out = []
|
||||
if n < 12:
|
||||
return out
|
||||
q = 5
|
||||
while q <= n - 9:
|
||||
s = q
|
||||
cla, ins, _p1, _p2, p3 = buf[s:s + 5]
|
||||
if cla not in EMBEDDED_CLA:
|
||||
q += 1
|
||||
continue
|
||||
if ins not in EMBEDDED_INS:
|
||||
q += 1
|
||||
continue
|
||||
if p3 == 0:
|
||||
q += 1
|
||||
continue
|
||||
d1 = s + 5 + p3
|
||||
if d1 + 3 > n:
|
||||
q += 1
|
||||
continue
|
||||
sw1 = -1
|
||||
for e in range(d1, min(n - 1, d1 + EMBEDDED_SW1_SLACK + 1)):
|
||||
if buf[e] in EMBEDDED_SW1:
|
||||
sw1 = e
|
||||
break
|
||||
if sw1 == -1 or sw1 >= n - 2:
|
||||
q += 1
|
||||
continue
|
||||
out.append((s, sw1))
|
||||
q += 1
|
||||
return out
|
||||
|
||||
|
||||
def _plausible_sw(sw1):
|
||||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||
|
||||
|
||||
def _plausible_ins(ins):
|
||||
if ins == 0x00:
|
||||
return False
|
||||
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
||||
return False
|
||||
if (ins & 0x01) != 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validate_t0_apdu(packet):
|
||||
'''See validate_t0_apdu in pd.py; same rules, duplicated here so the
|
||||
standalone vs_reader can classify pcap entries without importing the
|
||||
sigrok decoder module.'''
|
||||
n = len(packet)
|
||||
|
||||
if n < 7:
|
||||
return (False, 'too short (< 7)')
|
||||
if n > MAX_TPDU_LEN:
|
||||
return (False, 'exceeds T=0 max length')
|
||||
|
||||
cla = packet[0]
|
||||
ins = packet[1]
|
||||
sw1 = packet[-2]
|
||||
|
||||
if cla not in VALID_CLA:
|
||||
return (False, 'invalid CLA')
|
||||
if not _plausible_ins(ins):
|
||||
return (False, 'invalid INS')
|
||||
if not _plausible_sw(sw1):
|
||||
return (False, 'invalid SW1')
|
||||
|
||||
return (True, None)
|
||||
|
||||
|
||||
def parse_reader_log(path):
|
||||
"""Return list of exchange byte-strings (C-APDU + R-APDU concatenated)."""
|
||||
exchanges = []
|
||||
for line in open(path, errors='ignore'):
|
||||
m = re.match(r'^T0: .* TPDU: ([0-9a-fA-F]+)\s*=>\s*(.*)$', line.strip())
|
||||
if not m:
|
||||
continue
|
||||
cmd = m.group(1)
|
||||
resp = m.group(2).strip()
|
||||
data = ''
|
||||
sw = ''
|
||||
if resp.startswith('(no data)'):
|
||||
sw = resp[len('(no data)'):].strip().replace(' ', '')
|
||||
else:
|
||||
parts = resp.split()
|
||||
sw = parts[-1] if parts else ''
|
||||
data = ''.join(parts[:-1])
|
||||
exchanges.append((cmd + data + sw).lower())
|
||||
return exchanges
|
||||
|
||||
|
||||
def parse_pcap(path):
|
||||
"""Return (apdu_list, atr_count) where apdu_list is list of (apdu_hex, flags)
|
||||
tuples for APDU (sub_type 0) packets, and atr_count is the number of ATR
|
||||
(sub_type 1) packets."""
|
||||
data = open(path, 'rb').read()
|
||||
off = 24
|
||||
out = []
|
||||
atr_count = 0
|
||||
while off < len(data):
|
||||
if off + 16 > len(data):
|
||||
break
|
||||
ts_s, ts_u, caplen, origlen = struct.unpack('<IIII', data[off:off + 16])
|
||||
if off + 16 + caplen > len(data):
|
||||
break
|
||||
pkt = data[off + 16:off + 16 + caplen]
|
||||
off += 16 + caplen
|
||||
if caplen < 58:
|
||||
continue
|
||||
glen = struct.unpack('>H', pkt[38:40])[0]
|
||||
g = pkt[42:42 + glen - 8]
|
||||
if g[12] == 0: # APDU
|
||||
flags = g[15] # GSMTAP_FLAG_BAD_FCS etc.
|
||||
out.append((g[16:].hex(), flags))
|
||||
elif g[12] == 1: # ATR
|
||||
atr_count += 1
|
||||
return out, atr_count
|
||||
|
||||
|
||||
def is_cat(apdu_hex):
|
||||
b = bytes.fromhex(apdu_hex)
|
||||
if len(b) < 2:
|
||||
return False
|
||||
return b[0] == 0x80 and b[1] in CAT_INS
|
||||
|
||||
|
||||
def looks_valid(apdu_hex):
|
||||
"""Structural validation for an ISO 7816-4 T=0 APDU.
|
||||
|
||||
Delegates to validate_t0_apdu(); kept as a thin wrapper for
|
||||
backwards compatibility with existing reports."""
|
||||
b = bytes.fromhex(apdu_hex)
|
||||
valid, _ = validate_t0_apdu(b)
|
||||
return valid
|
||||
|
||||
|
||||
def byte_diff_details(expected_hex, actual_hex):
|
||||
"""Return list of (offset, expected_byte, actual_byte) for mismatched bytes."""
|
||||
eb = bytes.fromhex(expected_hex)
|
||||
ab = bytes.fromhex(actual_hex)
|
||||
diffs = []
|
||||
for i in range(min(len(eb), len(ab))):
|
||||
if eb[i] != ab[i]:
|
||||
diffs.append((i, eb[i], ab[i]))
|
||||
for i in range(len(eb), len(ab)):
|
||||
diffs.append((i, None, ab[i]))
|
||||
for i in range(len(ab), len(eb)):
|
||||
diffs.append((i, eb[i], None))
|
||||
return diffs
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('reader_log', nargs='?', default=None,
|
||||
help='PC/SC reader log')
|
||||
ap.add_argument('pcap', nargs='?', default=None,
|
||||
help='Decoded GSMTAP pcap')
|
||||
ap.add_argument('--pcap-only', action='store_true',
|
||||
help='Analyze pcap without a reader log (phone traces)')
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.pcap_only:
|
||||
reader = []
|
||||
if args.pcap is None:
|
||||
# --pcap-only pcap (pcap is the first positional)
|
||||
if args.reader_log:
|
||||
pcap_path = args.reader_log
|
||||
else:
|
||||
ap.error('--pcap-only requires a pcap argument')
|
||||
else:
|
||||
pcap_path = args.pcap
|
||||
else:
|
||||
if args.reader_log is None or args.pcap is None:
|
||||
ap.error('requires reader_log and pcap, or --pcap-only pcap')
|
||||
reader = parse_reader_log(args.reader_log)
|
||||
pcap_path = args.pcap
|
||||
|
||||
pcap_raw, atr_count = parse_pcap(pcap_path)
|
||||
pcap = [h for h, _ in pcap_raw]
|
||||
pcap_flags = [f for _, f in pcap_raw]
|
||||
|
||||
sm = difflib.SequenceMatcher(None, reader, pcap, autojunk=False)
|
||||
matched = sm.get_matching_blocks()
|
||||
matched = [m for m in matched if m.size > 0]
|
||||
|
||||
matched_reader = set()
|
||||
matched_pcap = set()
|
||||
for a, b, size in matched:
|
||||
for k in range(size):
|
||||
matched_reader.add(a + k)
|
||||
matched_pcap.add(b + k)
|
||||
|
||||
reader_missed = [i for i in range(len(reader)) if i not in matched_reader]
|
||||
pcap_unmatched = [i for i in range(len(pcap)) if i not in matched_pcap]
|
||||
pcap_cat = [i for i in pcap_unmatched if is_cat(pcap[i])]
|
||||
pcap_dup = [i for i in pcap_unmatched
|
||||
if i not in pcap_cat and looks_valid(pcap[i])]
|
||||
pcap_garbage = [i for i in pcap_unmatched
|
||||
if i not in pcap_cat and i not in pcap_dup]
|
||||
|
||||
# --- Degradation metrics ---
|
||||
|
||||
# BAD_FCS: decoder-flagged desynced APDUs
|
||||
bad_fcs_indices = [i for i in range(len(pcap))
|
||||
if pcap_flags[i] & GSMTAP_FLAG_BAD_FCS]
|
||||
bad_fcs_in_garbage = [i for i in bad_fcs_indices if i in pcap_garbage]
|
||||
bad_fcs_in_dup = [i for i in bad_fcs_indices if i in pcap_dup]
|
||||
bad_fcs_in_cat = [i for i in bad_fcs_indices if i in pcap_cat]
|
||||
bad_fcs_in_matched = [i for i in bad_fcs_indices if i in matched_pcap]
|
||||
|
||||
# CONCAT: structurally valid packets that embed complete exchanges
|
||||
# (a byte-drop mis-frame swallowed several real exchanges in one).
|
||||
concat_indices = [i for i in range(len(pcap))
|
||||
if embedded_exchanges(bytes.fromhex(pcap[i]))]
|
||||
|
||||
# Payload mismatches: LCS-matched pairs where bytes differ
|
||||
payload_mismatches = []
|
||||
for a, b, size in matched:
|
||||
for k in range(size):
|
||||
ri, pi = a + k, b + k
|
||||
if reader[ri] != pcap[pi]:
|
||||
payload_mismatches.append((ri, pi, reader[ri], pcap[pi]))
|
||||
|
||||
# Suspicious CLA: check all capture APDUs
|
||||
suspicious_cla = []
|
||||
for i, hex_str in enumerate(pcap):
|
||||
b = bytes.fromhex(hex_str)
|
||||
if len(b) < 1:
|
||||
continue
|
||||
cla = b[0]
|
||||
if cla == 0x80:
|
||||
continue
|
||||
if cla not in VALID_CLA:
|
||||
suspicious_cla.append((i, hex_str))
|
||||
|
||||
# --- Report ---
|
||||
|
||||
print('reader exchanges : %d' % len(reader))
|
||||
print('capture ATRs : %d' % atr_count)
|
||||
print('capture APDUs : %d' % len(pcap))
|
||||
print('byte-exact matches (LCS) : %d' % len(matched_reader))
|
||||
print('reader exchanges not in capture (pre-capture / dropped): %d'
|
||||
% len(reader_missed))
|
||||
print('capture APDUs not in reader log:')
|
||||
print(' - proactive/CAT (unlogged by reader SDK): %d' % len(pcap_cat))
|
||||
print(' - valid exchanges (reader retries/dupes): %d' % len(pcap_dup))
|
||||
print(' - GARBAGE (mis-framed/desync): %d' % len(pcap_garbage))
|
||||
for i in pcap_garbage[:15]:
|
||||
print(' GARBAGE: %s' % pcap[i])
|
||||
|
||||
print('')
|
||||
print('Degradation metrics:')
|
||||
print(' - BAD_FCS (desynced by decoder): %d' % len(bad_fcs_indices))
|
||||
if bad_fcs_indices:
|
||||
print(' in GARBAGE: %d, in retries: %d, in CAT: %d, in matched: %d'
|
||||
% (len(bad_fcs_in_garbage), len(bad_fcs_in_dup),
|
||||
len(bad_fcs_in_cat), len(bad_fcs_in_matched)))
|
||||
print(' - CONCAT (multi-exchange swallowed in one packet): %d'
|
||||
% len(concat_indices))
|
||||
for i in concat_indices[:10]:
|
||||
print(' pcap[%d]: %s' % (i, pcap[i][:72]))
|
||||
print(' - payload mismatches (LCS-matched, bytes differ): %d'
|
||||
% len(payload_mismatches))
|
||||
for ri, pi, exp, act in payload_mismatches[:10]:
|
||||
diffs = byte_diff_details(exp, act)
|
||||
diff_str = ', '.join(
|
||||
'off%+d: %02x->%02x' % (o, e if e is not None else 0xff,
|
||||
a if a is not None else 0xff)
|
||||
for o, e, a in diffs[:5])
|
||||
print(' reader[%d] vs pcap[%d]: %s' % (ri, pi, diff_str))
|
||||
if len(payload_mismatches) > 10:
|
||||
print(' ... and %d more' % (len(payload_mismatches) - 10))
|
||||
print(' - suspicious CLA: %d' % len(suspicious_cla))
|
||||
for i, hex_str in suspicious_cla[:10]:
|
||||
b = bytes.fromhex(hex_str)
|
||||
print(' pcap[%d]: CLA=0x%02x %s' % (i, b[0], hex_str[:40]))
|
||||
if len(suspicious_cla) > 10:
|
||||
print(' ... and %d more' % (len(suspicious_cla) - 10))
|
||||
|
||||
# A pcap with zero APDUs while the reader log has exchanges is a
|
||||
# vacuous pass: the decoder produced nothing to validate. Flag it.
|
||||
no_apdu_failure = (len(pcap) == 0 and len(reader) > 0)
|
||||
if no_apdu_failure:
|
||||
print('\nWARNING: capture contains 0 APDUs but reader log has %d exchange(s).'
|
||||
% len(reader))
|
||||
|
||||
ok = (len(pcap_garbage) == 0 and not no_apdu_failure)
|
||||
print('\nRESULT: %s' % ('OK - capture fully explained (reader + CAT + valid retries)'
|
||||
if ok else
|
||||
'UNCLEAN - %d mis-framed capture APDU(s)%s'
|
||||
% (len(pcap_garbage),
|
||||
', 0 capture APDUs' if no_apdu_failure else '')))
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||