Compare commits
16
Commits
3bf5cee222
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b54e130c2 | ||
|
|
6e663dcdcb | ||
|
|
8214089b2d | ||
|
|
9112eafec0 | ||
|
|
847b49ec12 | ||
|
|
a385648a80 | ||
|
|
8e36419143 | ||
|
|
50fe1bc51b | ||
|
|
2e3a563824 | ||
|
|
1cbafe715c | ||
|
|
6e6f465841 | ||
|
|
3e39032802 | ||
|
|
b574f900a1 | ||
|
|
883033e712 | ||
|
|
54dfc6586d | ||
|
|
9f42a88c01 |
No files matched your search
@@ -0,0 +1,93 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref_name }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: python3 tests/test_gsmtap.py
|
||||||
|
|
||||||
|
- name: Verify tag matches pd.py VERSION
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version=$(sed -n "s/^VERSION = '\([0-9][0-9.]*\)'.*/\1/p" pd.py)
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echo "::error::cannot read VERSION from pd.py"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
|
||||||
|
echo "::error::tag $GITHUB_REF_NAME does not match pd.py VERSION '$version'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "tag $GITHUB_REF_NAME matches pd.py VERSION $version"
|
||||||
|
|
||||||
|
- name: Build release archives
|
||||||
|
run: ./tools/build_release.sh "$GITHUB_REF_NAME"
|
||||||
|
|
||||||
|
- name: Write the install notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version=$(sed -n "s/^VERSION = '\([0-9][0-9.]*\)'.*/\1/p" pd.py)
|
||||||
|
notes="$RUNNER_TEMP/release-notes.md"
|
||||||
|
cat > "$notes" <<'EOF'
|
||||||
|
Runtime release - the decoder, setup/start scripts, docs and README
|
||||||
|
images; no git history and no development files.
|
||||||
|
|
||||||
|
**Install - release package (recommended)**
|
||||||
|
- Linux: `sigrok_iso7816_stream-<VERSION>.tar.gz`
|
||||||
|
- Windows: `sigrok_iso7816_stream-<VERSION>.zip`
|
||||||
|
|
||||||
|
Extract the archive, then run `./setup.sh` + `./start.sh` (Linux) or
|
||||||
|
`setup.bat` + `start.bat` (Windows). `SHA256SUMS` (attached) verifies
|
||||||
|
both downloads. Requires sigrok-cli and the FX2 firmware; see the
|
||||||
|
README *Install and start* section for the full prerequisites.
|
||||||
|
|
||||||
|
**Install - source checkout (development)**
|
||||||
|
`git clone https://github.com/anttro/sigrok_iso7816_stream.git`, then
|
||||||
|
the same setup/start scripts. The clone adds `tests/`, the baseline
|
||||||
|
tools and the (local-only) example traces; the release package is the
|
||||||
|
runtime tree.
|
||||||
|
EOF
|
||||||
|
sed -i "s/<VERSION>/$version/g" "$notes"
|
||||||
|
|
||||||
|
- name: Create or update the GitHub release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version=$(sed -n "s/^VERSION = '\([0-9][0-9.]*\)'.*/\1/p" pd.py)
|
||||||
|
notes="$RUNNER_TEMP/release-notes.md"
|
||||||
|
# Upload into an existing release first. If the release does not
|
||||||
|
# exist yet, create it as a draft so the published release never
|
||||||
|
# appears with a partial asset set. The install notes are prepended
|
||||||
|
# to GitHub's generated changelog.
|
||||||
|
if ! gh release upload "$GITHUB_REF_NAME" dist/* --clobber; then
|
||||||
|
gh release create "$GITHUB_REF_NAME" dist/* --draft \
|
||||||
|
--title "sigrok_iso7816_stream $version" \
|
||||||
|
--notes-file "$notes" --generate-notes
|
||||||
|
fi
|
||||||
|
# Publish a new draft and re-publish an existing one.
|
||||||
|
gh release edit "$GITHUB_REF_NAME" --draft=false
|
||||||
@@ -4,3 +4,6 @@ __pycache__/
|
|||||||
|
|
||||||
# Trace fixtures are local-only (large, machine-specific); never tracked.
|
# Trace fixtures are local-only (large, machine-specific); never tracked.
|
||||||
examples/
|
examples/
|
||||||
|
|
||||||
|
# Release archives built by tools/build_release.sh.
|
||||||
|
dist/
|
||||||
@@ -142,10 +142,10 @@ the reader log for all reader exchanges:
|
|||||||
|
|
||||||
Both traces show 0 garbage APDUs, 0 payload mismatches, and RESULT: OK.
|
Both traces show 0 garbage APDUs, 0 payload mismatches, and RESULT: OK.
|
||||||
|
|
||||||
**Root cause:** `_edge_read` is initialized to `True` and never set to
|
**Root cause:** `_edge_read` was initialized to `True` and never set to
|
||||||
`False`, so the edge-read T=0 procedure loop (lines 1808–1895) always
|
`False`, so the edge-read T=0 procedure loop always executed and the
|
||||||
executes — the ATR-based procedure loop (lines 1898–1941) is dead code.
|
ATR-based procedure loop was dead code. **FIXED (v1.9.7):** the flag and
|
||||||
The edge-read path had two bugs:
|
the dead loop are removed. The edge-read path had two bugs:
|
||||||
|
|
||||||
1. **NULLs treated as command data (FIXED).** The first post-header byte
|
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
|
was checked for ACK; if it was NULL (0x60), it entered the case-3/4
|
||||||
@@ -160,14 +160,30 @@ The edge-read path had two bugs:
|
|||||||
|
|
||||||
#### CLA validation note
|
#### CLA validation note
|
||||||
|
|
||||||
All example traces (`test_7`, `test_8`, `xiaomi`, `samsung`)
|
`plausible_cla()` (v1.9.3) accepts the higher logical channels:
|
||||||
contain only CLA `0x00` (interindustry) and `0x8x` (USIM/CAT, mostly `0x80`
|
|
||||||
with occasional `0x81` for logical channel 1).
|
- interindustry `0x00`-`0x1F` (channels 0-3 in b2b1) and `0x40`-`0x5F`
|
||||||
The decoder must **not** mandate specific CLA values — other CLA values
|
(`01xx xxxx`: channels 4-19 in b4..b1, no CLA-level SM; `0x50`-`0x5F`
|
||||||
(e.g. `0x04`, `0x08`, `0x84`, `0xA0`, `0xB0`) are valid per ISO 7816-4 / 3GPP
|
are the legacy chaining variants);
|
||||||
and must be accepted. However, during test runs against the example traces,
|
- the UICC extended classes `'CX'`/`'EX'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`,
|
||||||
any CLA value outside `0x00-0x0F` and `0x80-0x8F` is an error flag indicating
|
channels 4-19, SM in b6 -- TS 102 221 Table 10.4a);
|
||||||
the decoder mis-framed the byte stream.
|
- `0x80`/`0x84`/`0xA0`/`0xB0` (0xB0 has no pinned-spec basis; kept for
|
||||||
|
backward compatibility).
|
||||||
|
|
||||||
|
Rejected: `0x20`-`0x3F` (ISO-reserved); `0x60`-`0x6F`/`0x70`-`0x7F` (the
|
||||||
|
SM-indicating extended classes -- `0x60`-`0x6F` overlaps printable ASCII
|
||||||
|
and the text byte `0x61` started a bogus frame on xiaomi_mi_a1_coldboot
|
||||||
|
when it was accepted); `0x81`-`0x83`/`0x85`-`0x8F`/`0xA1`-`0xAF` (the
|
||||||
|
`'8X'`/`'AX'` standard-channel variants for channels 0-3 other than the
|
||||||
|
legacy `0x80`/`0x84`/`0xA0` -- the FCP byte `0xA5` did the
|
||||||
|
same); `0x90`-`0x9F`, `0xD0`-`0xDF`, `0xF0`-`0xFF`. They are spec-coded
|
||||||
|
but cost frame-start precision; add any of them back when a real trace
|
||||||
|
requires it.
|
||||||
|
During test runs such a CLA is an error flag indicating the decoder
|
||||||
|
mis-framed the byte stream. The example traces carry `0x00`
|
||||||
|
(interindustry), `0x01` (logical channel 1 -- the SELECT-by-AID after
|
||||||
|
MANAGE CHANNEL opens it) and `0x80` (USIM/CAT); no trace uses channels 2-3
|
||||||
|
or the higher channels yet.
|
||||||
|
|
||||||
#### ISO 7816 APDU validation (enforced from v1.1.7)
|
#### ISO 7816 APDU validation (enforced from v1.1.7)
|
||||||
|
|
||||||
@@ -179,10 +195,18 @@ Validation is purely structural (no content scanning of command/response
|
|||||||
data), so the false-positive rate is very low. The checks are:
|
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).
|
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`,
|
2. CLA byte is a structurally valid ISO 7816-4 / UICC class — see
|
||||||
`0x84`, `0xA0`, `0xB0`).
|
`plausible_cla()` (v1.9.3): `0x00`-`0x1F`, `0x40`-`0x5F` (logical
|
||||||
|
channels 4-19), `0xC0`-`0xCF`/`0xE0`-`0xEF` (`'CX'`/`'EX'`), plus
|
||||||
|
`0x80`/`0x84`/`0xA0`/`0xB0`.
|
||||||
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
||||||
collision), and the least-significant bit is `0`.
|
collision). Odd INS codes are accepted only as the defined alternate
|
||||||
|
variants (ISO 7816-4 Table 4 pairs: `'0F'`, `'21'`, `'87'`, `'A1'`,
|
||||||
|
`'B1'`, `'B3'`, `'C3'`, `'CB'`, `'D1'`, `'D7'`, `'DB'`, `'DD'`;
|
||||||
|
TS 102 221 AUTHENTICATE `'89'`). Other odd values stay rejected: a
|
||||||
|
mis-framed boundary exposes data bytes, and the text run `49 4D`
|
||||||
|
("IM") started a bogus frame on xiaomi_phone_sample when every odd INS
|
||||||
|
was accepted.
|
||||||
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
||||||
`0x60` is explicitly invalid: it is the T=0 NULL procedure byte
|
`0x60` is explicitly invalid: it is the T=0 NULL procedure byte
|
||||||
(ISO 7816-3 §10.3.3 Table 11 marks SW1 as `'6X'` **except `'60'`**;
|
(ISO 7816-3 §10.3.3 Table 11 marks SW1 as `'6X'` **except `'60'`**;
|
||||||
@@ -309,6 +333,232 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
|||||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||||
|
|
||||||
|
### v1.9.7: spec fixes, T=1/parity, EOF guards, dead code + rate scaling
|
||||||
|
|
||||||
|
The audit's P2 items:
|
||||||
|
|
||||||
|
- **Odd INS set completed.** `0x73` MANAGE SECURE CHANNEL and `0x75`
|
||||||
|
TRANSACT DATA (TS 102 221 Table 10.5) join `ODD_INS`; the mirrored set in
|
||||||
|
`tools/vs_reader.py` and the pinned test are updated.
|
||||||
|
- **9E/9F are SW1.** ISO 7816-3 §10.3.3 Table 11: every `9X` value is
|
||||||
|
SW1. The fabricated "extended-length procedure byte" (it consumed the
|
||||||
|
next byte as a length, eating SW2 and losing the status word) is gone.
|
||||||
|
The second copy lived in the dead ATR-based loop removed below.
|
||||||
|
- **T=1 blocks.** `t1_parse_block()` EOF-guards every read (a truncated
|
||||||
|
block returns no half INF instead of raising) and bounds the M-bit chain
|
||||||
|
with `T1_CHAIN_MAX` instead of unbounded recursion. The caller's
|
||||||
|
duplicate "chain follow-up" loop -- it read the *next* block after every
|
||||||
|
I-block, usually the peer's R-block, and merged the following I-block's
|
||||||
|
INF into the packet -- is gone. R/S blocks emit nothing; the (possibly
|
||||||
|
chained) I-block INF is emitted as the APDU payload. No T=1 trace
|
||||||
|
exists in the corpus, so this is unit-tested only.
|
||||||
|
- **Parity flag.** A packet containing a parity-error byte is now emitted
|
||||||
|
with `GSMTAP_FLAG_BAD_FCS` (the CHKSUM annotation alone was invisible to
|
||||||
|
downstream tooling), with a log note when the exchange is otherwise
|
||||||
|
clean.
|
||||||
|
- **EOF guards.** `handle_atr()` (TA/TB/TC/TD, historical bytes, TCK) and
|
||||||
|
`handle_pps()` (request and response fields, including the optional
|
||||||
|
PPS1-3) return cleanly on a truncated capture instead of raising on a
|
||||||
|
None byte; the T=1 block parser likewise.
|
||||||
|
- **Dead code removed.** `_edge_read` was initialized `True` and never
|
||||||
|
cleared, so the "ATR-based" T=0 procedure loop (58 lines) never ran and
|
||||||
|
the 1.5-ETU guard branch was unreachable. The flag and both dead paths
|
||||||
|
are removed; the edge reader is the only T=0 framing reader (the
|
||||||
|
CLK-sync reader is selected by `_clock_skip_confident`). The live loop
|
||||||
|
was dedented, not rewritten -- the trace metrics are unchanged.
|
||||||
|
- **Samplerate scaling.** `_samples_for_us()` replaces the hard-coded
|
||||||
|
16 MHz thresholds: the start-bit deglitch (`_min_pulse_samples`; `None`
|
||||||
|
= 2 samples @ 16 MHz), the `min_high` floor (0.25 us) and the ETU glitch
|
||||||
|
floor in `_etu_candidate` (0.75 us). At 16 MHz the values are
|
||||||
|
unchanged; another capture rate now scales instead of breaking the
|
||||||
|
filters.
|
||||||
|
|
||||||
|
Validation: 130/130 unit tests (13 new: 9E/9F SW1 for both values, the
|
||||||
|
parity BAD_FCS flag, T=1 I/R/S blocks + M chain + two truncation cases
|
||||||
|
+ a one-block-per-step caller test + two PPS truncations, the truncated
|
||||||
|
ATR TA, the samplerate/ETU-floor vectors); the 10-trace suite is
|
||||||
|
unchanged from v1.9.6 (identical
|
||||||
|
APDU/CHKSUM/CONCAT/BAD_FCS counts, `RESULT: OK` everywhere);
|
||||||
|
`compare_baseline.sh` reports IDENTICAL. VERSION -> 1.9.7.
|
||||||
|
|
||||||
|
### v1.9.6: ETU lock validation, hunt recovery, resync + ATR acceptance
|
||||||
|
|
||||||
|
The audit's P1 findings. The validation helpers are unit-tested; the trace
|
||||||
|
suite stays at the v1.9.5 metrics.
|
||||||
|
|
||||||
|
- **ETU measurement validation (`_etu_candidate`).** The mid-session
|
||||||
|
estimator used to lock the inter-character guard as the ETU on all-zero
|
||||||
|
data: a 0x00 run gives 10-ETU LOW runs plus a short HIGH guard, so the
|
||||||
|
smallest recurring spacing is the guard (2x-3x the true ETU), and a lone
|
||||||
|
0x00 character's single 10-ETU spacing is 10x. A burst now needs at
|
||||||
|
least two usable spacings, and a burst whose minimum recurrence is a HIGH
|
||||||
|
guard with every other spacing >= 2x it is rejected -- the decoder waits
|
||||||
|
for a burst with bit transitions (this covers the 2-5 ETU guard range).
|
||||||
|
A failed measurement leaves `bit_samples` unset instead of committing the
|
||||||
|
old `clock_skip*3` guess.
|
||||||
|
- **Invalidation.** `_confirm_etu()` no longer counts a dropped frame (no
|
||||||
|
genuine start -> empty bits) as parity-OK, and a rejected measurement now
|
||||||
|
clears `bit_samples`, `_etu_measured` and `_clock_skip_confident` (the old
|
||||||
|
code kept sampling at the rejected rate).
|
||||||
|
- **Hunt ETU recovery.** `handle_atr()` pre-seeds `bit_samples` with the
|
||||||
|
default-rate guess while the recovery condition tested `bit_samples is
|
||||||
|
None`, so the v1.6.0 live-ETU recovery could never fire from the hunt.
|
||||||
|
The condition now keys off `_etu_measured` (a real DATA measurement); a
|
||||||
|
measured rate still suppresses the recovery.
|
||||||
|
- **`resync_idle()`** now actually waits: it used `wait({'skip': 0})` in a
|
||||||
|
loop (the same sample 48 times -> a no-op). It now waits on the DATA idle
|
||||||
|
level (plus RST/VCC edges, bounded) before the next peek.
|
||||||
|
- **ATR acceptance.** A DATA-state `0x3B`/`0x3F` is an ATR candidate only
|
||||||
|
before any APDU has been emitted (`_apdu_seen`); a parsed ATR is rejected
|
||||||
|
when any of its bytes had a parity error or when it is a bare TS+T0
|
||||||
|
(`3B 00`). A data run misread at a frame boundary used to be committed
|
||||||
|
as an ATR (a fabricated `3b80900000` appeared while developing this
|
||||||
|
change), forcing `clock_skip=372` for the rest of the capture. Real
|
||||||
|
warm-reset ATRs are parity-clean and commit as before.
|
||||||
|
- **Inverse convention (TS=0x3F).** The readers assumed an idle-LOW line
|
||||||
|
and a rising start bit; ISO 7816-3 7.1 keeps the line idle HIGH and the
|
||||||
|
start bit LOW in both conventions -- only the data polarity and order
|
||||||
|
change. `wait_data_falling`/`resync_idle` now use the HIGH idle for
|
||||||
|
inverse too, `get_bytes()` assembles the inverse data msb-first, and the
|
||||||
|
DATA state accepts `0x3F` alongside `0x3B`. The data readers already
|
||||||
|
inverted the bit polarity. A real inverse TS decodes as `0x03` through
|
||||||
|
the direct reader, so `ts_from_bits()` matches the raw ISO 7816-3 8.1
|
||||||
|
pattern (moments 1-4 LHHL, 5-7 LLL/HHH, 8-10 LLH) in both the hunt and
|
||||||
|
the DATA state instead of trusting the decoded byte. The readers exempt
|
||||||
|
a matching TS from the parity check: its moment 10 is fixed by the
|
||||||
|
pattern, and the inverse TS is legitimately odd over moments 2-10, so it
|
||||||
|
must not log a CHKSUM or reject the ATR. No inverse-convention capture
|
||||||
|
exists in the corpus, so this is spec-verified, not trace-verified.
|
||||||
|
|
||||||
|
Validation: 117/117 unit tests (18 new: the ETU candidate vectors, the
|
||||||
|
hunt-recovery seeded/measured cases, the resync level wait, the bare-ATR
|
||||||
|
and parity-ATR guards, the `_apdu_seen` gate both ways, the inverse
|
||||||
|
bit-order/start-edge/TS-pattern vectors); the 10-trace suite is unchanged
|
||||||
|
from v1.9.5 (identical APDU/CHKSUM/CONCAT/BAD_FCS counts, `RESULT: OK`
|
||||||
|
everywhere);
|
||||||
|
`compare_baseline.sh` reports IDENTICAL. VERSION -> 1.9.6.
|
||||||
|
|
||||||
|
### v1.9.5: T=0 framing — immediate SW, short responses, Le=256
|
||||||
|
|
||||||
|
The audit's P0 framing fixes, each pinned by a `TestDecodeStep` vector:
|
||||||
|
|
||||||
|
- **Immediate SW.** The first post-header byte is classified as a status
|
||||||
|
word before the ACK/data decision. ISO 7816-3 §10.3.3 lets the card
|
||||||
|
finish at the first procedure byte; a case-2 rejection (`6A82`, `6CXX`,
|
||||||
|
...) carries no ACK and used to be read as command data (the exchange
|
||||||
|
and the following one were silently consumed).
|
||||||
|
- **Short responses (Lr < Le).** The ACK path still reads the P3 bytes,
|
||||||
|
but if the next byte is not SW1 the buffer is split at the first
|
||||||
|
plausible SW1 followed by a plausible CLA, the tail is replayed into
|
||||||
|
`_replay`, and the packet is emitted with `BAD_FCS`. Previously the SW
|
||||||
|
scan discarded the real SW, merged the following exchanges into a
|
||||||
|
clean-looking packet, and at EOF lost the exchange silently. A short
|
||||||
|
read with no plausible split is emitted flagged and resynced.
|
||||||
|
- **P3=0 = Le=256.** For an outgoing command P3=0 requests 256 bytes
|
||||||
|
(§10.3.2). The response data used to be discarded; it is now read until
|
||||||
|
SW and kept. The read-until-SW ambiguity on data bytes that look like
|
||||||
|
SW1 remains (inherent to T=0 sniffing).
|
||||||
|
- **~INS ACK.** One data byte per ACK (Table 11) via the new
|
||||||
|
`_t0_read_response_data()` helper, instead of "all remaining".
|
||||||
|
- **Case 4.** ISO 7816-3 §12.2/Table 14 map case 4 to a separate GET
|
||||||
|
RESPONSE; the ACK path now appends any inline data instead of dropping
|
||||||
|
it (permissive extension; conforming cards send SW/61XY there).
|
||||||
|
|
||||||
|
Validation: 99/99 unit tests; the 10-trace suite is unchanged except
|
||||||
|
`xiaomi_mi_a1_coldboot` 473 -> 475 APDUs (two previously lost exchanges
|
||||||
|
recovered), 0 CHKSUM/CONCAT/BAD_FCS/GARBAGE, `RESULT: OK`; all other traces
|
||||||
|
identical; `compare_baseline.sh` reports IDENTICAL. VERSION -> 1.9.5.
|
||||||
|
|
||||||
|
### v1.9.4: decode_step/hunt unit coverage + hunt EOF guard
|
||||||
|
|
||||||
|
The v1.9.3 review's residual risk -- the new acceptance classes were only
|
||||||
|
proven by "no regression" on the example traces -- is closed with a
|
||||||
|
byte-replay harness, and the new coverage surfaced one latent defect:
|
||||||
|
|
||||||
|
- **`TestDecodeStep`** drives the real `decode_step()` DATA-state gate chain
|
||||||
|
with a scripted byte queue (`read_byte`/`peek_byte`/`_consume_first`
|
||||||
|
stubbed over `_replay` + the queue; the re-align/PPS/ATR/annotation sinks
|
||||||
|
stubbed; `reset()` provides the state). Pinned: `'CX'`/`'EX'` exchanges
|
||||||
|
frame and emit clean; `0x5F` (always eaten by the idle-noise gate before
|
||||||
|
v1.9.3) frames as a command; the `0x61` menu-text boundary byte is a
|
||||||
|
suspicious CLA with exactly one byte consumed; the invalid-INS one-byte
|
||||||
|
re-frame recovers a shifted SELECT into the intended packet; `0xFE` still
|
||||||
|
takes the idle-noise path.
|
||||||
|
- **`TestAtrHuntDetect`** drives `handle_atr()` the same way: `0xC4 0xA4`
|
||||||
|
and `0xC4 0x89` (the odd-INS acceptance) hand the header to DATA via
|
||||||
|
`_replay`; CLA `0x00` stays excluded; an invalid INS does not hand off.
|
||||||
|
- **Bug found and fixed:** the hunt's command-detect called
|
||||||
|
`plausible_ins(nxt)` with no None check -- a capture ending right after a
|
||||||
|
plausible CLA byte made `peek_byte()` return None and raised TypeError
|
||||||
|
(previously seen only by `decode()`'s crash protection).
|
||||||
|
`test_eof_after_plausible_cla_is_clean` pins the clean end-of-capture
|
||||||
|
return.
|
||||||
|
|
||||||
|
Validation: 95/95 unit tests; the new framing/hunt tests fail against
|
||||||
|
v1.9.2 for the right reasons (the acceptance is missing) and the EOF test
|
||||||
|
fails against v1.9.3 (the TypeError); full trace suite unchanged
|
||||||
|
(`compare_baseline.sh` IDENTICAL, baseline regenerated for the version +
|
||||||
|
test listing). VERSION -> 1.9.4.
|
||||||
|
|
||||||
|
### v1.9.3: higher logical channels in the CLA gate + defined odd INS codes
|
||||||
|
|
||||||
|
A trace with higher logical channels (4-19) or an odd-INS command could not
|
||||||
|
decode: the acceptance gates knew neither. Both are now spec-derived, with
|
||||||
|
the empirical precision of the mis-frame filter preserved.
|
||||||
|
|
||||||
|
- **Higher logical channels (4-19).** TS 102 221 §10.1.1 Table 10.4a codes
|
||||||
|
them `'01x0 xxxx'` (`0x40`-`0x4F` without SM, `0x60`-`0x6F` with SM in b6)
|
||||||
|
and `'11x0 xxxx'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`, the `'CX'`/`'EX'`
|
||||||
|
classes) -- ISO 7816-4 §5.1.1 Table 3 is the same with b5 = chaining.
|
||||||
|
`plausible_cla()` now accepts the `'CX'`/`'EX'` classes alongside the
|
||||||
|
previous `0x40`-`0x5F`, so extended-channel commands (e.g. STATUS on
|
||||||
|
channel 4 = `0xC0`) and higher channels in the no-SM `'4X'` form decode.
|
||||||
|
Rejected after testing: `0x60`-`0x6F`/`0x70`-`0x7F` (the SM-indicating
|
||||||
|
interindustry classes) and the `'8X'`/`'AX'` channel variants
|
||||||
|
`0x81`-`0x83`/`0x85`-`0x8F`/`0xA1`-`0xAF`. They are spec-coded, but
|
||||||
|
they cost
|
||||||
|
frame-start precision on the mis-framed traces: `0x60`-`0x6F` overlaps
|
||||||
|
printable ASCII, and the menu-text byte `0x61` ('a') started a bogus
|
||||||
|
frame on xiaomi_mi_a1_coldboot whose P3 swallowed the following exchanges
|
||||||
|
(a CONCAT regression); the FCP byte `0xA5` did the same for `'AX'`.
|
||||||
|
Previously such stray bytes were discarded as "suspicious CLA" and the
|
||||||
|
decoder resynced; the wider classes turned them into headers. Add the
|
||||||
|
classes back when a real trace needs them.
|
||||||
|
- **Odd INS.** The gates rejected every odd INS code, so TS 102 221
|
||||||
|
AUTHENTICATE `'89'` (chunked authentication data) and the ISO 7816-4
|
||||||
|
Table 4 alternate variants mis-framed. `plausible_ins()` now accepts
|
||||||
|
exactly the 13 defined odd codes (`ODD_INS`: `0F 21 87 89 A1 B1 B3 C3
|
||||||
|
CB D1 D7 DB DD`) and still rejects the rest: accepting every odd value
|
||||||
|
let the text run `49 4D` ("IM") start a bogus frame on
|
||||||
|
xiaomi_phone_sample. The header check calls the same `plausible_ins()`
|
||||||
|
now (the duplicate inline condition is gone) and the stale ISO `§12.1.1`
|
||||||
|
citation was corrected to `§5.1.2`.
|
||||||
|
- **Idle-noise gate.** The 6+ set-bits discard runs before the CLA check,
|
||||||
|
so it would swallow the legitimately bit-dense classes (`0xCF`, `0xE7`,
|
||||||
|
`0xEB`, `0xED`-`0xEF`) and kept `0x5F` dead (`plausible_cla()` accepted
|
||||||
|
it, the gate always discarded it). The gate is now
|
||||||
|
`is_idle_noise_byte()`: only bytes no spec allows are idle noise; valid
|
||||||
|
CLAs take the normal header path.
|
||||||
|
- **CONCAT detector unchanged by design.** `EMBEDDED_CLA` no longer
|
||||||
|
derives from `plausible_cla()`; it is pinned to the empirically tuned 36
|
||||||
|
values (`0x00`-`0x1F` + `0x80`/`0x84`/`0xA0`/`0xB0`) in both `pd.py` and
|
||||||
|
`tools/vs_reader.py`, with a membership-pin test and a `pd.py` ↔
|
||||||
|
`vs_reader.py` mirror test -- the CONCAT scan runs
|
||||||
|
across payload bytes where the extended classes false-hit (the Alcor
|
||||||
|
`"40 70 70 61 15"` run).
|
||||||
|
- **Tests.** CLA accept/reject vectors (including the two regressing bytes
|
||||||
|
`0x61`/`0xA5` and the reserved ranges), extended-channel
|
||||||
|
`validate_t0_apdu`/`is_desynced` vectors, the odd-INS vectors, the
|
||||||
|
idle-noise vectors, the EMBEDDED_CLA membership pin, the `ODD_INS` set
|
||||||
|
pin and the `pd.py` ↔ `vs_reader.py` mirror test (75 -> 84 tests);
|
||||||
|
`tools/vs_reader.py` mirrors the predicate, `ODD_INS` and the pinned set.
|
||||||
|
|
||||||
|
Validation: all 10 traces identical to the v1.9.2 baseline (APDU counts,
|
||||||
|
CHKSUM/CONCAT/BAD_FCS/suspicious-CLA counts, `RESULT: OK` everywhere);
|
||||||
|
`compare_baseline.sh` reports IDENTICAL. The xiaomi CONCAT regressions the
|
||||||
|
wider sets produced were found by this loop and narrowed away before the
|
||||||
|
baseline was regenerated (version + test listing only). VERSION -> 1.9.3.
|
||||||
|
|
||||||
### v1.9.2: PPS CLK upshift + exact CLK-edge projection (new reader support)
|
### v1.9.2: PPS CLK upshift + exact CLK-edge projection (new reader support)
|
||||||
|
|
||||||
Two timing bugs surfaced by `examples/alcor_reader_sample.sr`, a PC/SC reader
|
Two timing bugs surfaced by `examples/alcor_reader_sample.sr`, a PC/SC reader
|
||||||
@@ -503,8 +753,9 @@ the source**, not just gated. All 10 traces report 0 CONCAT:
|
|||||||
| S21p_coldboot_new_cable_sim2 | 375 | 0 |
|
| S21p_coldboot_new_cable_sim2 | 375 | 0 |
|
||||||
|
|
||||||
1. **One-byte re-frame on invalid INS.** When a 5-byte header window passes
|
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
|
`plausible_cla()` but the INS is unusable (`0x00`, `0x6x`/`0x9x`, or an
|
||||||
LSB — ISO 7816-4 §12.1.1), the window is almost always shifted by one
|
odd code outside the defined variants -- see the v1.9.3 note), the
|
||||||
|
window is almost always shifted by one
|
||||||
stray leading byte (e.g. a leftover SW2 read after a PACKETEND). The old
|
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
|
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
|
`resync_idle()` gave up on a busy burst the next byte became a bogus CLA
|
||||||
@@ -635,13 +886,11 @@ The 2-sample advance in `wait_data_falling` shifts `_start_fall` by 2
|
|||||||
samples, but at Samsung's 16 CLK/ETU (~72 samples) this is negligible
|
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.
|
(<3% of a bit period) and does not affect downstream byte reading.
|
||||||
|
|
||||||
**Design note: magic number.** `_min_pulse_samples = 2` is tuned for the
|
**Design note: pulse threshold.** `_min_pulse_samples` used to be a
|
||||||
primary 16 MHz sample rate (2 samples ≈ 125 ns, well within the FX2LP
|
hard-coded 2 samples, tuned for the 16 MHz corpus (≈125 ns, within the
|
||||||
pull-up rise time of 200–500 ns). At other sample rates the value would
|
FX2LP pull-up rise time of 200–500 ns). Since v1.9.7 it is `None` by
|
||||||
need adjustment — but since all current traces are 16 MHz, this is left
|
default and scales from `samplerate` (`_samples_for_us(0.125)` → 2 at
|
||||||
as a hardcoded constant rather than a decoder option. If 12 MHz or
|
16 MHz); a test or option may still override it (0 disables the check).
|
||||||
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
|
**Logging.** Each rejected start-bit glitch is logged with a running
|
||||||
count (`start-bit glitch rejected (#N)`) visible in the session log.
|
count (`start-bit glitch rejected (#N)`) visible in the session log.
|
||||||
@@ -665,40 +914,54 @@ window may be filtering real start bits.
|
|||||||
| ACK (== INS or == ~INS) | Full data match: card acknowledges and expects all P3 data bytes |
|
| 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 |
|
| INS^0x01 or ~INS^0x01 | Single-byte match: card validates syntax but processes data one byte at a time |
|
||||||
| 0x60 | NULL: card busy, wait for next procedure byte (never an SW1) |
|
| 0x60 | NULL: card busy, wait for next procedure byte (never an SW1) |
|
||||||
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
| SW1 (`0x61`–`0x6F` / `0x90`–`0x9F`) | Status word: exchange complete. ISO 7816-3 §10.3.3 Table 11 has no exception for `9X`: `9E`/`9F` are SW1 too (the v1.9.7 fix) |
|
||||||
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
|
||||||
|
|
||||||
##### CLA (Class) Byte — Interindustry Structure
|
##### CLA (Class) Byte — Coding and Logical Channels
|
||||||
|
|
||||||
```
|
Accepted (v1.9.3):
|
||||||
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
|
- `000x xxxx` (`0x00`-`0x1F`): first interindustry — SM in b4b3, chaining in
|
||||||
- Bit 6: `0` for interindustry; `1` for proprietary structural formats
|
b5, logical channel **0–3** in b2b1.
|
||||||
- Bits 4–3: Secure Messaging indicator (`00` = no SM)
|
- `01xx xxxx`: further interindustry — logical channel **4–19**
|
||||||
- Bits 2–1: Logical channel (0–3 for interindustry)
|
(b4..b1 = channel − 4). Accepted: `0x40`-`0x4F` plus the legacy
|
||||||
|
`0x50`-`0x5F` chaining variants (no CLA-level SM).
|
||||||
|
- UICC extended classes `'CX'`/`'EX'` (`0xC0`-`0xCF`/`0xE0`-`0xEF`):
|
||||||
|
channels 4–19 in b4..b1, SM in b6.
|
||||||
|
- Legacy `0x80`/`0x84`/`0xA0`/`0xB0`.
|
||||||
|
|
||||||
Valid interindustry CLA digits: `0x`, `1x`, `4x`, `5x`.
|
Rejected: `0x20`-`0x3F` (ISO-reserved); `0x60`-`0x6F`/`0x70`-`0x7F` (SM in
|
||||||
|
b6 — `0x60`-`0x6F` overlaps printable ASCII, and text bytes started bogus
|
||||||
|
frames on the mis-framed phone traces); the `'8X'`/`'AX'` standard-channel
|
||||||
|
variants `0x81`-`0x83`/`0x85`-`0x8F`/`0xA1`-`0xAF` (channels 0–3; same
|
||||||
|
trade-off);
|
||||||
|
`0x90`-`0x9F`, `0xD0`-`0xDF`, `0xF0`-`0xFF`.
|
||||||
|
|
||||||
Examples: `0x00` (standard), `0x0C` (secure messaging).
|
Examples: `0x00` (standard), `0x01` (channel 1), `0x44` (channel 8),
|
||||||
|
`0x4F` (channel 19), `0xC0` (STATUS on channel 4, `'CX'` class),
|
||||||
|
`0x0C` (secure messaging).
|
||||||
|
|
||||||
##### INS (Instruction) Byte — Valid Ranges
|
##### INS (Instruction) Byte — Valid Ranges
|
||||||
|
|
||||||
- Low nibble cannot be `X0` if high nibble is even
|
- `0x00` is reserved/invalid.
|
||||||
- Low nibble cannot be `XF` under any circumstances
|
- `0x6x`/`0x9x` collide with status words and are invalid.
|
||||||
- `0x6x` and `0x7x` ranges are invalid (reserved for SW1 / protocol bytes)
|
- Odd INS codes are valid only as the defined alternate variants
|
||||||
|
(`0F` ERASE BINARY, `21` VERIFY, `87` GENERAL AUTHENTICATE,
|
||||||
|
`A1` SEARCH BINARY, `B1` READ BINARY, `B3` READ RECORD, `C3` ENVELOPE,
|
||||||
|
`CB` GET DATA, `D1` WRITE BINARY, `D7` UPDATE BINARY, `DB` PUT DATA,
|
||||||
|
`DD` UPDATE RECORD; TS 102 221 AUTHENTICATE `89`).
|
||||||
|
|
||||||
Standard interindustry INS values: `A4` (SELECT), `B0` (READ BINARY),
|
Common INS values: `A4` (SELECT), `B0`/`B1` (READ BINARY),
|
||||||
`D6` (UPDATE BINARY), `B2` (READ RECORD), `E2` (APPEND RECORD),
|
`D6`/`D7` (UPDATE BINARY), `B2`/`B3` (READ RECORD), `E2` (APPEND RECORD),
|
||||||
`20` (VERIFY), `82` (EXTERNAL AUTHENTICATE), `88` (INTERNAL AUTHENTICATE).
|
`20`/`21` (VERIFY), `82` (EXTERNAL AUTHENTICATE), `88`/`89` (AUTHENTICATE),
|
||||||
|
`C2`/`C3` (ENVELOPE: CAT), `C0` (GET RESPONSE).
|
||||||
|
|
||||||
##### Quick Validation Checklist
|
##### Quick Validation Checklist
|
||||||
|
|
||||||
1. CLA high nibble is `0`, `1`, `4`, or `5` (for interindustry)
|
1. CLA is a valid class: `0x00`–`0x1F`/`0x40`–`0x5F` (interindustry, channels
|
||||||
2. INS low nibble is not `F`
|
0–3/4–19) or `0xC0`–`0xCF`/`0xE0`–`0xEF` (UICC extended) or
|
||||||
3. INS is outside `0x6x`–`0x7x` range
|
`0x80`/`0x84`/`0xA0`/`0xB0`
|
||||||
|
2. INS is not `0x00`
|
||||||
|
3. INS is outside the `0x6x`/`0x9x` status-word ranges
|
||||||
|
|
||||||
**Current smoke-test status:**
|
**Current smoke-test status:**
|
||||||
|
|
||||||
@@ -753,9 +1016,52 @@ append the measured CLK frequency when known
|
|||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.2'`.
|
The decoder version is defined in `pd.py` as `VERSION = '1.9.7'`.
|
||||||
The version is printed to the log on decoder startup.
|
The version is printed to the log on decoder startup.
|
||||||
|
|
||||||
|
### Release packaging (v1.9.7)
|
||||||
|
|
||||||
|
The GitHub release archives let users run the decoder without cloning the
|
||||||
|
repo. `./tools/build_release.sh [REF]` (REF defaults to `HEAD`) builds
|
||||||
|
`dist/sigrok_iso7816_stream-<VERSION>.tar.gz` (Linux) and
|
||||||
|
`dist/sigrok_iso7816_stream-<VERSION>.zip` (Windows) plus
|
||||||
|
`dist/SHA256SUMS`. `VERSION` comes from the ref's `pd.py`; a `v*` REF must
|
||||||
|
equal it.
|
||||||
|
|
||||||
|
The release file set is an explicit allowlist passed to `git archive` as its
|
||||||
|
pathspec, and also compared exactly against both archive listings (missing
|
||||||
|
*and* unexpected entries fail): the decoder (`__init__.py`, `pd.py`,
|
||||||
|
`gsmtap_stream.py`), the scripts (`setup.sh`/`start.sh`,
|
||||||
|
`setup.bat`/`start.bat`), `tools/vs_reader.py`, `README.md`, `README_RUS.md`,
|
||||||
|
`LICENSE`, every `resources/` photo referenced from both READMEs, and the two
|
||||||
|
language variants of the pipeline diagram (`resources/diagram_en.svg`,
|
||||||
|
`resources/diagram_ru.svg`, added explicitly because each README embeds its
|
||||||
|
own). Tests, `AGENTS.md`, the CI config, the development tools and the
|
||||||
|
unreferenced `resources/` images never enter the release. No `export-ignore`
|
||||||
|
rules are used, so GitHub's own source archives still contain the full tree.
|
||||||
|
`dist/` is gitignored.
|
||||||
|
|
||||||
|
Both archives are unpacked and verified: `pd.py` VERSION, `py_compile` of the
|
||||||
|
three modules, `bash -n` of the shell scripts, the tar exec bits, CRLF on the
|
||||||
|
`.bat` files and no CR in the `.sh` files. The archives are built in a temp
|
||||||
|
staging dir; only after every check passes are stale `dist/` archives removed
|
||||||
|
and the new tar.gz/zip/`SHA256SUMS` moved in (a failed build leaves `dist/`
|
||||||
|
untouched, and `gh release upload dist/*` can never attach a stale version).
|
||||||
|
|
||||||
|
`.github/workflows/release.yml` runs on a `v*` tag push: unit tests
|
||||||
|
(`tests/test_gsmtap.py`), tag == `pd.py` VERSION, the build script, then
|
||||||
|
`gh release upload --clobber` (or `gh release create --draft` when there is
|
||||||
|
no release yet) followed by `gh release edit --draft=false`, with
|
||||||
|
`release-<tag>` concurrency. The workflow prepends a standard install block
|
||||||
|
to the generated notes: release package (recommended; Linux tar.gz / Windows
|
||||||
|
zip, setup/start, SHA256SUMS) and source checkout (development). **Release
|
||||||
|
process:** commit the docs, workflow and scripts first, then tag `v1.9.7` and
|
||||||
|
push the tag (with user approval) - the workflow must exist in the tagged
|
||||||
|
commit, and the archive ships the committed READMEs. The changelog itself is
|
||||||
|
GitHub's auto-generated notes from the commits; `AGENTS.md` and `tests/` are
|
||||||
|
not in the archive. Packaging/docs
|
||||||
|
only - no decoder behavior change, and `VERSION` is unchanged.
|
||||||
|
|
||||||
### Testing after decoder changes
|
### Testing after decoder changes
|
||||||
|
|
||||||
After any change to `pd.py`:
|
After any change to `pd.py`:
|
||||||
@@ -934,23 +1240,35 @@ no `0xff`/`0xfc` garbage.
|
|||||||
|
|
||||||
Commands that carry command-data (e.g. `80 12 … d0`, P3=208) send those
|
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
|
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:
|
framing classifies the first post-header byte in this order (v1.9.5):
|
||||||
|
|
||||||
- If the first post-header byte **is the ACK (== INS)** → case 2/4: the
|
- **SW1** (`0x61-0x6F`/`0x90-0x9F`) → the card finished with SW1 SW2 at the
|
||||||
terminal sent no command data; read `P3` response bytes (+SW).
|
first procedure byte (ISO 7816-3 §10.3.3; a case-2 rejection such as
|
||||||
- Otherwise the byte is terminal command data → case 3/4: read `P3`
|
`6A82`/`6CXX` carries no ACK).
|
||||||
command-data bytes, then the card's procedure byte (ACK/SW/`9E`/`9F`).
|
- **ACK (== INS)** → case 2/4: the terminal sent no command data. Read the
|
||||||
On ACK (case 4) the response is read `P3` response bytes (+SW).
|
`P3` response bytes, then the SW. `P3=0` is Le=256 (§10.3.2): read the
|
||||||
|
response data until the SW. If the card returns **fewer** bytes than Le
|
||||||
|
(`6282` end of file), the buffer is split at the first plausible SW1
|
||||||
|
followed by a plausible CLA, the tail is replayed and the packet is
|
||||||
|
flagged BAD_FCS (v1.9.5; previously the SW scan discarded the real SW and
|
||||||
|
the following exchanges could be merged silently).
|
||||||
|
- **~INS** → byte-by-byte transfer: one data byte per ACK, read until SW.
|
||||||
|
- Otherwise → case 3/4: the byte is terminal command data; read `P3`
|
||||||
|
command-data bytes, then the card's procedure byte (ACK/SW; `9E`/`9F`
|
||||||
|
are SW1, not an extended length).
|
||||||
|
On ACK, append any inline response data until SW (ISO 7816-3 §12.2 maps
|
||||||
|
case 4 to a separate GET RESPONSE, so conforming cards send SW/61XY
|
||||||
|
here; the append is a permissive extension).
|
||||||
|
|
||||||
This consolidates case-3/4 exchanges (e.g. the `80 12`/`80 14` STK envelope
|
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
|
commands) into one C-APDU = header + command data, R-APDU = response + SW
|
||||||
APDU, instead of fragmenting them across frames. It is gated to
|
APDU, instead of fragmenting them across frames.
|
||||||
`_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
|
P3-bounded reads are kept for case 2 (`Lr == Le`): the read-until-SW
|
||||||
experiment from v1.1.0 which mis-identified FCP `0x62` as SW1).
|
experiment from v1.1.0 mis-identified FCP `0x62` as SW1 and was reverted.
|
||||||
|
Where the length is unknown (P3=0, case-4 inline data) the read-until-SW
|
||||||
|
ambiguity remains — a data byte that looks like SW1 is indistinguishable
|
||||||
|
on the wire.
|
||||||
|
|
||||||
## Decoder methods tried — reliability registry
|
## Decoder methods tried — reliability registry
|
||||||
|
|
||||||
@@ -984,42 +1302,53 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
|||||||
`clock_skip=369` before this fix). Logs "RST low at start: expecting cold-boot
|
`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` /
|
ATR". Decodes the full cold-boot ATR on `xiaomi_mi_a1_coldboot_sample.sr` /
|
||||||
`4gmodem_coldboot_sample.sr` (0 GARBAGE).
|
`4gmodem_coldboot_sample.sr` (0 GARBAGE).
|
||||||
- **1.5-ETU start-bit guard.** Rejects the phantom `0x00` an idle glitch
|
- **Half-ETU start-bit guard (edge reader).** Rejects the phantom `0x00`
|
||||||
inserts before an ATR. Reliable.
|
an idle glitch inserts before an ATR while still accepting the ~1-ETU
|
||||||
|
inter-byte gap on fast ETUs (v1.9.7 removed the unreachable 1.5-ETU
|
||||||
|
CLK-sync branch). Reliable.
|
||||||
- **VCC/RST 10 ms level-stability hysteresis.** Reports a level only after
|
- **VCC/RST 10 ms level-stability hysteresis.** Reports a level only after
|
||||||
≥10 ms stable. Reliable; symmetric power-up/down, kills spurious events
|
≥10 ms stable. Reliable; symmetric power-up/down, kills spurious events
|
||||||
from a ringing/floating sense line.
|
from a ringing/floating sense line.
|
||||||
- **PPS speed-switch follow (`clock_skip = F/D`).** After an accepted PPS the
|
- **PPS speed-switch follow (`clock_skip = F/D`).** After an accepted PPS the
|
||||||
native skip becomes F/D CLK cycles/bit. Reliable (verified live).
|
native skip becomes F/D CLK cycles/bit. Reliable (verified live).
|
||||||
- **T=1 block-chain follow-up bound.** Caps chained I-blocks so garbage
|
- **T=1 block decode (v1.9.7).** NAD/PCB/LEN/INF/LRC with the M-bit chain
|
||||||
cannot loop forever. Reliable.
|
followed inside `t1_parse_block()` (bounded by `T1_CHAIN_MAX`) and every
|
||||||
- **Synthetic T=0 ATR fallback `[0x3B, 0x00]`.** After 8 failed ATR hunts,
|
read EOF-guarded; R/S blocks carry no INF and are not emitted. Unit
|
||||||
commit a minimal ATR and set `hasT0=True`. Reliable for mid-session starts
|
tested only (no T=1 capture in the corpus). Reliable by construction.
|
||||||
(no ATR in the trace).
|
- **Hunt give-up (no synthetic ATR).** After `ATR_HUNT_FAIL_LIMIT` failed
|
||||||
|
hunts the decoder resumes DATA without fabricating an ATR (v1.6.0 removed
|
||||||
|
the old `[0x3B, 0x00]` fallback); the mid-session ETU estimator then
|
||||||
|
recovers the rate from DATA. Reliable.
|
||||||
- **ETU auto-detect from DATA via edge counting (`_measure_etu`).** Edge-waits
|
- **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
|
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
|
it **once**. Reliable for gated/stopped CLK (phone, ~53 samples). Locking
|
||||||
once (not re-measuring) is drift-immune and avoids glitch re-anchors.
|
once (not re-measuring) is drift-immune and avoids glitch re-anchors. A
|
||||||
|
burst must yield >=2 usable spacings, and a guard-dominated burst (all-zero
|
||||||
|
data: long LOW runs + short HIGH guard) is rejected (`_etu_candidate`,
|
||||||
|
v1.9.6); a rate that fails parity is invalidated and re-measured.
|
||||||
- **Edge-list bit reader (`_read_byte_edges`).** Per-byte edge reconstruction
|
- **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`).
|
from the DATA edge list (re-anchors the next byte via `_next_start_fall`).
|
||||||
Drift-immune. Reliable for mid-session/edge mode; gated by `_edge_read`.
|
Drift-immune. The T=0 framing reader; the CLK-sync reader is selected
|
||||||
|
when `_clock_skip_confident` is set (`_edge_read` removed in v1.9.7).
|
||||||
- **`decode_step` unified dispatch.** One state machine handles
|
- **`decode_step` unified dispatch.** One state machine handles
|
||||||
ATR/PPS/DATA + mid-session. Reliable; current architecture.
|
ATR/PPS/DATA + mid-session. Reliable; current architecture.
|
||||||
- **`hasT0`/`hasT1` guards + init.** Prevents `AttributeError` and skips byte
|
- **`hasT0`/`hasT1` guards + init.** Prevents `AttributeError` and skips byte
|
||||||
reading until a protocol is known. Reliable.
|
reading until a protocol is known. Reliable.
|
||||||
- **T=0 case-3/4 ACK==INS discriminator.** First post-header byte == INS ⇒
|
- **T=0 procedure-byte classification (v1.9.5).** The first post-header
|
||||||
case 2/4 (response bounded by `P3`); else command data (read `P3`, then
|
byte is SW1 ⇒ immediate status word; ACK (== INS) ⇒ case 2/4 (P3
|
||||||
the procedure byte). Reliable for consolidating STK envelope commands.
|
response bytes, short-read split + BAD_FCS); ~INS ⇒ one byte per ACK;
|
||||||
Gated to `_edge_read`. P3-bounded reads avoid FCP tag misidentification
|
else command data (case 3/4). Reliable for consolidating STK envelope
|
||||||
(v1.1.1 reverted read-until-SW from v1.1.0).
|
commands. P3-bounded reads avoid FCP tag misidentification (v1.1.1
|
||||||
- **1.5-ETU start-bit guard in `wait_data_falling`.** `min_high = 1.5 *
|
reverted read-until-SW from v1.1.0).
|
||||||
bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
|
- **Half-ETU start-bit guard in `wait_data_falling` (v1.9.7).** `min_high
|
||||||
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
|
= 0.5 * bit_samples` (with a rate-scaled floor) rejects intra-byte glitch
|
||||||
after `_measure_etu()` consumption (v1.1.1 fix for post-ATR desync).
|
falls while accepting the ~1-ETU inter-byte gap; the next byte's start is
|
||||||
- **Inverse-convention ATR detection in DATA state.** `firstByte == 0x3f`
|
queued by the edge reader (`_next_start_fall`), so no 1.5-ETU guard is
|
||||||
alongside `0x3b` at line 1656, so inverse-convention ATRs (TS=0x3F) are
|
needed on this path.
|
||||||
parsed by the default state. Sets `clock_skip=372` correctly
|
- **Inverse-convention support (TS=0x3F, v1.9.6).** The hunt already read
|
||||||
before PPS (v1.1.1 fix).
|
`0x3F`; the DATA state now accepts it too, the start bit stays a falling
|
||||||
|
edge from an idle-HIGH line (ISO 7816-3 7.1), and `get_bytes()` assembles
|
||||||
|
inverse data msb-first. Spec-verified; no inverse capture in the corpus.
|
||||||
- **`_resync = True` after PPS acceptance (native CLK path).** Forces
|
- **`_resync = True` after PPS acceptance (native CLK path).** Forces
|
||||||
`resync_idle()` before the first post-PPS command, preventing the decoder
|
`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).
|
from latching onto a mid-byte transition at the new speed (v1.1.1 fix).
|
||||||
@@ -1054,11 +1383,11 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
|||||||
`P3`) with P3-bounded reads for all paths (v1.1.1 reverted the
|
`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
|
read-until-SW experiment from v1.1.0 which mis-identified FCP `0x62` as
|
||||||
SW1).
|
SW1).
|
||||||
- **0.5-ETU start-bit guard in `wait_data_falling` (for `_edge_read`).**
|
- **0.5-ETU start-bit guard for the ATR-based CLK-sync loop.** It was
|
||||||
`min_high = 0.5 * bit_samples` was too small for slow ETUs — it accepted
|
rejected in v1.1.1 for that path (it accepted intra-byte HIGH periods on
|
||||||
intra-byte HIGH periods (data bit transitions) as start bits, causing byte
|
slow ETUs). The path was dead (`_edge_read` never False) and is removed
|
||||||
misalignment after `_measure_etu()` consumption. Replaced by 1.5-ETU guard
|
in v1.9.7; the surviving edge reader uses the 0.5 guard because its next
|
||||||
in v1.1.1.
|
start bit is queued by `_next_start_fall` (no re-hunt mid-burst).
|
||||||
|
|
||||||
### Open residual limitations
|
### Open residual limitations
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,266 @@
|
|||||||
# sigrok_iso7816_stream
|
# sigrok_iso7816_stream — a passive ISO 7816 sniffer
|
||||||
|
|
||||||
|
English | [Русский](README_RUS.md)
|
||||||
|
|
||||||
|
📢 Project news and updates: [website](https://atroshin.ru) · [Telegram channel](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
Fork of [svenso/sigrok_iso7816](https://github.com/svenso/sigrok_iso7816) with
|
Fork of [svenso/sigrok_iso7816](https://github.com/svenso/sigrok_iso7816) with
|
||||||
**live GSMTAP UDP streaming**, making a cheap FX2 logic analyzer (Saleae clone)
|
**live GSMTAP UDP streaming**, making a cheap FX2 logic analyzer (Saleae clone)
|
||||||
behave like a SIMtrace2 sniffer:
|
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 → [SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) / Wireshark
|
|
||||||
```
|
|
||||||
|
|
||||||
[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) is the
|
[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) is the
|
||||||
recommended tool for visualising live traces and viewing saved captures —
|
recommended tool for visualising live traces and viewing saved captures —
|
||||||
it can also import/export GSMTAP pcap files (including the PCAP written by
|
it can also import/export GSMTAP pcap files (including the PCAP written by
|
||||||
this decoder's `pcap_file=<path>` option).
|
this decoder's `pcap_file=<path>` option).
|
||||||
|
|
||||||
|
## Install and start
|
||||||
|
|
||||||
|
Both ways install the same decoder — pick one:
|
||||||
|
|
||||||
|
- **Download (recommended)** — a release archive. Nothing to clone and no git
|
||||||
|
history: extract it, run `setup`, then `start`. Best if you only want to
|
||||||
|
capture.
|
||||||
|
- **Source checkout** — a `git clone`. The same runtime plus the development
|
||||||
|
material: `tests/` and the trace/baseline tools (`tools/capture_baseline.sh`,
|
||||||
|
`tools/compare_baseline.sh`, `tools/vs_reader.py`). Example traces
|
||||||
|
(`examples/`) are gitignored and stay local in either case. Release archives
|
||||||
|
do not contain `tests/`, the dev tools or `examples/`.
|
||||||
|
|
||||||
|
### Download (recommended)
|
||||||
|
|
||||||
|
Grab the latest archive from
|
||||||
|
[Releases](https://github.com/anttro/sigrok_iso7816_stream/releases/latest):
|
||||||
|
|
||||||
|
- `sigrok_iso7816_stream-<version>.tar.gz` — Linux
|
||||||
|
- `sigrok_iso7816_stream-<version>.zip` — Windows
|
||||||
|
|
||||||
|
Each release also publishes `SHA256SUMS`, which lists **both** archives; keep
|
||||||
|
it next to the file you downloaded and verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c --ignore-missing SHA256SUMS # GNU coreutils (Linux)
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows run `certutil -hashfile sigrok_iso7816_stream-<version>.zip SHA256`
|
||||||
|
and compare the output with the `.zip` line in `SHA256SUMS`.
|
||||||
|
|
||||||
|
#### Linux (download)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzf sigrok_iso7816_stream-<version>.tar.gz
|
||||||
|
cd sigrok_iso7816_stream-<version>
|
||||||
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` verifies sigrok-cli and the FX2 firmware package
|
||||||
|
(`sigrok-firmware-fx2lafw`) are installed, symlinks the project directory into
|
||||||
|
`~/.local/share/libsigrokdecode/decoders/iso7816` (user-local decoders shadow
|
||||||
|
system-wide ones), and probes the connected device for its maximum supported
|
||||||
|
samplerate.
|
||||||
|
|
||||||
|
#### Windows (download)
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
1. **sigrok-cli** Windows installer (0.7.2 x86_64) from
|
||||||
|
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads) — the tested
|
||||||
|
release. Nightly Windows installers are also published on that page; they
|
||||||
|
may change the sigrok-cli syntax or the decoder API, so the 0.7.2 release
|
||||||
|
installer remains the supported version.
|
||||||
|
If sigrok-cli 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).
|
||||||
|
2. 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 extract the release `.zip` (right-click → Extract All in Explorer) and
|
||||||
|
run, inside the extracted folder:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
setup.bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.bat` verifies sigrok-cli and bundled firmware, registers this project's
|
||||||
|
decoder via a junction into `%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816`
|
||||||
|
(confirmed by `sigrok-cli -L` listing `iso7816`), and probes the device's
|
||||||
|
accepted samplerates. The release zip ships the batch files with CRLF line
|
||||||
|
endings, so no conversion is needed. `start.bat --help` lists its options
|
||||||
|
(the core subset of `start.sh`: channels, samplerate, clock, host/port, pcap,
|
||||||
|
protocol, debug).
|
||||||
|
|
||||||
|
### Source checkout
|
||||||
|
|
||||||
|
#### Linux (source checkout)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
|
cd sigrok_iso7816_stream
|
||||||
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` performs the same checks and symlink as above.
|
||||||
|
|
||||||
|
#### Windows (source checkout)
|
||||||
|
|
||||||
|
The Windows prerequisites (sigrok-cli + the Visual C++ 2010 Redistributable,
|
||||||
|
the bundled fx2lafw firmware, and the Zadig/WinUSB driver setup) are the same
|
||||||
|
as in [Windows (download)](#windows-download) above.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
|
cd sigrok_iso7816_stream
|
||||||
|
setup.bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.bat` performs the same checks and junction registration as above.
|
||||||
|
`start.bat` supports the core option subset; the space form is the primary
|
||||||
|
one, and `--clk=CH` also works (`start.bat --help` lists them):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat --clk D1 --data D6
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: batch files need CRLF line endings; the repo enforces this via
|
||||||
|
`.gitattributes`, so a fresh clone on Windows is already correct (the release
|
||||||
|
zip ships the same CRLF files).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`start.sh` captures continuously at 16 MHz until Ctrl+C (`rc=130`) and streams
|
||||||
|
GSMTAP to 127.0.0.1:4729. Defaults: pins D5=CLK, D0=I/O, D4=RST, D2=VCC.
|
||||||
|
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 # full decoder + sigrok output (all
|
||||||
|
# annotation rows + libsigrok -l 4);
|
||||||
|
# by default only decoded APDUs and
|
||||||
|
# decoder warnings/errors are shown
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows, `start.bat` takes the same core options (space form; the
|
||||||
|
`--opt=value` form also works) — see the Windows install notes above.
|
||||||
|
|
||||||
|
### Wiring
|
||||||
|
|
||||||
|
The recommended tap is the short cable with external pull resistors (see
|
||||||
|
*The tap in practice* below): 10 kΩ to GND on VCC (pulls low when off) and
|
||||||
|
47 kΩ to VCC on DATA (faster rise to high). Its pin mapping is **fixed and
|
||||||
|
known in advance** — see the "Pin mapping" table below — and is exactly what
|
||||||
|
`start.sh` defaults to (`D5`=CLK, `D0`=I/O, `D4`=RST, `D2`=VCC). No wiring
|
||||||
|
auto-detection is needed; the pins never move.
|
||||||
|
|
||||||
|
> **GND first.** Plug GND first and verify 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.
|
||||||
|
|
||||||
|
### Pin mapping
|
||||||
|
|
||||||
|
The FX2 clone channels are assigned via `-P iso7816:clk=N:data=N:rst=N:vcc=N`.
|
||||||
|
The recommended short-cable tap (external pull resistors) uses fixed pins that
|
||||||
|
are exactly `start.sh`'s defaults; `--clk`/`--data`/`--rst`/`--vcc` remap them
|
||||||
|
if you wire differently:
|
||||||
|
|
||||||
|
| 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.
|
||||||
|
|
||||||
|
### Clock modes
|
||||||
|
|
||||||
|
- `native` (default) — live captures. The decoder follows the PPS speed
|
||||||
|
switch and re-arms on RST, so warm resets and post-PPS traffic decode
|
||||||
|
correctly.
|
||||||
|
- `detect` — for offline experiments or captures without PPS. It does not
|
||||||
|
follow the PPS switch, so post-PPS traffic can be misread.
|
||||||
|
- `sample_as_clock` — experimental upstream mode; not recommended for SIM
|
||||||
|
captures.
|
||||||
|
|
||||||
|
### Troubleshooting live capture
|
||||||
|
|
||||||
|
**The capture stops early (especially on Windows).** A common cause is the
|
||||||
|
wrong USB driver. The Saleae Logic driver works only with Saleae Logic
|
||||||
|
software; sigrok needs the **WinUSB** driver. Install it with **Zadig**
|
||||||
|
(bundled with the sigrok-cli installer) and repeat the assignment after the
|
||||||
|
device re-enumerates with the firmware's VID/PID. See the sigrok documentation:
|
||||||
|
|
||||||
|
- https://sigrok.org/wiki/Windows#Drivers
|
||||||
|
- https://sigrok.org/wiki/Fx2lafw
|
||||||
|
|
||||||
|
**Device not found / no signal.** Check the WinUSB driver (Windows), the
|
||||||
|
fx2lafw firmware, the wiring and GND (see Wiring), and try another USB port or
|
||||||
|
a known-good cable. Cheap USB cables and hubs are common causes of stalls.
|
||||||
|
|
||||||
|
**The capture ends by itself (`rc=0`).** The FX2 is at its USB bandwidth limit
|
||||||
|
at high samplerates. Lower `--samplerate`, avoid hubs, and close other USB
|
||||||
|
traffic. `start.sh` automatically restarts the capture on `rc=0` (default 20
|
||||||
|
restarts, 2 s apart; `--no-loop`, `--max-restarts=N`, `--restart-delay=S`).
|
||||||
|
With `--pcap=FILE`, each restart writes `FILE.<N>.<ext>`.
|
||||||
|
|
||||||
|
`start.sh` runs until Ctrl+C; keystrokes do not stop it (the "press any key"
|
||||||
|
prompt is disabled). Useful options:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh --pcap=s.pcap --log=s.log # record events and the rc= verdict
|
||||||
|
./start.sh ... --samplerate=24M # more timing headroom if needed
|
||||||
|
./start.sh ... --klog # also record the kernel journal
|
||||||
|
./start.sh ... --debug # full decoder + sigrok output
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--log` records the session-end `rc=` line.
|
||||||
|
- The decoder logs `SIGNAL INTEGRITY WARNING` when ≥ 8 of the last 64 frames
|
||||||
|
fail parity.
|
||||||
|
- Desynced packets are kept and flagged `GSMTAP_FLAG_BAD_FCS` in the GSMTAP
|
||||||
|
header `res` byte — visible in Wireshark as a BAD-FCS warning.
|
||||||
|
|
||||||
|
### Raw sigrok-cli
|
||||||
|
|
||||||
|
Equivalent commands without the wrapper scripts:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
## The tap in practice
|
## The tap in practice
|
||||||
|
|
||||||

|

|
||||||
@@ -45,17 +290,23 @@ this decoder's `pcap_file=<path>` option).
|
|||||||

|

|
||||||
*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)*
|
*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
|
## Features
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| Live GSMTAP stream | Every decoded event is sent as a GSMTAP-SIM UDP packet (default on, mimics simtrace2-sniff) |
|
| 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) |
|
| Correct sub_types | ATR=0x01 and PPS=0x02 are tagged properly in PCAP output |
|
||||||
| ATR/PPS in PCAP | ATR and combined PPS request+response are emitted as PCAP packets |
|
| 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) |
|
| 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) |
|
| 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 |
|
| Direct PCAP file | `pcap_file=<path>` writes PCAP without shell redirection |
|
||||||
| Start-bit deglitch | 2-sample deglitch in the start-bit detector eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
| Start-bit deglitch | Suppresses sub-ETU LOW glitches (2 samples at 16 MHz, scaled to the samplerate) |
|
||||||
|
| Mid-session ETU lock | Recovers the ETU from DATA for captures without a usable CLK; a rate that fails parity is re-measured |
|
||||||
|
| ATR / resync guards | Validates the ATR, ignores mis-framed ATR candidates, and re-aligns after a desync |
|
||||||
|
| Inverse convention | Supports direct (TS=0x3B) and inverse (TS=0x3F) cards |
|
||||||
|
| T=1 blocks | Decodes NAD/PCB/LEN/INF/LRC with M-bit chaining; the I-block INF is emitted as the APDU |
|
||||||
|
| Parity flag | Packets containing a parity-error byte are flagged `GSMTAP_FLAG_BAD_FCS` |
|
||||||
|
| Rate-aware thresholds | Deglitch, guard and ETU floors scale with the capture samplerate |
|
||||||
|
|
||||||
## Decoder options
|
## Decoder options
|
||||||
|
|
||||||
@@ -109,260 +360,6 @@ This carriage is this decoder's own extension (upstream simtrace2-sniff
|
|||||||
never sets `res`); the flag semantics mirror simtrace2's per-message USB
|
never sets `res`); the flag semantics mirror simtrace2's per-message USB
|
||||||
data flags and are not part of the libosmocore spec.
|
data flags and are 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 # full decoder + sigrok output (all
|
|
||||||
# annotation rows + libsigrok -l 4);
|
|
||||||
# by default only decoded APDUs and
|
|
||||||
# decoder warnings/errors are shown
|
|
||||||
```
|
|
||||||
|
|
||||||
### Wiring
|
|
||||||
|
|
||||||
The recommended tap is the short cable with external pull resistors (see
|
|
||||||
the photos above): 10 kΩ to GND on VCC (pulls low when off) and 47 kΩ to
|
|
||||||
VCC on DATA (faster rise to high). Its pin mapping is **fixed and known
|
|
||||||
in advance** — see the "Pin mapping" table below — and is exactly what
|
|
||||||
`start.sh` defaults to (`D5`=CLK, `D0`=I/O, `D4`=RST, `D2`=VCC). No
|
|
||||||
wiring auto-detection is needed; the pins never move.
|
|
||||||
|
|
||||||
> **GND first.** Plug GND first and verify 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 # full output: all annotation rows
|
|
||||||
# + libsigrok internals at -l 4
|
|
||||||
# (default: APDUs + warnings only)
|
|
||||||
```
|
|
||||||
|
|
||||||
`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"), the capture auto-restarts so the session is not lost —
|
|
||||||
it re-launches 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). When restarting with `--pcap=FILE`, each session's capture
|
|
||||||
is written to `FILE.<N>.<ext>` so a restart never overwrites the previous one,
|
|
||||||
and the repetitive `sr_session_stop: session was NULL` teardown line is
|
|
||||||
filtered from stderr.
|
|
||||||
|
|
||||||
- `--no-loop` disables auto-restart (single capture only).
|
|
||||||
- `--loop` forces auto-restart on (this is the default).
|
|
||||||
- `--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`.
|
|
||||||
The recommended short-cable tap (external pull resistors) uses fixed pins that
|
|
||||||
are exactly `start.sh`'s defaults; `--clk`/`--data`/`--rst`/`--vcc` remap them
|
|
||||||
if you wire differently:
|
|
||||||
|
|
||||||
| 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. **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).
|
|
||||||
2. 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`), and probes the device's
|
|
||||||
accepted samplerates.
|
|
||||||
|
|
||||||
Capture works the same as on Linux:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
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
|
## RST/VCC detection notes and limitations
|
||||||
|
|
||||||
- Events are detected **asynchronously before the first ATR** (power-on /
|
- Events are detected **asynchronously before the first ATR** (power-on /
|
||||||
@@ -373,13 +370,9 @@ Note: batch files need CRLF line endings; the repo enforces this via
|
|||||||
- VCC is digital-only: the decoder sees powered/not-powered, never the
|
- VCC is digital-only: the decoder sees powered/not-powered, never the
|
||||||
voltage class (1.8/3/5V).
|
voltage class (1.8/3/5V).
|
||||||
- **RST and VCC use level-stability hysteresis at 10 ms** (samplerate-scaled):
|
- **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
|
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
|
This makes power-up and power-down symmetric and suppresses ringing and
|
||||||
that long. This makes power-up and power-down *symmetric*: a VCC sense line
|
short glitches.
|
||||||
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
|
- **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),
|
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
|
so the decoder sees "powered" constantly and emits spurious power events on
|
||||||
@@ -390,23 +383,14 @@ Note: batch files need CRLF line endings; the repo enforces this via
|
|||||||
- Unassigned optional channels are ignored even if `rst_detect`/`vcc_detect`
|
- Unassigned optional channels are ignored even if `rst_detect`/`vcc_detect`
|
||||||
is enabled (guarded via `has_channel()`).
|
is enabled (guarded via `has_channel()`).
|
||||||
|
|
||||||
## Testing
|
## Development and testing
|
||||||
|
|
||||||
Verified against the upstream example captures:
|
The decoder is validated against example captures with `tools/vs_reader.py`.
|
||||||
|
Trace fixtures are kept locally and are not part of the repository.
|
||||||
```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 the SIMtrace Analyser 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`)
|
### Validation against the reader SDK (`tools/vs_reader.py`)
|
||||||
|
|
||||||
On a controlled rig (PCSC reader driven by software) the reference APDU log
|
On a controlled rig (PC/SC reader driven by software) the reference APDU log
|
||||||
from the reader SDK can be diffed against the decoded PCAP:
|
from the reader SDK can be diffed against the decoded PCAP:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -419,32 +403,12 @@ python3 tools/vs_reader.py <reader-log>.log out.pcap
|
|||||||
`vs_reader.py` aligns each reader-log exchange to the capture APDUs
|
`vs_reader.py` aligns each reader-log exchange to the capture APDUs
|
||||||
(byte-exact LCS match) and classifies the leftovers as proactive/CAT
|
(byte-exact LCS match) and classifies the leftovers as proactive/CAT
|
||||||
(unseen by the reader SDK), pre-capture/dropped, or garbage.
|
(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)
|
### Cold-boot captures (capture starts before power-up)
|
||||||
|
|
||||||
When the capture starts pre-power-up, the decoder must arm the ATR
|
When the capture starts before power-up, RST is still low at sample 0. Use
|
||||||
hunt because RST is still low at sample 0: it primes the initial
|
`rst_detect=true` and `protocol=T=0`; the decoder arms the ATR hunt on the
|
||||||
VCC/RST levels on the first step (instead of defaulting them to deasserted),
|
initial RST level and decodes the full ATR. Example:
|
||||||
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
|
```bash
|
||||||
sigrok-cli -i <coldboot>.sr \
|
sigrok-cli -i <coldboot>.sr \
|
||||||
@@ -452,48 +416,16 @@ sigrok-cli -i <coldboot>.sr \
|
|||||||
-A iso7816
|
-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)
|
### 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
|
- **Reader / constant-CLK capture** — the reader drives a steady CLK for the
|
||||||
whole session. In mid-session mode the capture
|
whole session. Validate it against the reader SDK with
|
||||||
decodes to **0 garbage** (`RESULT: OK`) against the reader-SDK
|
`tools/vs_reader.py <reader-log>.log out.pcap`.
|
||||||
ground truth (`tools/vs_reader.py <reader-log>.log out.pcap`).
|
- **Phone / gated or stopped-CLK capture** — the phone gates or stops CLK
|
||||||
Warm-reset reader captures use the synthetic-T=0-ATR fallback.
|
between transfers (Clock Stop Mode), so the ETU cannot be derived from CLK.
|
||||||
|
Run with `protocol=T=0`; the decoder recovers the ETU from the DATA line and
|
||||||
- **Phone / stopped-CLK capture** — the phone gates or stops CLK between
|
decodes with the edge-list reader. T=0 case 3/4 commands (e.g. `80 12`/`80 14`
|
||||||
transfers (Clock Stop Mode), so the ETU is not the 372-clock default and
|
STK envelopes) are consolidated into one command→response APDU.
|
||||||
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 |
|
|
||||||
| alcor_reader | constant | ~53 | 16 (F=512/D=32) | Direct | PC/SC reader, CLK 4.0 MHz at ATR -> 4.8 MHz after PPS |
|
|
||||||
|
|
||||||
The decoder auto-detects the ETU from the trace and selects the appropriate
|
|
||||||
bit reader (CLK-sync for constant CLK, edge-list for gated/stopped CLK).
|
|
||||||
No manual configuration is needed beyond `clock_option=native` and
|
|
||||||
`protocol=T=0` for phone captures.
|
|
||||||
|
|
||||||
Recommended invocations:
|
Recommended invocations:
|
||||||
|
|
||||||
@@ -510,6 +442,40 @@ sigrok-cli -i <phone>.sr \
|
|||||||
-A iso7816
|
-A iso7816
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Data rates and ETUs
|
||||||
|
|
||||||
|
Reference values observed on the example captures at a 16 MHz samplerate:
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| alcor_reader | constant | ~53 | 16 (F=512/D=32) | Direct | PC/SC reader, CLK 4.0 MHz at ATR -> 4.8 MHz after PPS |
|
||||||
|
|
||||||
|
The decoder auto-detects the ETU and selects the 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Related tools
|
||||||
|
|
||||||
|
SIMple, SIMtrace Analyser and sigrok_iso7816_stream are three standalone
|
||||||
|
tools for working with SIM/UICC cards, published together on the same
|
||||||
|
[website](https://atroshin.ru) and [news channel](https://t.me/youeyeseesee).
|
||||||
|
Each one is fully independent — install only the tools you need:
|
||||||
|
|
||||||
|
- **[SIMple](https://github.com/anttro/simple)** — a UICC workbench: APDU
|
||||||
|
construction, SCP80/SCP81 OTA, SIM Toolkit simulation and card profiling.
|
||||||
|
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** — an
|
||||||
|
APDU trace analyser for GSMTAP-SIM captures.
|
||||||
|
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
||||||
|
a passive ISO 7816 sniffer built on a cheap FX2 logic analyser.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-2.0+, same as upstream.
|
GPL-2.0+, same as upstream.
|
||||||
+325
-365
@@ -1,17 +1,14 @@
|
|||||||
> English version: [README.md](README.md)
|
# sigrok_iso7816_stream — пассивный ISO 7816-сниффер
|
||||||
|
|
||||||
# sigrok_iso7816_stream
|
[English](README.md) | Русский
|
||||||
|
|
||||||
|
📢 Новости и обновления проекта: [сайт](https://atroshin.ru) · [Telegram-канал](https://t.me/youeyeseesee)
|
||||||
|
|
||||||
Форк [svenso/sigrok_iso7816](https://github.com/svenso/sigrok_iso7816) с
|
Форк [svenso/sigrok_iso7816](https://github.com/svenso/sigrok_iso7816) с
|
||||||
**живым GSMTAP-UDP-стримингом**: дешёвый логический анализатор FX2 (клон
|
**живым GSMTAP-UDP-стримингом**: дешёвый логический анализатор FX2 (клон
|
||||||
Saleae) ведёт себя как сниффер SIMtrace2:
|
Saleae) ведёт себя как сниффер SIMtrace2:
|
||||||
|
|
||||||
```
|

|
||||||
SIM-карта ⇄ ридер (пассивный съём с контактов)
|
|
||||||
→ логический анализатор FX2 (fx2lafw, 8 каналов, до 24 МГц)
|
|
||||||
→ sigrok-cli + декодер iso7816
|
|
||||||
→ GSMTAP UDP :4729 → [SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) / Wireshark
|
|
||||||
```
|
|
||||||
|
|
||||||
[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) —
|
[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser) —
|
||||||
рекомендуемый инструмент для визуализации живых трасс и просмотра
|
рекомендуемый инструмент для визуализации живых трасс и просмотра
|
||||||
@@ -19,6 +16,262 @@ SIM-карта ⇄ ридер (пассивный съём с контактов
|
|||||||
GSMTAP pcap-файлы (включая PCAP, который этот декодер пишет через опцию
|
GSMTAP pcap-файлы (включая PCAP, который этот декодер пишет через опцию
|
||||||
`pcap_file=<path>`).
|
`pcap_file=<path>`).
|
||||||
|
|
||||||
|
## Установка и запуск
|
||||||
|
|
||||||
|
Оба способа ставят один и тот же декодер — выберите один:
|
||||||
|
|
||||||
|
- **Скачать (рекомендуется)** — архив релиза. Не нужно ничего клонировать и
|
||||||
|
нет истории git: распакуйте, запустите `setup`, затем `start`. Лучший
|
||||||
|
вариант, если нужен только съём.
|
||||||
|
- **Из исходников** — `git clone`. Тот же рантайм плюс материал для
|
||||||
|
разработки: `tests/` и инструменты трасс и базовой линии
|
||||||
|
(`tools/capture_baseline.sh`, `tools/compare_baseline.sh`,
|
||||||
|
`tools/vs_reader.py`). Трассы примеров (`examples/`) исключены из git и
|
||||||
|
остаются локальными. В архивах релиза нет `tests/`, dev-инструментов и
|
||||||
|
`examples/`.
|
||||||
|
|
||||||
|
### Скачать (рекомендуется)
|
||||||
|
|
||||||
|
Скачайте последний архив со страницы
|
||||||
|
[Releases](https://github.com/anttro/sigrok_iso7816_stream/releases/latest):
|
||||||
|
|
||||||
|
- `sigrok_iso7816_stream-<version>.tar.gz` — Linux
|
||||||
|
- `sigrok_iso7816_stream-<version>.zip` — Windows
|
||||||
|
|
||||||
|
К каждому релизу прилагается `SHA256SUMS` — в нём **оба** архива.
|
||||||
|
Положите файл рядом со скачанным архивом и проверьте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c --ignore-missing SHA256SUMS # GNU coreutils (Linux)
|
||||||
|
```
|
||||||
|
|
||||||
|
В Windows выполните `certutil -hashfile sigrok_iso7816_stream-<version>.zip SHA256`
|
||||||
|
и сравните вывод со строкой `.zip` в `SHA256SUMS`.
|
||||||
|
|
||||||
|
#### Linux (скачать)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzf sigrok_iso7816_stream-<version>.tar.gz
|
||||||
|
cd sigrok_iso7816_stream-<version>
|
||||||
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` проверяет, что установлены sigrok-cli и пакет прошивки FX2
|
||||||
|
(`sigrok-firmware-fx2lafw`), создаёт симлинк каталога проекта в
|
||||||
|
`~/.local/share/libsigrokdecode/decoders/iso7816` (пользовательские
|
||||||
|
декодеры перекрывают системные) и опрашивает подключённое устройство на
|
||||||
|
предмет максимальной поддерживаемой частоты дискретизации.
|
||||||
|
|
||||||
|
#### Windows (скачать)
|
||||||
|
|
||||||
|
Предварительные требования:
|
||||||
|
|
||||||
|
1. **sigrok-cli** — установщик для Windows (0.7.2 x86_64) с
|
||||||
|
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads) —
|
||||||
|
протестированная версия. На этой же странице публикуются ночные сборки
|
||||||
|
для Windows; они могут менять синтаксис sigrok-cli или API декодеров,
|
||||||
|
поэтому поддерживаемой остаётся версия из релизного установщика 0.7.2.
|
||||||
|
Если sigrok-cli не запускается с ошибкой `0xc0150002`, установите
|
||||||
|
[Visual C++ 2010 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=26999)
|
||||||
|
(`msvcr100.dll` требуется встроенному Python-рантайму).
|
||||||
|
2. Прошивка fx2lafw поставляется вместе с установщиком — отдельно скачивать не нужно.
|
||||||
|
|
||||||
|
Настройка драйвера (донглам FX2 нужен WinUSB, вендорский драйвер не подойдёт):
|
||||||
|
|
||||||
|
- Запустите **Zadig**, который лежит *внутри* каталога установки sigrok-cli
|
||||||
|
(также доступен из меню «Пуск»). Выберите устройство FX2 в выпадающем
|
||||||
|
списке и установите драйвер **WinUSB**.
|
||||||
|
- После загрузки прошивки устройство переподключается с другим VID/PID —
|
||||||
|
если оно всё ещё не находится, запустите Zadig **второй раз** для
|
||||||
|
переподключившегося устройства.
|
||||||
|
- Windows привязывает драйверы к USB-порту, когда нет серийного номера:
|
||||||
|
перенос донгла в другой порт может потребовать повторить Zadig.
|
||||||
|
|
||||||
|
Затем распакуйте архив `.zip` (в Проводнике: правой кнопкой → «Извлечь все»)
|
||||||
|
и запустите в распакованном каталоге:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
setup.bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.bat` проверяет sigrok-cli и встроенную прошивку, регистрирует декодер
|
||||||
|
этого проекта через junction в
|
||||||
|
`%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816` (подтверждается тем, что
|
||||||
|
`sigrok-cli -L` показывает `iso7816`), и опрашивает устройство на предмет
|
||||||
|
поддерживаемых частот дискретизации. В zip-архиве релиза bat-файлы уже с
|
||||||
|
переводами строк CRLF, поэтому преобразование не требуется.
|
||||||
|
`start.bat --help` перечисляет его опции (основное подмножество `start.sh`:
|
||||||
|
каналы, частота дискретизации, clock, host/port, pcap, protocol, debug).
|
||||||
|
|
||||||
|
### Из исходников
|
||||||
|
|
||||||
|
#### Linux (из исходников)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
|
cd sigrok_iso7816_stream
|
||||||
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.sh` выполняет те же проверки и создаёт тот же симлинк, что и выше.
|
||||||
|
|
||||||
|
#### Windows (из исходников)
|
||||||
|
|
||||||
|
Предварительные требования Windows (sigrok-cli и Visual C++ 2010
|
||||||
|
Redistributable, встроенная прошивка fx2lafw и настройка драйвера
|
||||||
|
Zadig/WinUSB) — те же, что в разделе
|
||||||
|
[Windows (скачать)](#windows-скачать) выше.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
|
cd sigrok_iso7816_stream
|
||||||
|
setup.bat
|
||||||
|
start.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`setup.bat` выполняет те же проверки и регистрацию junction, что и выше.
|
||||||
|
`start.bat` поддерживает основное подмножество опций; основной формат — с
|
||||||
|
пробелом, `--clk=CH` тоже работает (`start.bat --help` перечисляет опции):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat --clk D1 --data D6
|
||||||
|
```
|
||||||
|
|
||||||
|
Примечание: bat-файлы требуют переводов строк CRLF; репозиторий следит за
|
||||||
|
этим через `.gitattributes`, поэтому свежий клон в Windows уже корректен
|
||||||
|
(zip-архив релиза содержит те же файлы с CRLF).
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
`start.sh` пишет непрерывный поток на частоте 16 МГц до Ctrl+C (`rc=130`) и
|
||||||
|
стримит GSMTAP на 127.0.0.1:4729. Умолчания: выводы D5=CLK, D0=I/O,
|
||||||
|
D4=RST, D2=VCC. Переопределяются аргументами:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh --clk=D1 --data=D6 # другое подключение
|
||||||
|
./start.sh --no-rst --no-vcc # только CLK+I/O
|
||||||
|
./start.sh --host=10.0.28.7 # стримить на удалённый анализатор
|
||||||
|
./start.sh --pcap=session.pcap # также записать декодированные события в PCAP
|
||||||
|
./start.sh --debug # полный вывод декодера и sigrok (все
|
||||||
|
# строки аннотаций + libsigrok -l 4);
|
||||||
|
# по умолчанию показываются только
|
||||||
|
# декодированные APDU и предупреждения/ошибки
|
||||||
|
```
|
||||||
|
|
||||||
|
В Windows `start.bat` принимает то же основное подмножество опций (форма с
|
||||||
|
пробелом; форма `--opt=value` тоже работает) — см. заметки по установке
|
||||||
|
Windows выше.
|
||||||
|
|
||||||
|
### Подключение
|
||||||
|
|
||||||
|
Рекомендуемый способ съёма — короткий кабель с внешними подтягивающими
|
||||||
|
резисторами (см. раздел «Съём на практике» ниже): 10 кОм к GND на VCC
|
||||||
|
(тянет вниз без питания) и 47 кОм к VCC на DATA (быстрее фронт вверх).
|
||||||
|
Его распиновка **фиксирована и известна заранее** — см. таблицу
|
||||||
|
«Распиновка» ниже — и в точности совпадает с умолчаниями `start.sh`
|
||||||
|
(`D5`=CLK, `D0`=I/O, `D4`=RST, `D2`=VCC). Автодетект подключения не нужен;
|
||||||
|
выводы не меняются.
|
||||||
|
|
||||||
|
> **Сначала GND.** Сначала подключите GND и проверьте его вручную
|
||||||
|
> (мультиметром или заведомо исправным контактом) — отсутствующий или
|
||||||
|
> плохой контакт земли маскируется под все остальные неисправности сразу:
|
||||||
|
> плавающие линии, фантомные стартовые биты, кадры из одних нулей.
|
||||||
|
|
||||||
|
### Распиновка
|
||||||
|
|
||||||
|
Каналы клона FX2 назначаются через `-P iso7816:clk=N:data=N:rst=N:vcc=N`.
|
||||||
|
Рекомендуемый съём коротким кабелем (с внешними подтягивающими резисторами)
|
||||||
|
использует фиксированные выводы, которые в точности совпадают с умолчаниями
|
||||||
|
`start.sh`; `--clk`/`--data`/`--rst`/`--vcc` переназначают их, если вы
|
||||||
|
подключили иначе:
|
||||||
|
|
||||||
|
| Канал FX2 | Контакт SIM | Сигнал |
|
||||||
|
|-------------|-------------|--------|
|
||||||
|
| D0 | C7 | I/O |
|
||||||
|
| D2 | C1 | VCC (только цифровой уровень!) |
|
||||||
|
| D4 | C2 | RST |
|
||||||
|
| D5 | C3 | CLK |
|
||||||
|
| GND | C5 | GND |
|
||||||
|
|
||||||
|
Входной порог FX2 близок к TTL (~1.4 В): карты класса 1.8 В могут читаться
|
||||||
|
нестабильно; классы 3 В/5 В — нормально.
|
||||||
|
|
||||||
|
### Режимы CLK
|
||||||
|
|
||||||
|
- `native` (по умолчанию) — живые записи. Декодер отслеживает переключение
|
||||||
|
скорости по PPS и заново взводится по RST, поэтому тёплые сбросы и трафик
|
||||||
|
после PPS разбираются корректно.
|
||||||
|
- `detect` — для офлайн-экспериментов или записей без PPS. Он не отслеживает
|
||||||
|
переключение по PPS, поэтому трафик после PPS может читаться неверно.
|
||||||
|
- `sample_as_clock` — экспериментальный режим upstream; для съёма SIM не
|
||||||
|
рекомендуется.
|
||||||
|
|
||||||
|
### Решение проблем при живой записи
|
||||||
|
|
||||||
|
**Запись обрывается рано (особенно в Windows).** Частая причина —
|
||||||
|
неподходящий драйвер USB. Драйвер Saleae Logic работает только с Saleae
|
||||||
|
Logic software; sigrok нужен драйвер **WinUSB**. Установите его через
|
||||||
|
**Zadig** (входит в установщик sigrok-cli) и повторите назначение после
|
||||||
|
переподключения устройства с VID/PID прошивки. Подробности в документации
|
||||||
|
sigrok:
|
||||||
|
|
||||||
|
- https://sigrok.org/wiki/Windows#Drivers
|
||||||
|
- https://sigrok.org/wiki/Fx2lafw
|
||||||
|
|
||||||
|
**Устройство не найдено / нет сигнала.** Проверьте драйвер WinUSB (Windows),
|
||||||
|
прошивку fx2lafw, подключение и GND (см. «Подключение»), попробуйте другой
|
||||||
|
USB-порт и заведомо исправный кабель. Дешёвые USB-кабели и хабы — частая
|
||||||
|
причина остановок.
|
||||||
|
|
||||||
|
**Запись завершается сама (`rc=0`).** FX2 упирается в потолок пропускной
|
||||||
|
способности USB на высокой частоте дискретизации. Снизьте `--samplerate`,
|
||||||
|
избегайте хабов, закройте другой USB-трафик. `start.sh` автоматически
|
||||||
|
перезапускает запись при `rc=0` (по умолчанию 20 перезапусков с паузой 2 с;
|
||||||
|
`--no-loop`, `--max-restarts=N`, `--restart-delay=S`). При `--pcap=FILE`
|
||||||
|
каждый перезапуск пишет `FILE.<N>.<ext>`.
|
||||||
|
|
||||||
|
`start.sh` работает до Ctrl+C; нажатия клавиш его не останавливают
|
||||||
|
(приглашение «нажмите любую клавишу» отключено). Полезные опции:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh --pcap=s.pcap --log=s.log # записать события и вердикт rc=
|
||||||
|
./start.sh ... --samplerate=24M # больше запаса по таймингу при необходимости
|
||||||
|
./start.sh ... --klog # также записать журнал ядра
|
||||||
|
./start.sh ... --debug # полный вывод декодера и sigrok
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--log` сохраняет строку вердикта `rc=` в конце сессии.
|
||||||
|
- Декодер пишет `SIGNAL INTEGRITY WARNING`, когда ≥ 8 из последних 64 кадров
|
||||||
|
не проходят проверку чётности.
|
||||||
|
- Десинхронизированные пакеты сохраняются с флагом `GSMTAP_FLAG_BAD_FCS` в
|
||||||
|
байте `res` заголовка GSMTAP — видно в Wireshark как предупреждение BAD-FCS.
|
||||||
|
|
||||||
|
### Сырые команды sigrok-cli
|
||||||
|
|
||||||
|
Эквивалентные команды без скриптов-обёрток:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Минимум: только CLK+I/O, живой поток на localhost:4729
|
||||||
|
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
|
||||||
|
-P iso7816:clk=D0:data=D1:clock_option=detect -A iso7816
|
||||||
|
|
||||||
|
# Полное подключение: CLK + I/O + RST + VCC с отслеживанием событий линий
|
||||||
|
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
|
||||||
|
|
||||||
|
# Поток на удалённый анализатор / отключить стриминг / писать PCAP-файл
|
||||||
|
... -P iso7816:...:gsmtap_host=10.0.28.7 ...
|
||||||
|
... -P iso7816:...:gsmtap_enable=false ...
|
||||||
|
... -P iso7816:...:pcap_file=trace.pcap ...
|
||||||
|
|
||||||
|
# Классический режим upstream: бинарный PCAP в stdout
|
||||||
|
sigrok-cli -i capture.sr -P iso7816:clk=2:data=0 -B iso7816=pcap > out.pcap
|
||||||
|
```
|
||||||
|
|
||||||
## Съём на практике
|
## Съём на практике
|
||||||
|
|
||||||

|

|
||||||
@@ -48,17 +301,23 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
|||||||

|

|
||||||
*Короткий кабель с внешними подтягивающими резисторами — 10 кОм к GND на VCC (тянет вниз, когда питания нет), 47 кОм к VCC на DATA (быстрее фронт вверх)*
|
*Короткий кабель с внешними подтягивающими резисторами — 10 кОм к GND на VCC (тянет вниз, когда питания нет), 47 кОм к VCC на DATA (быстрее фронт вверх)*
|
||||||
|
|
||||||
## Что добавлено поверх upstream
|
## Возможности
|
||||||
|
|
||||||
| Возможность | Описание |
|
| Возможность | Описание |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| Живой поток GSMTAP | Каждое декодированное событие отправляется как GSMTAP-SIM UDP-пакет (включено по умолчанию, повторяет поведение simtrace2-sniff) |
|
| Живой поток GSMTAP | Каждое декодированное событие отправляется как GSMTAP-SIM UDP-пакет (включено по умолчанию, повторяет поведение simtrace2-sniff) |
|
||||||
| Правильные sub_types | ATR=0x01 и PPS=0x02 теперь корректно помечаются в PCAP (в upstream всё писалось как APDU=0x00) |
|
| Правильные sub_types | ATR=0x01 и PPS=0x02 корректно помечаются в PCAP |
|
||||||
| ATR/PPS в PCAP | ATR и объединённые запрос+ответ PPS выводятся как PCAP-пакеты |
|
| ATR/PPS в PCAP | ATR и объединённые запрос+ответ PPS выводятся как PCAP-пакеты |
|
||||||
| Отслеживание RST (опционально) | `rst_detect=true`: события assert/deassert сброса (sub_type 0x10) |
|
| Отслеживание RST (опционально) | `rst_detect=true`: события assert/deassert сброса (sub_type 0x10) |
|
||||||
| Отслеживание VCC (опционально) | `vcc_detect=true`: события включения/выключения питания (sub_type 0x11) |
|
| Отслеживание VCC (опционально) | `vcc_detect=true`: события включения/выключения питания (sub_type 0x11) |
|
||||||
| PCAP сразу в файл | `pcap_file=<path>` пишет PCAP без перенаправления вывода оболочки |
|
| PCAP сразу в файл | `pcap_file=<path>` пишет PCAP без перенаправления вывода оболочки |
|
||||||
| Деглитч стартового бита | Деглитч в 2 отсчёта в детекторе стартового бита убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
| Деглитч стартового бита | Подавляет LOW-глитчи короче ETU (2 отсчёта при 16 МГц, масштабируется от частоты дискретизации) |
|
||||||
|
| Захват ETU в середине сессии | Восстанавливает ETU по линии DATA, когда CLK непригоден; скорость, не прошедшая проверку чётности, измеряется заново |
|
||||||
|
| Защита ATR / ресинхронизации | Проверяет ATR, игнорирует неверно кадрированные кандидаты и выравнивается после десинхронизации |
|
||||||
|
| Инверсная конвенция | Поддерживает прямую (TS=0x3B) и инверсную (TS=0x3F) карты |
|
||||||
|
| Блоки T=1 | Разбирает NAD/PCB/LEN/INF/LRC с цепочкой по биту M; INF I-блока выдаётся как APDU |
|
||||||
|
| Флаг чётности | Пакеты с ошибкой чётности помечаются `GSMTAP_FLAG_BAD_FCS` |
|
||||||
|
| Пороги от частоты | Пороги деглитча, защиты и ETU масштабируются от частоты дискретизации |
|
||||||
|
|
||||||
## Опции декодера
|
## Опции декодера
|
||||||
|
|
||||||
@@ -113,271 +372,6 @@ uint32 в герцах (полезная нагрузка 7 байт). Пока
|
|||||||
никогда не заполняет `res`); семантика флагов повторяет флаги данных (data
|
никогда не заполняет `res`); семантика флагов повторяет флаги данных (data
|
||||||
flags) USB-протокола simtrace2 и не является частью спецификации libosmocore.
|
flags) USB-протокола simtrace2 и не является частью спецификации libosmocore.
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
Быстрый старт после клонирования (см. «Установка» ниже):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./setup.sh # проверить sigrok-cli, зарегистрировать симлинк декодера
|
|
||||||
./start.sh # запись + поток GSMTAP на 127.0.0.1:4729
|
|
||||||
```
|
|
||||||
|
|
||||||
Умолчания `start.sh`: выводы D5=CLK, D0=I/O, D4=RST, D2=VCC, частота
|
|
||||||
дискретизации 16 МГц. Переопределяются аргументами:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./start.sh --clk=D1 --data=D6 # другое подключение
|
|
||||||
./start.sh --no-rst --no-vcc # только CLK+I/O
|
|
||||||
./start.sh --host=10.0.28.7 # стримить на удалённый анализатор
|
|
||||||
./start.sh --pcap=session.pcap # также записать декодированные события в PCAP
|
|
||||||
./start.sh --debug # полный вывод декодера и sigrok (все
|
|
||||||
# строки аннотаций + libsigrok -l 4);
|
|
||||||
# по умолчанию показываются только
|
|
||||||
# декодированные APDU и предупреждения/ошибки
|
|
||||||
```
|
|
||||||
|
|
||||||
### Подключение
|
|
||||||
|
|
||||||
Рекомендуемый способ съёма — короткий кабель с внешними подтягивающими
|
|
||||||
резисторами (см. фото выше): 10 кОм к GND на VCC (тянет вниз без питания)
|
|
||||||
и 47 кОм к VCC на DATA (быстрее фронт вверх). Его распиновка **фиксирована
|
|
||||||
и известна заранее** — см. таблицу «Распиновка» ниже — и в точности
|
|
||||||
совпадает с умолчаниями `start.sh` (`D5`=CLK, `D0`=I/O, `D4`=RST,
|
|
||||||
`D2`=VCC). Автодетект подключения не нужен; выводы не меняются.
|
|
||||||
|
|
||||||
> **Сначала GND.** Сначала подключите GND и проверьте его вручную
|
|
||||||
> (мультиметром или заведомо исправным контактом) — отсутствующий или
|
|
||||||
> плохой контакт земли маскируется под все остальные неисправности сразу:
|
|
||||||
> плавающие линии, фантомные стартовые биты, кадры из одних нулей.
|
|
||||||
|
|
||||||
### Устойчивость к шумным сигналам
|
|
||||||
|
|
||||||
Живые записи могут глитчить (фантомный стартовый бит при включении питания
|
|
||||||
раньше сдвигал весь разбор ATR на единицу). Теперь декодер:
|
|
||||||
|
|
||||||
- проверяет байт TS в ATR (`0x3B`/`0x3F` по TS 102 221 §5.2.2) и
|
|
||||||
ресинхронизируется на следующем настоящем спаде, если он неверен;
|
|
||||||
- никогда не даёт исключению при декодировании убить запись — ошибки
|
|
||||||
логируются, аннотируются, а конечный автомат ресинхронизируется
|
|
||||||
(конец потока по-прежнему завершается чисто);
|
|
||||||
- ограничивает продолжение цепочки T=1-блоков, чтобы мусор не мог
|
|
||||||
зациклиться навсегда.
|
|
||||||
|
|
||||||
После принятого PPS декодер **следует переключению скорости**: в режиме
|
|
||||||
native CLK `clock_skip` становится равным F/D тактов CLK на бит (например,
|
|
||||||
512/16 = 32), что соответствует новой скорости UART у обеих сторон. Живая
|
|
||||||
запись подтвердила, что сохранение 372 после PPS алиасит весь последующий
|
|
||||||
трафик.
|
|
||||||
|
|
||||||
**Тёплый сброс** (ридер заново подаёт RST примерно каждые ~1.6 с) заставляет
|
|
||||||
карту ответить свежим ATR на стандартных 372 тактах/бит. Декодер
|
|
||||||
**заново взводит охоту за ATR по deassert RST**: состояние возвращается в
|
|
||||||
`FIND START`, native-пропуск сбрасывается в 372, счётчик охоты очищается —
|
|
||||||
поэтому повторный ATR разбирается как ATR, а не ошибочно читается как
|
|
||||||
TPDU-трафик на скорости, согласованной PPS. **Защита стартового бита в
|
|
||||||
1.5 ETU** дополнительно отбрасывает фантомный `0x00`, который глитч на
|
|
||||||
холостом ходу может вставить перед этим ATR. Требуется `rst_detect=true`
|
|
||||||
(назначенный канал `rst`); без него сброс в середине сессии всё равно
|
|
||||||
будет прочитан неверно.
|
|
||||||
|
|
||||||
### Решение проблем при живой записи
|
|
||||||
|
|
||||||
`start.sh` запускает одну `--continuous` запись, пока вы не нажмёте
|
|
||||||
**Ctrl+C** (`rc=130`). stdin отвязан от sigrok-cli, поэтому нажатия
|
|
||||||
клавиш/Enter в терминале **не** останавливают запись (приглашение sigrok
|
|
||||||
«нажмите любую клавишу для остановки» отключено) — раньше это нажатие
|
|
||||||
ошибочно трактовалось как «преждевременный обрыв», хотя никакой
|
|
||||||
неисправности устройства не было.
|
|
||||||
|
|
||||||
Большая часть раннего «шума» в записях (ошибки чётности, мусор из-за
|
|
||||||
неверного кадрирования, дребезг линии VCC) была следствием плохо
|
|
||||||
декодированного конечного автомата, а не железа: переподключение съёма
|
|
||||||
ничего не давало, а переписанный декодер полностью это вылечил (re-arm по
|
|
||||||
тёплому сбросу, защита стартового бита 1.5 ETU, антидребезг VCC 10 мс,
|
|
||||||
сборка обмена T=0). Чистая запись теперь декодируется с нулём мусора —
|
|
||||||
см. проверку `vs_reader.py` ниже.
|
|
||||||
|
|
||||||
Если запись *всё-таки* завершается сама (`rc=0`), устройство действительно
|
|
||||||
перестало стримить; обычный подозреваемый — потолок пропускной способности
|
|
||||||
USB у FX2 при длительной высокой частоте дискретизации.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./start.sh --pcap=s.pcap --log=s.log # также записать события и лог rc=
|
|
||||||
./start.sh ... --samplerate=24M # больше запаса по таймингу при необходимости
|
|
||||||
./start.sh ... --klog # также записать журнал ядра
|
|
||||||
./start.sh ... --debug # полный вывод: все строки аннотаций
|
|
||||||
# + внутренности libsigrok на -l 4
|
|
||||||
# (по умолчанию: только APDU + предупреждения)
|
|
||||||
```
|
|
||||||
|
|
||||||
`start.sh` пишет непрерывный поток на настроенной частоте (по умолчанию
|
|
||||||
16 МГц) до Ctrl+C (`rc=130`). Здоровая запись никогда не завершается
|
|
||||||
сама; если FX2 роняет USB-передачу в середине сессии (нестабильное
|
|
||||||
устройство, `rc=0` «device stopped streaming»), запись автоматически
|
|
||||||
перезапускается, чтобы не потерять сессию — она перезапускается при каждом
|
|
||||||
`rc=0`, пока вы не нажмёте Ctrl+C или не сработает `--max-restarts=N`
|
|
||||||
(по умолчанию 20; `0` = без ограничения), с паузой `--restart-delay=S`
|
|
||||||
(по умолчанию 2 с). При перезапуске с `--pcap=FILE` запись каждой сессии
|
|
||||||
пишется в `FILE.<N>.<ext>`, поэтому перезапуск никогда не перезаписывает
|
|
||||||
предыдущую, а повторяющаяся строка завершения
|
|
||||||
`sr_session_stop: session was NULL` фильтруется из stderr.
|
|
||||||
|
|
||||||
- `--no-loop` отключает авто-перезапуск (только одна запись).
|
|
||||||
- `--loop` принудительно включает авто-перезапуск (это поведение по умолчанию).
|
|
||||||
- `--max-restarts=N` ограничивает число перезапусков (по умолчанию 20; `0` = без ограничения).
|
|
||||||
- `--restart-delay=S` — секунды ожидания между перезапусками (по умолчанию 2).
|
|
||||||
- `--log` сохраняет строку вердикта `rc=` для завершения сессии.
|
|
||||||
- Декодер пишет `SIGNAL INTEGRITY WARNING`, когда ≥ 8 из последних
|
|
||||||
64 кадров не проходят проверку чётности.
|
|
||||||
- Десинхронизированные TPDU (обмены с ошибкой кадрирования) сохраняются,
|
|
||||||
но помечаются `GSMTAP_FLAG_BAD_FCS` в байте `res` заголовка GSMTAP —
|
|
||||||
видно в Wireshark как предупреждение BAD-FCS.
|
|
||||||
- Заголовок T=0 со структурно некорректным INS (ISO 7816-4 §12.1.1:
|
|
||||||
`0x00`, `0x6x`/`0x9x` или нечётный младший бит) почти всегда сдвинут на
|
|
||||||
один байт из-за лишнего ведущего байта (например, дочитанного остатка
|
|
||||||
SW2 после PACKETEND). Вместо молчаливого отбрасывания всего 5-байтового
|
|
||||||
окна (из-за чего следующий байт становился фиктивным CLA и проглатывал
|
|
||||||
остаток диалога в один CONCAT-пакет) декодер отбрасывает только ведущий
|
|
||||||
байт и перекадрирует остальные, восстанавливая настоящую команду.
|
|
||||||
Каждый потреблённый, но не выведенный байт явно логируется
|
|
||||||
(`discarded byte 0x…: reason`); отбрасывания холостых 0x00/0xFF
|
|
||||||
ограничены по частоте, отбрасывания не-холостых байтов всегда дают
|
|
||||||
аннотацию декодера.
|
|
||||||
|
|
||||||
Производительность: в режиме native CLK декодер один раз калибрует число
|
|
||||||
отсчётов на фронт CLK по первому кадру ATR, а затем продвигает битовую
|
|
||||||
синхронизацию одиночными пропусками отсчётов вместо ожидания на каждый
|
|
||||||
фронт CLK. Переключение скорости по PPS отслеживается через
|
|
||||||
`clock_skip = F/D`; никакой перекалибровки после PPS или по чётности нет.
|
|
||||||
|
|
||||||
### Сравнение режимов на железе FX2 (результат A/B)
|
|
||||||
|
|
||||||
- `native` декодировал реальный трафик (ATR, переключение PPS, 100+ валидных
|
|
||||||
APDU за 12-секундное окно); единственным устойчивым артефактом был
|
|
||||||
«дребезг» линии VCC — звон ~1 кГц, который теперь подавляет антидребезг 10 мс.
|
|
||||||
- `detect` ни разу не дал пригодной сессии на этом стенде: он полностью
|
|
||||||
пропускал переключение скорости по PPS (обработка PPS не срабатывала) и
|
|
||||||
алиасил весь трафик после PPS; вторая попытка поймала мёртвую линию I/O.
|
|
||||||
|
|
||||||
`native` остаётся режимом по умолчанию. Используйте `--clock=detect`
|
|
||||||
только для офлайн-экспериментов без PPS. Запись, целиком состоящая из
|
|
||||||
нулевых кадров, означает, что съём потерял контакт с линией.
|
|
||||||
|
|
||||||
Чётность — **чётная** по TS 102 221 («чётное число битов, установленных в
|
|
||||||
'1', включая бит чётности»); кадры, не прошедшие проверку, помечаются
|
|
||||||
`CHKSUM ERROR`, но всё равно декодируются.
|
|
||||||
|
|
||||||
Эквивалентные «сырые» команды sigrok-cli:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Минимум: только CLK+I/O, живой поток на localhost:4729
|
|
||||||
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
|
|
||||||
-P iso7816:clk=D0:data=D1:clock_option=detect -A iso7816
|
|
||||||
|
|
||||||
# Полное подключение: CLK + I/O + RST + VCC с отслеживанием событий линий
|
|
||||||
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
|
|
||||||
|
|
||||||
# Поток на удалённый анализатор / отключить стриминг / писать PCAP-файл
|
|
||||||
... -P iso7816:...:gsmtap_host=10.0.28.7 ...
|
|
||||||
... -P iso7816:...:gsmtap_enable=false ...
|
|
||||||
... -P iso7816:...:pcap_file=trace.pcap ...
|
|
||||||
|
|
||||||
# Классический режим upstream: бинарный PCAP в stdout
|
|
||||||
sigrok-cli -i capture.sr -P iso7816:clk=2:data=0 -B iso7816=pcap > out.pcap
|
|
||||||
```
|
|
||||||
|
|
||||||
### Распиновка
|
|
||||||
|
|
||||||
Каналы клона FX2 назначаются через `-P iso7816:clk=N:data=N:rst=N:vcc=N`.
|
|
||||||
Рекомендуемый съём коротким кабелем (с внешними подтягивающими резисторами)
|
|
||||||
использует фиксированные выводы, которые в точности совпадают с умолчаниями
|
|
||||||
`start.sh`; `--clk`/`--data`/`--rst`/`--vcc` переназначают их, если вы
|
|
||||||
подключили иначе:
|
|
||||||
|
|
||||||
| Канал FX2 | Контакт SIM | Сигнал |
|
|
||||||
|-------------|-------------|--------|
|
|
||||||
| D0 | C7 | I/O |
|
|
||||||
| D2 | C1 | VCC (только цифровой уровень!) |
|
|
||||||
| D4 | C2 | RST |
|
|
||||||
| D5 | C3 | CLK |
|
|
||||||
| GND | C5 | GND |
|
|
||||||
|
|
||||||
Входной порог FX2 близок к TTL (~1.4 В): карты класса 1.8 В могут читаться
|
|
||||||
нестабильно; классы 3 В/5 В — нормально.
|
|
||||||
|
|
||||||
**Частота дискретизации и 8-битный режим (важно):** номинальные 24 МГц FX2
|
|
||||||
доступны только при дискретизации в 8-битном режиме, т.е. когда **включены
|
|
||||||
только каналы D0–D7**. Включение 9-го канала (A0/аналоговый на клонах CWAV
|
|
||||||
AX) или любого аналогового входа переводит сбор в 16-битный, который
|
|
||||||
libsigrok ограничивает 12 МГц. Поэтому `start.sh`/`start.bat` явно
|
|
||||||
передают `-C D0,D1,D2,D3,D4,D5,D6,D7` и по умолчанию используют **16 МГц**
|
|
||||||
(с комфортным запасом под потолком USB 24 МГц; ~3.3 отсчёта на такт CLK
|
|
||||||
4.8 МГц). `setup.sh`/`setup.bat` проверяют и сообщают фактический максимум
|
|
||||||
вашего железа при том же ограничении.
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
|
||||||
cd sigrok_iso7816_stream
|
|
||||||
./setup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
`setup.sh` проверяет, что установлены sigrok-cli и пакет прошивки FX2
|
|
||||||
(`sigrok-firmware-fx2lafw`), создаёт симлинк репозитория в
|
|
||||||
`~/.local/share/libsigrokdecode/decoders/iso7816` (пользовательские
|
|
||||||
декодеры перекрывают системные) и опрашивает подключённое устройство на
|
|
||||||
предмет максимальной поддерживаемой частоты дискретизации.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
Предварительные требования:
|
|
||||||
|
|
||||||
1. **sigrok-cli** — установщик для Windows (0.7.2 x86_64) с
|
|
||||||
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads).
|
|
||||||
Если он не запускается с ошибкой `0xc0150002`, установите
|
|
||||||
[Visual C++ 2010 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=26999)
|
|
||||||
(`msvcr100.dll` требуется встроенному Python-рантайму).
|
|
||||||
2. Прошивка fx2lafw поставляется вместе с установщиком — отдельно скачивать не нужно.
|
|
||||||
|
|
||||||
Настройка драйвера (донглам FX2 нужен WinUSB, вендорский драйвер не подойдёт):
|
|
||||||
|
|
||||||
- Запустите **Zadig**, который лежит *внутри* каталога установки sigrok-cli
|
|
||||||
(также доступен из меню «Пуск»). Выберите устройство FX2 в выпадающем
|
|
||||||
списке и установите драйвер **WinUSB**.
|
|
||||||
- После загрузки прошивки устройство переподключается с другим VID/PID —
|
|
||||||
если оно всё ещё не находится, запустите Zadig **второй раз** для
|
|
||||||
переподключившегося устройства.
|
|
||||||
- Windows привязывает драйверы к USB-порту, когда нет серийного номера:
|
|
||||||
перенос донгла в другой порт может потребовать повторить Zadig.
|
|
||||||
|
|
||||||
Затем:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
|
||||||
cd sigrok_iso7816_stream
|
|
||||||
setup.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
`setup.bat` проверяет sigrok-cli и встроенную прошивку, регистрирует декодер
|
|
||||||
этого репозитория через junction в
|
|
||||||
`%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816` (подтверждается тем, что
|
|
||||||
`sigrok-cli -L` показывает `iso7816`), и опрашивает устройство на предмет
|
|
||||||
поддерживаемых частот дискретизации.
|
|
||||||
|
|
||||||
Запись работает так же, как в Linux:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
start.bat rem запись + поток GSMTAP
|
|
||||||
```
|
|
||||||
|
|
||||||
Примечание: bat-файлы требуют переводов строк CRLF; репозиторий следит за
|
|
||||||
этим через `.gitattributes`, поэтому свежий клон в Windows уже корректен.
|
|
||||||
|
|
||||||
## Заметки и ограничения по детектированию RST/VCC
|
## Заметки и ограничения по детектированию RST/VCC
|
||||||
|
|
||||||
- События детектируются **асинхронно до первого ATR** (включение питания /
|
- События детектируются **асинхронно до первого ATR** (включение питания /
|
||||||
@@ -388,16 +382,11 @@ start.bat rem запись + поток GSMTAP
|
|||||||
пакета могут быть пропущены.
|
пакета могут быть пропущены.
|
||||||
- VCC — только цифровой: декодер видит «питание есть/нет», но никогда
|
- VCC — только цифровой: декодер видит «питание есть/нет», но никогда
|
||||||
не класс напряжения (1.8/3/5 В).
|
не класс напряжения (1.8/3/5 В).
|
||||||
- **RST и VCC используют гистерезис стабильности уровня в 10 мс**
|
- **RST и VCC используют гистерезис стабильности уровня 10 мс**
|
||||||
(масштабируется от частоты дискретизации): новый уровень сообщается
|
(масштабируется от частоты дискретизации): новый уровень сообщается
|
||||||
только после того, как он непрерывно стабилен ≥ 10 мс — либо потому, что
|
только после того, как он непрерывно стабилен ≥ 10 мс. Это делает
|
||||||
он держится так долго, либо потому, что он закончился, продержавшись
|
включение и выключение питания *симметричными* и подавляет звон и
|
||||||
столько. Это делает включение и выключение питания *симметричными*:
|
короткие глитчи.
|
||||||
линия VCC, которая **звенит на фронте** (шумный/плавающий съём), больше
|
|
||||||
не теряет событие «VCC on» — оно срабатывает, как только линия
|
|
||||||
устанавливается в HIGH, вместо того чтобы быть съеденным дребезгом
|
|
||||||
фронта. RST использует ту же схему, поэтому короткий глитч RST больше
|
|
||||||
не вызывает фантомный re-arm по тёплому сбросу.
|
|
||||||
- **События VCC в целом ненадёжны — лучше не отслеживать VCC.** Плавающий
|
- **События VCC в целом ненадёжны — лучше не отслеживать VCC.** Плавающий
|
||||||
или неподключённый пин съёма VCC всегда читается как HIGH (логическая
|
или неподключённый пин съёма VCC всегда читается как HIGH (логическая
|
||||||
единица без нагрузки), поэтому декодер постоянно видит «питание подано»
|
единица без нагрузки), поэтому декодер постоянно видит «питание подано»
|
||||||
@@ -410,20 +399,10 @@ start.bat rem запись + поток GSMTAP
|
|||||||
- Неназначенные опциональные каналы игнорируются, даже если
|
- Неназначенные опциональные каналы игнорируются, даже если
|
||||||
`rst_detect`/`vcc_detect` включены (защита через `has_channel()`).
|
`rst_detect`/`vcc_detect` включены (защита через `has_channel()`).
|
||||||
|
|
||||||
## Тестирование
|
## Разработка и тестирование
|
||||||
|
|
||||||
Проверено на примерах записей из upstream:
|
Декодер проверяется на примерах записей с помощью `tools/vs_reader.py`.
|
||||||
|
Фикстуры трасс хранятся локально и не входят в репозиторий.
|
||||||
```bash
|
|
||||||
# ATR+PPS+54 APDU из записи ридера test_8 (~19 с декодирования)
|
|
||||||
# Поток GSMTAP проверен через tshark (ATR/APDU разбираются нативно),
|
|
||||||
# кастомные события 0x10/0x11 — через UDP-слушатель, и сквозным путём
|
|
||||||
# в сервер SIMtrace Analyser (POST /api/capture/start → decode → stop).
|
|
||||||
```
|
|
||||||
|
|
||||||
Синтетический генератор VCD, покрывающий краевые случаи RST/VCC, живёт в
|
|
||||||
заметках проекта (`projects/SIGROK_ISO7816.md` в родительском рабочем
|
|
||||||
пространстве).
|
|
||||||
|
|
||||||
### Проверка по SDK ридера (`tools/vs_reader.py`)
|
### Проверка по SDK ридера (`tools/vs_reader.py`)
|
||||||
|
|
||||||
@@ -440,33 +419,12 @@ python3 tools/vs_reader.py <reader-log>.log out.pcap
|
|||||||
`vs_reader.py` выравнивает каждый обмен из журнала ридера с APDU из записи
|
`vs_reader.py` выравнивает каждый обмен из журнала ридера с APDU из записи
|
||||||
(побайтовое LCS-совпадение) и классифицирует остатки как proactive/CAT
|
(побайтовое LCS-совпадение) и классифицирует остатки как proactive/CAT
|
||||||
(невидимые для SDK ридера), pre-capture/потерянные или мусор.
|
(невидимые для SDK ридера), pre-capture/потерянные или мусор.
|
||||||
Запись, начатая в середине сессии, тёплые сбросы которой раньше давали
|
|
||||||
4 мусорных события, с re-arm и защитой стартового бита декодируется в
|
|
||||||
**0 мусора** (`RESULT: OK - capture fully explained`).
|
|
||||||
|
|
||||||
test_7 (4 сброса ATR, фрагментирующих сессию) теперь захватывает все 37/37
|
|
||||||
обменов ридера (было 10/37 до исправления деглитча). Телефонные трассы
|
|
||||||
Samsung с 16 CLK/ETU (F=512/D=32) теперь разбираются на 924 отдельных
|
|
||||||
обмена T=0 (раньше сливались в 52 APDU завышенного размера).
|
|
||||||
|
|
||||||
Воспроизводимые фикстуры трасс (хранятся локально, вне репозитория)
|
|
||||||
покрывают сессии ридера с контролируемого стенда (с эталонными журналами
|
|
||||||
SDK ридера), телефонные записи с гейтированным CLK и **записи холодного
|
|
||||||
старта**, начинающиеся *до* включения питания (VCC/RST низкие при t=0).
|
|
||||||
Четыре фикстуры Samsung — холодные старты, снятые новым коротким кабелем
|
|
||||||
(провода 30 мм, внешние подтягивающие резисторы GND→VCC и VCC→DATA), с
|
|
||||||
каналами `DATA/VCC/RST/CLK` на битах 0/2/4/5; варианты `sim2` обрезаны до
|
|
||||||
окна включения питания.
|
|
||||||
|
|
||||||
### Записи холодного старта (запись начинается до включения питания)
|
### Записи холодного старта (запись начинается до включения питания)
|
||||||
|
|
||||||
Когда запись начинается до включения питания, декодер должен взвести охоту
|
Когда запись начинается до включения питания, RST всё ещё низкий на
|
||||||
за ATR, потому что RST всё ещё низкий на отсчёте 0: он инициализирует
|
отсчёте 0. Используйте `rst_detect=true` и `protocol=T=0`; декодер взводит
|
||||||
начальные уровни VCC/RST на первом шаге (вместо того чтобы по умолчанию
|
охоту за ATR по начальному уровню RST и разбирает полный ATR. Пример:
|
||||||
считать их снятыми) и при низком RST на старте входит в `FIND START` с
|
|
||||||
очищенным ETU, поэтому переходы DATA при включении питания до ATR не могут
|
|
||||||
зафиксировать фиктивную битовую скорость. Затем он декодирует полный ATR
|
|
||||||
(`3b9f96801fc78031e073fe2117574a3382033339001e`) с 0 мусора:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sigrok-cli -i <coldboot>.sr \
|
sigrok-cli -i <coldboot>.sr \
|
||||||
@@ -474,52 +432,17 @@ sigrok-cli -i <coldboot>.sr \
|
|||||||
-A iso7816
|
-A iso7816
|
||||||
```
|
```
|
||||||
|
|
||||||
Снятие RST (deassert), которое приходит, пока разбор ATR уже идёт, больше не
|
|
||||||
перевзводит охоту посреди чтения — оно логирует `RST deasserted (hunt/ATR
|
|
||||||
already in progress)` и не трогает разбираемый ATR (test_8/test_7
|
|
||||||
CHKSUM 18→0).
|
|
||||||
|
|
||||||
### Сценарии записи: ридер (постоянный CLK) и телефон (Clock Stop Mode)
|
### Сценарии записи: ридер (постоянный CLK) и телефон (Clock Stop Mode)
|
||||||
|
|
||||||
Декодер обрабатывает две физически разные схемы съёма SIM:
|
|
||||||
|
|
||||||
- **Запись ридера / постоянный CLK** — ридер ведёт ровный CLK всю сессию.
|
- **Запись ридера / постоянный CLK** — ридер ведёт ровный CLK всю сессию.
|
||||||
В режиме mid-session запись декодируется в **0 мусора**
|
Проверяйте её по SDK ридера через
|
||||||
(`RESULT: OK`) относительно эталона из SDK ридера
|
`tools/vs_reader.py <reader-log>.log out.pcap`.
|
||||||
(`tools/vs_reader.py <reader-log>.log out.pcap`).
|
- **Запись телефона / гейтированный или остановленный CLK** — телефон
|
||||||
Записи ридера с тёплыми сбросами используют fallback с синтетическим
|
гейтирует или останавливает CLK между передачами (Clock Stop Mode),
|
||||||
T=0-ATR.
|
поэтому ETU нельзя вывести из CLK. Запускайте с `protocol=T=0`; декодер
|
||||||
|
восстанавливает ETU из линии DATA и декодирует edge-list читателем.
|
||||||
- **Запись телефона / остановленный CLK** — телефон гейтирует или
|
Команды T=0 case 3/4 (например, `80 12`/`80 14` STK envelope) объединяются
|
||||||
останавливает CLK между передачами (Clock Stop Mode), поэтому ETU не
|
в один APDU команда→ответ.
|
||||||
равен стандартным 372 тактам и не может быть выведен из CLK. Запускайте
|
|
||||||
с `protocol=T=0`; декодер восстанавливает ETU из линии DATA подсчётом
|
|
||||||
фронтов, фиксирует его и декодирует устойчивым к дрейфу edge-list
|
|
||||||
читателем битов. Команды T=0 case 3/4 (например, `80 12`/`80 14`
|
|
||||||
STK envelope) объединяются в один APDU команда→ответ. Деглитч стартового
|
|
||||||
бита в 2 отсчёта убирает LOW-глитчи короче ETU, вызванные артефактами
|
|
||||||
внутренней подтяжки FX2LP, и предотвращает смещение байтов на быстрых
|
|
||||||
телефонных трассах.
|
|
||||||
|
|
||||||
### Проверенные скорости данных и ETU
|
|
||||||
|
|
||||||
Все трассы декодированы на **частоте дискретизации 16 МГц**, 0 мусора,
|
|
||||||
RESULT: OK. Начиная с v1.5.0 каждая трасса из набора примеров также
|
|
||||||
сообщает **0 CONCAT** (перекадрирование по некорректному INS устранило
|
|
||||||
последнее семейство проглатывания нескольких обменов на
|
|
||||||
4gmodem/A55_sim2/S21p_sim2).
|
|
||||||
|
|
||||||
| Трасса | Режим CLK | ETU (отсчёты) | CLK/ETU | Конвенция | Примечания |
|
|
||||||
|-------|----------|---------------|---------|------------|-------|
|
|
||||||
| test_8 | постоянный | ~43 | 372 (стандарт) | Direct | PC/SC ридер, ATR+PPS+APDU |
|
|
||||||
| test_7 | постоянный | ~43 | 372 (стандарт) | Direct | PC/SC ридер, тёплые сбросы |
|
|
||||||
| xiaomi | гейтированный/остановленный | ~53 | восстановлен из DATA | Direct | Телефон, середина сессии |
|
|
||||||
| samsung | гейтированный/остановленный | ~72 | 16 (F=512/D=32) | Direct | Телефон, нестандартный F/D, меню STK |
|
|
||||||
|
|
||||||
Декодер автоопределяет ETU по трассе и выбирает подходящий читатель битов
|
|
||||||
(CLK-синхронный для постоянного CLK, edge-list для гейтированного/
|
|
||||||
остановленного CLK). Никакой ручной настройки не требуется, кроме
|
|
||||||
`clock_option=native` и `protocol=T=0` для телефонных записей.
|
|
||||||
|
|
||||||
Рекомендуемые вызовы:
|
Рекомендуемые вызовы:
|
||||||
|
|
||||||
@@ -536,6 +459,43 @@ sigrok-cli -i <phone>.sr \
|
|||||||
-A iso7816
|
-A iso7816
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Скорости данных и ETU
|
||||||
|
|
||||||
|
Справочные значения, наблюдавшиеся на примерах записей при частоте
|
||||||
|
дискретизации 16 МГц:
|
||||||
|
|
||||||
|
| Трасса | Режим CLK | ETU (отсчёты) | CLK/ETU | Конвенция | Примечания |
|
||||||
|
|-------|----------|---------------|---------|------------|-------|
|
||||||
|
| test_8 | постоянный | ~43 | 372 (стандарт) | Direct | PC/SC ридер, ATR+PPS+APDU |
|
||||||
|
| test_7 | постоянный | ~43 | 372 (стандарт) | Direct | PC/SC ридер, тёплые сбросы |
|
||||||
|
| xiaomi | гейтированный/остановленный | ~53 | восстановлен из DATA | Direct | Телефон, середина сессии |
|
||||||
|
| samsung | гейтированный/остановленный | ~72 | 16 (F=512/D=32) | Direct | Телефон, нестандартный F/D, меню STK |
|
||||||
|
| alcor_reader | постоянный | ~53 | 16 (F=512/D=32) | Direct | PC/SC ридер, CLK 4.0 МГц на ATR -> 4.8 МГц после PPS |
|
||||||
|
|
||||||
|
Декодер автоопределяет ETU и выбирает подходящий читатель битов
|
||||||
|
(CLK-синхронный для постоянного CLK, edge-list для гейтированного/
|
||||||
|
остановленного CLK). Никакой ручной настройки не требуется, кроме
|
||||||
|
`clock_option=native` и `protocol=T=0` для телефонных записей.
|
||||||
|
|
||||||
|
Чётность — **чётная** по TS 102 221 («чётное число битов, установленных в
|
||||||
|
'1', включая бит чётности»); кадры, не прошедшие проверку, помечаются
|
||||||
|
`CHKSUM ERROR`, но всё равно декодируются.
|
||||||
|
|
||||||
|
## Связанные инструменты
|
||||||
|
|
||||||
|
SIMple, SIMtrace Analyser и sigrok_iso7816_stream — три автономных
|
||||||
|
инструмента для работы с SIM/UICC-картами, представленные на одном
|
||||||
|
[сайте](https://atroshin.ru) и в одном [Telegram-канале](https://t.me/youeyeseesee).
|
||||||
|
Каждый полностью независим — устанавливайте только нужные вам:
|
||||||
|
|
||||||
|
- **[SIMple](https://github.com/anttro/simple)** — мастерская для UICC/SIM:
|
||||||
|
конструкторы APDU, OTA SCP80/SCP81, симуляция SIM Toolkit и профилирование
|
||||||
|
карт.
|
||||||
|
- **[SIMtrace Analyser](https://github.com/anttro/simtrace-analyser)** —
|
||||||
|
анализатор APDU-трассировок GSMTAP-SIM.
|
||||||
|
- **[sigrok_iso7816_stream](https://github.com/anttro/sigrok_iso7816_stream)** —
|
||||||
|
пассивный ISO 7816-сниффер на дешёвом логическом анализаторе FX2.
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
GPL-2.0+, как и upstream.
|
GPL-2.0+, как и upstream.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="690" viewBox="0 0 960 690" role="img" aria-labelledby="diagram-title diagram-desc" font-family="'Segoe UI', 'Helvetica Neue', Arial, 'DejaVu Sans', sans-serif">
|
||||||
|
<title id="diagram-title">sigrok_iso7816_stream: signal path</title>
|
||||||
|
<desc id="diagram-desc">SIM card and device with a passive tap on the contacts, FX2 logic analyzer (fx2lafw, 8 channels, up to 24 MHz), sigrok-cli with the iso7816 decoder, GSMTAP UDP :4729, SIMtrace Analyser and Wireshark</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
|
<path d="M0 0 L10 5 L0 10 Z" fill="#64748b"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect x="1" y="1" width="958" height="688" rx="18" fill="#ffffff" stroke="#e2e8f0" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="30" text-anchor="middle" font-size="14" fill="#475569">passive tap on the contacts</text>
|
||||||
|
<rect x="150" y="48" width="210" height="72" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="255" y="90" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">SIM card</text>
|
||||||
|
<rect x="600" y="48" width="210" height="72" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="705" y="90" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">Device</text>
|
||||||
|
<line x1="372" y1="84" x2="588" y2="84" stroke="#64748b" stroke-width="2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||||
|
<circle cx="480" cy="84" r="5" fill="#2563eb"/>
|
||||||
|
<line x1="480" y1="89" x2="480" y2="196" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="270" y="200" width="420" height="92" rx="12" fill="#f5f3ff" stroke="#c4b5fd" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="236" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">FX2 logic analyzer</text>
|
||||||
|
<text x="480" y="265" text-anchor="middle" font-size="15" fill="#475569">fx2lafw · 8 channels · up to 24 MHz</text>
|
||||||
|
<line x1="480" y1="292" x2="480" y2="332" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="270" y="336" width="420" height="80" rx="12" fill="#ecfdf5" stroke="#6ee7b7" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="382" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" xml:space="preserve"><tspan font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">sigrok-cli</tspan><tspan> + </tspan><tspan font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">iso7816</tspan><tspan> decoder</tspan></text>
|
||||||
|
<line x1="480" y1="416" x2="480" y2="456" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="320" y="460" width="320" height="72" rx="12" fill="#fffbeb" stroke="#fcd34d" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="502" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">GSMTAP UDP :4729</text>
|
||||||
|
<path d="M480 532 L480 548 Q480 558 470 558 L255 558 Q245 558 245 568 L245 584" fill="none" stroke="#64748b" stroke-width="2" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M480 532 L480 548 Q480 558 490 558 L705 558 Q715 558 715 568 L715 584" fill="none" stroke="#64748b" stroke-width="2" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="110" y="588" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="245" y="630" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">SIMtrace Analyser</text>
|
||||||
|
<rect x="580" y="588" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="715" y="630" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">Wireshark</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="690" viewBox="0 0 960 690" role="img" aria-labelledby="diagram-title diagram-desc" font-family="'Segoe UI', 'Helvetica Neue', Arial, 'DejaVu Sans', sans-serif">
|
||||||
|
<title id="diagram-title">sigrok_iso7816_stream: путь сигнала</title>
|
||||||
|
<desc id="diagram-desc">SIM-карта и устройство с пассивным съёмом с контактов, логический анализатор FX2 (fx2lafw, 8 каналов, до 24 МГц), sigrok-cli с декодером iso7816, GSMTAP UDP :4729, SIMtrace Analyser и Wireshark</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||||
|
<path d="M0 0 L10 5 L0 10 Z" fill="#64748b"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect x="1" y="1" width="958" height="688" rx="18" fill="#ffffff" stroke="#e2e8f0" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="30" text-anchor="middle" font-size="14" fill="#475569">пассивный съём с контактов</text>
|
||||||
|
<rect x="150" y="48" width="210" height="72" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="255" y="90" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">SIM-карта</text>
|
||||||
|
<rect x="600" y="48" width="210" height="72" rx="12" fill="#eff6ff" stroke="#93c5fd" stroke-width="1.5"/>
|
||||||
|
<text x="705" y="90" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">Устройство</text>
|
||||||
|
<line x1="372" y1="84" x2="588" y2="84" stroke="#64748b" stroke-width="2" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||||
|
<circle cx="480" cy="84" r="5" fill="#2563eb"/>
|
||||||
|
<line x1="480" y1="89" x2="480" y2="196" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="270" y="200" width="420" height="92" rx="12" fill="#f5f3ff" stroke="#c4b5fd" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="236" text-anchor="middle" font-size="20" font-weight="600" fill="#0f172a">Логический анализатор FX2</text>
|
||||||
|
<text x="480" y="265" text-anchor="middle" font-size="15" fill="#475569">fx2lafw · 8 каналов · до 24 МГц</text>
|
||||||
|
<line x1="480" y1="292" x2="480" y2="332" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="270" y="336" width="420" height="80" rx="12" fill="#ecfdf5" stroke="#6ee7b7" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="382" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" xml:space="preserve"><tspan font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">sigrok-cli</tspan><tspan> + декодер </tspan><tspan font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">iso7816</tspan></text>
|
||||||
|
<line x1="480" y1="416" x2="480" y2="456" stroke="#64748b" stroke-width="2" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="320" y="460" width="320" height="72" rx="12" fill="#fffbeb" stroke="#fcd34d" stroke-width="1.5"/>
|
||||||
|
<text x="480" y="502" text-anchor="middle" font-size="19" font-weight="600" fill="#0f172a" font-family="'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace">GSMTAP UDP :4729</text>
|
||||||
|
<path d="M480 532 L480 548 Q480 558 470 558 L255 558 Q245 558 245 568 L245 584" fill="none" stroke="#64748b" stroke-width="2" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M480 532 L480 548 Q480 558 490 558 L705 558 Q715 558 715 568 L715 584" fill="none" stroke="#64748b" stroke-width="2" stroke-linejoin="round" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="110" y="588" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="245" y="630" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">SIMtrace Analyser</text>
|
||||||
|
<rect x="580" y="588" width="270" height="72" rx="12" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
|
||||||
|
<text x="715" y="630" text-anchor="middle" font-size="18" font-weight="600" fill="#0f172a">Wireshark</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -36,27 +36,43 @@ if /i "%~1"=="--no-rst" ( set "RST=" & shift & goto :argloop )
|
|||||||
if /i "%~1"=="--no-vcc" ( set "VCC=" & shift & goto :argloop )
|
if /i "%~1"=="--no-vcc" ( set "VCC=" & shift & goto :argloop )
|
||||||
if /i "%~1"=="-h" ( goto :usage )
|
if /i "%~1"=="-h" ( goto :usage )
|
||||||
if /i "%~1"=="--help" ( goto :usage )
|
if /i "%~1"=="--help" ( goto :usage )
|
||||||
|
rem Value options also accept the --opt=value form (usage shows the space form).
|
||||||
|
for /f "tokens=1,* delims==" %%A in ("%~1") do (
|
||||||
|
if /i "%%A"=="--clk" ( set "CLK=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--data" ( set "DATA=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--rst" ( set "RST=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--vcc" ( set "VCC=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--samplerate" ( set "SAMPLERATE=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--clock" ( set "CLOCK=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--host" ( set "HOST=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--port" ( set "PORT=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--pcap" ( set "PCAP=%%B" & shift & goto :argloop )
|
||||||
|
if /i "%%A"=="--protocol" ( set "PROTOCOL=%%B" & shift & goto :argloop )
|
||||||
|
)
|
||||||
echo Unknown option: %~1
|
echo Unknown option: %~1
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
:usage
|
:usage
|
||||||
echo Usage: %~n0 [OPTIONS]
|
echo Usage: %~n0 [OPTIONS]
|
||||||
echo --clk=CH CLK channel default: %CLK%
|
echo --clk CH CLK channel default: %CLK%
|
||||||
echo --data=CH I/O channel default: %DATA%
|
echo --data CH I/O channel default: %DATA%
|
||||||
echo --rst=CH RST channel default: %RST%
|
echo --rst CH RST channel default: %RST%
|
||||||
echo --vcc=CH VCC channel default: %VCC%
|
echo --vcc CH VCC channel default: %VCC%
|
||||||
echo --samplerate=SR Sample rate default: %SAMPLERATE%
|
echo --samplerate SR Sample rate default: %SAMPLERATE%
|
||||||
echo --clock=MODE native/detect/sample_as_clock
|
echo --clock MODE native/detect/sample_as_clock
|
||||||
echo --host=IP GSMTAP destination default: 127.0.0.1
|
echo --host IP GSMTAP destination default: 127.0.0.1
|
||||||
echo --port=N GSMTAP port default: 4729
|
echo --port N GSMTAP port default: 4729
|
||||||
echo --pcap=FILE Also write decoded events to FILE (no spaces in path)
|
echo --pcap FILE Also write decoded events to FILE (no spaces in path)
|
||||||
echo --debug Full decoder + sigrok output (all annotation rows + -l 4).
|
echo --debug Full decoder + sigrok output (all annotation rows + -l 4).
|
||||||
echo
|
echo
|
||||||
echo By default only decoded APDUs and decoder warnings/errors are shown.
|
echo By default only decoded APDUs and decoder warnings/errors are shown.
|
||||||
echo --protocol=MODE T=0 (default), T=1, or auto (mid-session decode)
|
echo --protocol MODE T=0 (default), T=1, or auto (mid-session decode)
|
||||||
echo --no-rst Disable RST tracking
|
echo --no-rst Disable RST tracking
|
||||||
echo --no-vcc Disable VCC tracking
|
echo --no-vcc Disable VCC tracking
|
||||||
echo.
|
echo.
|
||||||
|
echo The --clk=CH form also works. start.bat supports this core subset;
|
||||||
|
echo start.sh has the full option set.
|
||||||
|
echo.
|
||||||
echo To log output on Windows use redirection: %~n0 ... ^> capture.log 2^>^&1
|
echo To log output on Windows use redirection: %~n0 ... ^> capture.log 2^>^&1
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
|
|||||||
+71
-16
@@ -1,7 +1,7 @@
|
|||||||
=== Baseline captured 2026-09-27T23:45:58+03:00 ===
|
=== Baseline captured 2026-10-20T18:55:32+03:00 ===
|
||||||
|
|
||||||
=== test_8 ===
|
=== test_8 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('etu recovered (samples):', 1237)
|
('etu recovered (samples):', 1237)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== test_7 ===
|
=== test_7 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('etu recovered (samples):', 1236)
|
('etu recovered (samples):', 1236)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_phone_sample ===
|
=== xiaomi_phone_sample ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('etu recovered (samples):', 53)
|
('etu recovered (samples):', 53)
|
||||||
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_mi_a1_coldboot ===
|
=== xiaomi_mi_a1_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -112,12 +112,12 @@ CHKSUM ERROR count: 0
|
|||||||
CONCAT APDU count: 0
|
CONCAT APDU count: 0
|
||||||
reader exchanges : 0
|
reader exchanges : 0
|
||||||
capture ATRs : 1
|
capture ATRs : 1
|
||||||
capture APDUs : 473
|
capture APDUs : 475
|
||||||
byte-exact matches (LCS) : 0
|
byte-exact matches (LCS) : 0
|
||||||
reader exchanges not in capture (pre-capture / dropped): 0
|
reader exchanges not in capture (pre-capture / dropped): 0
|
||||||
capture APDUs not in reader log:
|
capture APDUs not in reader log:
|
||||||
- proactive/CAT (unlogged by reader SDK): 10
|
- proactive/CAT (unlogged by reader SDK): 10
|
||||||
- valid exchanges (reader retries/dupes): 463
|
- valid exchanges (reader retries/dupes): 465
|
||||||
- GARBAGE (mis-framed/desync): 0
|
- GARBAGE (mis-framed/desync): 0
|
||||||
|
|
||||||
Degradation metrics:
|
Degradation metrics:
|
||||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== 4gmodem_coldboot ===
|
=== 4gmodem_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== alcor_reader_sample ===
|
=== alcor_reader_sample ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable ===
|
=== samsung_A55_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable ===
|
=== samsung_S21p_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.2 ===',)
|
('=== iso7816 decoder v1.9.7 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -342,27 +342,57 @@ test_explicit_eof_wins (__main__.TestAtEof.test_explicit_eof_wins) ... ok
|
|||||||
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
||||||
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
||||||
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
||||||
|
test_atr_with_parity_error_is_ignored (__main__.TestAtrHuntDetect.test_atr_with_parity_error_is_ignored) ... ok
|
||||||
|
test_bare_ts_t0_is_not_committed_as_an_atr (__main__.TestAtrHuntDetect.test_bare_ts_t0_is_not_committed_as_an_atr) ... ok
|
||||||
|
test_eof_after_plausible_cla_is_clean (__main__.TestAtrHuntDetect.test_eof_after_plausible_cla_is_clean) ... ok
|
||||||
|
test_handoff_on_extended_channel_cla (__main__.TestAtrHuntDetect.test_handoff_on_extended_channel_cla) ... ok
|
||||||
|
test_handoff_on_odd_ins_89 (__main__.TestAtrHuntDetect.test_handoff_on_odd_ins_89) ... ok
|
||||||
|
test_inverse_ts_pattern_is_parsed (__main__.TestAtrHuntDetect.test_inverse_ts_pattern_is_parsed) ... ok
|
||||||
|
test_no_handoff_on_basic_channel_cla (__main__.TestAtrHuntDetect.test_no_handoff_on_basic_channel_cla) ... ok
|
||||||
|
test_no_handoff_on_invalid_ins (__main__.TestAtrHuntDetect.test_no_handoff_on_invalid_ins) ... ok
|
||||||
|
test_truncated_atr_ta_is_clean (__main__.TestAtrHuntDetect.test_truncated_atr_ta_is_clean) ... ok
|
||||||
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
||||||
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
||||||
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
||||||
|
test_measured_etu_skips_recovery (__main__.TestAtrHuntEscape.test_measured_etu_skips_recovery) ... ok
|
||||||
test_no_recovery_when_etu_known (__main__.TestAtrHuntEscape.test_no_recovery_when_etu_known) ... ok
|
test_no_recovery_when_etu_known (__main__.TestAtrHuntEscape.test_no_recovery_when_etu_known) ... ok
|
||||||
test_non_ff_resumes_when_count_high (__main__.TestAtrHuntEscape.test_non_ff_resumes_when_count_high) ... ok
|
test_non_ff_resumes_when_count_high (__main__.TestAtrHuntEscape.test_non_ff_resumes_when_count_high) ... ok
|
||||||
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
||||||
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
||||||
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
||||||
|
test_seeded_bit_samples_does_not_block_recovery (__main__.TestAtrHuntEscape.test_seeded_bit_samples_does_not_block_recovery) ... ok
|
||||||
|
test_9e_9f_are_status_words (__main__.TestDecodeStep.test_9e_9f_are_status_words) ... ok
|
||||||
|
test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_dense_valid_cla_is_not_idle_noise) ... ok
|
||||||
|
test_data_0x3b_before_traffic_is_atr_candidate (__main__.TestDecodeStep.test_data_0x3b_before_traffic_is_atr_candidate) ... ok
|
||||||
|
test_data_0x3b_is_not_an_atr_after_traffic (__main__.TestDecodeStep.test_data_0x3b_is_not_an_atr_after_traffic) ... ok
|
||||||
|
test_data_0x3f_before_traffic_is_atr_candidate (__main__.TestDecodeStep.test_data_0x3f_before_traffic_is_atr_candidate) ... ok
|
||||||
|
test_ex_class_exchange_frames (__main__.TestDecodeStep.test_ex_class_exchange_frames) ... ok
|
||||||
|
test_extended_channel_exchange_frames (__main__.TestDecodeStep.test_extended_channel_exchange_frames) ... ok
|
||||||
|
test_idle_noise_still_discarded (__main__.TestDecodeStep.test_idle_noise_still_discarded) ... ok
|
||||||
|
test_immediate_sw_rejection_frames (__main__.TestDecodeStep.test_immediate_sw_rejection_frames) ... ok
|
||||||
|
test_invalid_ins_re_frame_recovers_command (__main__.TestDecodeStep.test_invalid_ins_re_frame_recovers_command) ... ok
|
||||||
|
test_inverse_ts_raw_pattern_is_an_atr_candidate (__main__.TestDecodeStep.test_inverse_ts_raw_pattern_is_an_atr_candidate) ... ok
|
||||||
|
test_le_256_response_data_is_kept (__main__.TestDecodeStep.test_le_256_response_data_is_kept) ... ok
|
||||||
|
test_parity_error_flags_bad_fcs (__main__.TestDecodeStep.test_parity_error_flags_bad_fcs) ... ok
|
||||||
|
test_short_response_is_split_and_flagged (__main__.TestDecodeStep.test_short_response_is_split_and_flagged) ... ok
|
||||||
|
test_short_response_replays_following_exchange (__main__.TestDecodeStep.test_short_response_replays_following_exchange) ... ok
|
||||||
|
test_text_byte_boundary_is_discarded_not_framed (__main__.TestDecodeStep.test_text_byte_boundary_is_discarded_not_framed) ... ok
|
||||||
test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_removed) ... ok
|
test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_removed) ... ok
|
||||||
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
||||||
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... 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_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pulse_start_edge_preserved) ... ok
|
||||||
|
test_idle_noise_byte (__main__.TestDesyncHelpers.test_idle_noise_byte) ... ok
|
||||||
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... 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_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_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
||||||
|
test_is_desynced_extended_channel_valid (__main__.TestDesyncHelpers.test_is_desynced_extended_channel_valid) ... ok
|
||||||
test_is_desynced_null_sw1_is_invalid (__main__.TestDesyncHelpers.test_is_desynced_null_sw1_is_invalid) ... ok
|
test_is_desynced_null_sw1_is_invalid (__main__.TestDesyncHelpers.test_is_desynced_null_sw1_is_invalid) ... ok
|
||||||
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... 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_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
||||||
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... 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_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_plausible_sw_rejects_garbage (__main__.TestDesyncHelpers.test_plausible_sw_rejects_garbage) ... ok
|
||||||
|
test_resync_idle_waits_on_a_level_condition (__main__.TestDesyncHelpers.test_resync_idle_waits_on_a_level_condition) ... ok
|
||||||
test_a55_u1_concat (__main__.TestEmbeddedExchanges.test_a55_u1_concat) ... 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_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_channel1_select_after_manage_channel_swallow (__main__.TestEmbeddedExchanges.test_channel1_select_after_manage_channel_swallow) ... ok
|
||||||
@@ -374,15 +404,24 @@ test_clean_secured_packet_payload_alcor (__main__.TestEmbeddedExchanges.test_cle
|
|||||||
test_clean_select_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok
|
test_clean_select_611e (__main__.TestEmbeddedExchanges.test_clean_select_611e) ... ok
|
||||||
test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... ok
|
test_clean_select_6a82 (__main__.TestEmbeddedExchanges.test_clean_select_6a82) ... ok
|
||||||
test_concat_packets_are_not_is_desynced (__main__.TestEmbeddedExchanges.test_concat_packets_are_not_is_desynced) ... ok
|
test_concat_packets_are_not_is_desynced (__main__.TestEmbeddedExchanges.test_concat_packets_are_not_is_desynced) ... ok
|
||||||
|
test_embedded_cla_membership_pinned (__main__.TestEmbeddedExchanges.test_embedded_cla_membership_pinned) ... ok
|
||||||
test_samsung_record_burst_concat (__main__.TestEmbeddedExchanges.test_samsung_record_burst_concat) ... 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_xiaomi_select_getresp_swallow (__main__.TestEmbeddedExchanges.test_xiaomi_select_getresp_swallow) ... ok
|
||||||
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
test_eoferror_propagates (__main__.TestEndOfStream.test_eoferror_propagates) ... ok
|
||||||
test_systemerror_propagates (__main__.TestEndOfStream.test_systemerror_propagates) ... ok
|
test_systemerror_propagates (__main__.TestEndOfStream.test_systemerror_propagates) ... ok
|
||||||
|
test_ff_burst_is_accepted (__main__.TestEtuCandidate.test_ff_burst_is_accepted) ... ok
|
||||||
|
test_normal_burst_is_accepted (__main__.TestEtuCandidate.test_normal_burst_is_accepted) ... ok
|
||||||
|
test_single_spacing_is_rejected (__main__.TestEtuCandidate.test_single_spacing_is_rejected) ... ok
|
||||||
|
test_zero_run_guard_is_rejected (__main__.TestEtuCandidate.test_zero_run_guard_is_rejected) ... ok
|
||||||
test_build_packet_bad_fcs_flag (__main__.TestGsmtapFlags.test_build_packet_bad_fcs_flag) ... 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_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_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_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
||||||
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
||||||
|
test_get_bytes_direct_lsb_first (__main__.TestInverseConvention.test_get_bytes_direct_lsb_first) ... ok
|
||||||
|
test_get_bytes_inverse_msb_first (__main__.TestInverseConvention.test_get_bytes_inverse_msb_first) ... ok
|
||||||
|
test_ts_from_bits_direct_inverse_and_garbage (__main__.TestInverseConvention.test_ts_from_bits_direct_inverse_and_garbage) ... ok
|
||||||
|
test_wait_data_falling_uses_high_idle_and_falling_start (__main__.TestInverseConvention.test_wait_data_falling_uses_high_idle_and_falling_start) ... ok
|
||||||
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
||||||
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
||||||
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
||||||
@@ -394,27 +433,43 @@ test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall
|
|||||||
Regression test for Samsung 16 CLK/ETU concatenation. ... ok
|
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_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_returns_none_when_no_fall (__main__.TestNextStartFall.test_returns_none_when_no_fall) ... ok
|
||||||
|
test_etu_glitch_floor_scales (__main__.TestSamplerateScaling.test_etu_glitch_floor_scales) ... ok
|
||||||
|
test_samples_for_us (__main__.TestSamplerateScaling.test_samples_for_us) ... ok
|
||||||
test_advancing_sample_is_not_stall (__main__.TestStallCheck.test_advancing_sample_is_not_stall) ... ok
|
test_advancing_sample_is_not_stall (__main__.TestStallCheck.test_advancing_sample_is_not_stall) ... ok
|
||||||
test_none_pins_is_eof (__main__.TestStallCheck.test_none_pins_is_eof) ... ok
|
test_none_pins_is_eof (__main__.TestStallCheck.test_none_pins_is_eof) ... ok
|
||||||
test_repeated_same_sample_is_bounded_stall (__main__.TestStallCheck.test_repeated_same_sample_is_bounded_stall) ... ok
|
test_repeated_same_sample_is_bounded_stall (__main__.TestStallCheck.test_repeated_same_sample_is_bounded_stall) ... ok
|
||||||
test_untracked_level_wait_never_stalls (__main__.TestStallCheck.test_untracked_level_wait_never_stalls) ... ok
|
test_untracked_level_wait_never_stalls (__main__.TestStallCheck.test_untracked_level_wait_never_stalls) ... ok
|
||||||
test_untracked_resets_stall_window (__main__.TestStallCheck.test_untracked_resets_stall_window) ... ok
|
test_untracked_resets_stall_window (__main__.TestStallCheck.test_untracked_resets_stall_window) ... ok
|
||||||
|
test_decode_step_reads_exactly_one_block (__main__.TestT1Blocks.test_decode_step_reads_exactly_one_block) ... ok
|
||||||
|
test_handle_pps_truncated_is_clean (__main__.TestT1Blocks.test_handle_pps_truncated_is_clean) ... ok
|
||||||
|
test_handle_pps_truncated_optional_is_clean (__main__.TestT1Blocks.test_handle_pps_truncated_optional_is_clean) ... ok
|
||||||
|
test_i_block (__main__.TestT1Blocks.test_i_block) ... ok
|
||||||
|
test_m_chain_concatenates_inf (__main__.TestT1Blocks.test_m_chain_concatenates_inf) ... ok
|
||||||
|
test_r_and_s_blocks_have_no_inf (__main__.TestT1Blocks.test_r_and_s_blocks_have_no_inf) ... ok
|
||||||
|
test_truncated_block_before_lrc_is_clean (__main__.TestT1Blocks.test_truncated_block_before_lrc_is_clean) ... ok
|
||||||
|
test_truncated_inf_is_clean (__main__.TestT1Blocks.test_truncated_inf_is_clean) ... ok
|
||||||
test_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
test_invalid_cla (__main__.TestValidateT0Apdu.test_invalid_cla) ... ok
|
||||||
test_invalid_cla_proprietary_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_proprietary_unseen) ... ok
|
test_invalid_cla_reserved_and_unseen (__main__.TestValidateT0Apdu.test_invalid_cla_reserved_and_unseen) ... ok
|
||||||
test_invalid_ins_lsb_odd (__main__.TestValidateT0Apdu.test_invalid_ins_lsb_odd) ... ok
|
test_invalid_cla_untraced_variants (__main__.TestValidateT0Apdu.test_invalid_cla_untraced_variants) ... ok
|
||||||
|
test_invalid_ins_odd_undefined (__main__.TestValidateT0Apdu.test_invalid_ins_odd_undefined) ... ok
|
||||||
test_invalid_ins_sw_collision (__main__.TestValidateT0Apdu.test_invalid_ins_sw_collision) ... 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_ins_zero (__main__.TestValidateT0Apdu.test_invalid_ins_zero) ... ok
|
||||||
test_invalid_oversized (__main__.TestValidateT0Apdu.test_invalid_oversized) ... ok
|
test_invalid_oversized (__main__.TestValidateT0Apdu.test_invalid_oversized) ... ok
|
||||||
test_invalid_sw1 (__main__.TestValidateT0Apdu.test_invalid_sw1) ... ok
|
test_invalid_sw1 (__main__.TestValidateT0Apdu.test_invalid_sw1) ... ok
|
||||||
test_invalid_too_short (__main__.TestValidateT0Apdu.test_invalid_too_short) ... ok
|
test_invalid_too_short (__main__.TestValidateT0Apdu.test_invalid_too_short) ... ok
|
||||||
|
test_odd_ins_set_pinned (__main__.TestValidateT0Apdu.test_odd_ins_set_pinned) ... ok
|
||||||
test_valid_case1_direct_sw (__main__.TestValidateT0Apdu.test_valid_case1_direct_sw) ... 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_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_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_channel1_select_after_manage_channel (__main__.TestValidateT0Apdu.test_valid_cla_channel1_select_after_manage_channel) ... ok
|
||||||
|
test_valid_cla_extended_channel_exchanges (__main__.TestValidateT0Apdu.test_valid_cla_extended_channel_exchanges) ... ok
|
||||||
test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla_interindustry_classes) ... ok
|
test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla_interindustry_classes) ... ok
|
||||||
|
test_valid_cla_uicc_classes (__main__.TestValidateT0Apdu.test_valid_cla_uicc_classes) ... ok
|
||||||
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
||||||
|
test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_variants) ... ok
|
||||||
|
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 75 tests in 0.024s
|
Ran 130 tests in 0.029s
|
||||||
|
|
||||||
OK
|
OK
|
||||||
+804
-11
@@ -98,6 +98,31 @@ class TestDesyncHelpers(unittest.TestCase):
|
|||||||
self.assertFalse(self.pd.is_desynced(
|
self.assertFalse(self.pd.is_desynced(
|
||||||
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
||||||
|
|
||||||
|
def test_is_desynced_extended_channel_valid(self):
|
||||||
|
# v1.9.3: higher logical channels and the UICC extended classes
|
||||||
|
# are clean traffic, not desync. 0x44 = SELECT on channel 8,
|
||||||
|
# 0x4F = SELECT on channel 19, 0xC0 = STATUS on channel 4
|
||||||
|
# ('CX'), 0xE2 = STATUS on channel 6 with SM ('EX').
|
||||||
|
for h in ('44a40004009000', '4fa40004009000',
|
||||||
|
'c0f20000009000', 'e2f20000009000'):
|
||||||
|
with self.subTest(h=h):
|
||||||
|
self.assertFalse(self.pd.is_desynced(bytes.fromhex(h)))
|
||||||
|
|
||||||
|
def test_idle_noise_byte(self):
|
||||||
|
# All-1s artifacts with dropped bits are idle noise (including
|
||||||
|
# 0x6F/0x7F/0xAF, which are not valid classes) ...
|
||||||
|
for b in (0x6f, 0x7f, 0xaf, 0xbf, 0xf7, 0xfc, 0xfd, 0xfe):
|
||||||
|
with self.subTest(b=b):
|
||||||
|
self.assertTrue(self.pd.is_idle_noise_byte(b))
|
||||||
|
# ... but valid, bit-dense CLAs are command bytes: 0x5F/0xCF/
|
||||||
|
# 0xE7/0xEB/0xED-0xEF must not be swallowed by the noise resync.
|
||||||
|
for b in (0x5f, 0xcf, 0xe7, 0xeb, 0xed, 0xee, 0xef):
|
||||||
|
with self.subTest(b=b):
|
||||||
|
self.assertFalse(self.pd.is_idle_noise_byte(b))
|
||||||
|
# A sparse invalid class is not "idle noise" either (it takes the
|
||||||
|
# suspicious-CLA path).
|
||||||
|
self.assertFalse(self.pd.is_idle_noise_byte(0x20))
|
||||||
|
|
||||||
def test_is_desynced_null_sw1_is_invalid(self):
|
def test_is_desynced_null_sw1_is_invalid(self):
|
||||||
# Regression (v1.9.1): the T=0 NULL byte 0x60 is not SW1. The
|
# Regression (v1.9.1): the T=0 NULL byte 0x60 is not SW1. The
|
||||||
# wire "NULL 0x60 + real SW1 0x61" was mis-framed as the status
|
# wire "NULL 0x60 + real SW1 0x61" was mis-framed as the status
|
||||||
@@ -126,6 +151,262 @@ class TestDesyncHelpers(unittest.TestCase):
|
|||||||
self.assertFalse(self.pd.is_desynced(b'\x00' * 7))
|
self.assertFalse(self.pd.is_desynced(b'\x00' * 7))
|
||||||
|
|
||||||
|
|
||||||
|
def test_resync_idle_waits_on_a_level_condition(self):
|
||||||
|
# The re-align must WAIT for DATA (a level condition that advances
|
||||||
|
# the sample counter). It used to call wait({'skip': 0}) in a loop,
|
||||||
|
# which never advanced -- a no-op repeated on the same sample.
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.bit_samples = 100
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._start_fall = None
|
||||||
|
inst.samplenum = 0
|
||||||
|
levels = [0, 0, 1]
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def wait(cond):
|
||||||
|
calls.append(cond)
|
||||||
|
want = None
|
||||||
|
if isinstance(cond, list):
|
||||||
|
for c in cond:
|
||||||
|
if self.pd.Decoder.DATA_IDX in c:
|
||||||
|
want = c[self.pd.Decoder.DATA_IDX]
|
||||||
|
while levels:
|
||||||
|
inst.samplenum += 10
|
||||||
|
lvl = levels.pop(0)
|
||||||
|
if (want == 'h' and lvl == 1) or (want == 'l' and lvl == 0):
|
||||||
|
return [lvl] * 8
|
||||||
|
inst.samplenum += 10
|
||||||
|
return [1] * 8
|
||||||
|
|
||||||
|
inst.wait = wait
|
||||||
|
inst.resync_idle()
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertIsInstance(calls[0], list)
|
||||||
|
self.assertIn(self.pd.Decoder.DATA_IDX, calls[0][0])
|
||||||
|
# it advanced through the two LOW samples to the HIGH one
|
||||||
|
self.assertEqual(inst.samplenum, 30)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEtuCandidate(unittest.TestCase):
|
||||||
|
"""The mid-session ETU estimator's burst validation (_etu_candidate)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.inst = object.__new__(self.pd.Decoder)
|
||||||
|
|
||||||
|
def test_zero_run_guard_is_rejected(self):
|
||||||
|
# All-zero characters: 10-ETU LOW runs + a short HIGH inter-byte
|
||||||
|
# guard. The smallest recurring spacing is the guard, not the ETU.
|
||||||
|
pairs = [(1000, 0), (200, 1)] * 8
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 200)
|
||||||
|
self.assertFalse(ok)
|
||||||
|
|
||||||
|
def test_single_spacing_is_rejected(self):
|
||||||
|
# A lone 0x00 character yields one 10-ETU run (10x the true ETU).
|
||||||
|
self.assertEqual(self.inst._etu_candidate([(1000, 0)]),
|
||||||
|
(None, False))
|
||||||
|
self.assertEqual(self.inst._etu_candidate([(8, 0)]),
|
||||||
|
(None, False))
|
||||||
|
|
||||||
|
def test_normal_burst_is_accepted(self):
|
||||||
|
# A 1-ETU LOW spacing (start bit / data run) alongside 2- and
|
||||||
|
# 3-ETU runs: the recurring minimum is the true ETU.
|
||||||
|
pairs = [(100, 0), (100, 1), (200, 0), (300, 1),
|
||||||
|
(100, 0), (100, 1), (200, 0)]
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 100)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
def test_ff_burst_is_accepted(self):
|
||||||
|
# 0xFF: 1-ETU LOW start pulses + long HIGH runs. The minimum is a
|
||||||
|
# LOW run (the start bit), so the guard heuristic must not fire.
|
||||||
|
pairs = [(100, 0), (900, 1), (100, 0), (900, 1), (100, 0)]
|
||||||
|
etu, ok = self.inst._etu_candidate(pairs)
|
||||||
|
self.assertEqual(etu, 100)
|
||||||
|
self.assertTrue(ok)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInverseConvention(unittest.TestCase):
|
||||||
|
"""ISO 7816-3 7.1: inverse = inverted data, msb first; the start bit
|
||||||
|
stays a falling edge from an idle-HIGH line (the old code assumed an
|
||||||
|
idle-LOW line / rising start)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.inst = object.__new__(self.pd.Decoder)
|
||||||
|
self.inst.reset()
|
||||||
|
|
||||||
|
def test_get_bytes_direct_lsb_first(self):
|
||||||
|
self.inst._inverse_convention = False
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 0, 0, 0, 0, 0, 0, 0]), 0x01)
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 1, 0, 0, 0, 0, 0, 0]), 0x03)
|
||||||
|
|
||||||
|
def test_get_bytes_inverse_msb_first(self):
|
||||||
|
self.inst._inverse_convention = True
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 0, 0, 0, 0, 0, 0, 0]), 0x80)
|
||||||
|
self.assertEqual(self.inst.get_bytes([1, 1, 0, 0, 0, 0, 0, 0]), 0xC0)
|
||||||
|
|
||||||
|
def test_ts_from_bits_direct_inverse_and_garbage(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 1, 1, 1, 0, 0, 1]), 0x3b)
|
||||||
|
self.assertEqual(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 0, 0, 0, 0, 0, 1]), 0x3f)
|
||||||
|
self.assertIsNone(
|
||||||
|
self.pd.ts_from_bits([0, 1, 1, 0, 1, 0, 1, 0, 0, 1]))
|
||||||
|
self.assertIsNone(self.pd.ts_from_bits([0, 1, 1, 0]))
|
||||||
|
|
||||||
|
def test_wait_data_falling_uses_high_idle_and_falling_start(self):
|
||||||
|
self.inst._inverse_convention = True
|
||||||
|
self.inst.bit_samples = 100
|
||||||
|
self.inst._min_pulse_samples = 0
|
||||||
|
self.inst.samplenum = 0
|
||||||
|
conds = []
|
||||||
|
|
||||||
|
def wait(cond):
|
||||||
|
conds.append(cond)
|
||||||
|
self.inst.samplenum += 200
|
||||||
|
return [1] * 8 if len(conds) == 1 else [0] * 8
|
||||||
|
|
||||||
|
self.inst.wait = wait
|
||||||
|
self.inst.wait_data_falling()
|
||||||
|
self.assertFalse(self.inst._eof)
|
||||||
|
self.assertEqual(conds[0][0][self.pd.Decoder.DATA_IDX], 'h')
|
||||||
|
self.assertEqual(conds[1][0][self.pd.Decoder.DATA_IDX], 'f')
|
||||||
|
self.assertEqual(self.inst._start_fall, self.inst.samplenum)
|
||||||
|
|
||||||
|
|
||||||
|
class TestT1Blocks(unittest.TestCase):
|
||||||
|
"""T=1 block parsing: NAD/PCB/LEN/INF/LRC, the M-bit chain, and the
|
||||||
|
EOF guards (a truncated block never raises or emits a half INF)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def _make(self, byte_seq):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.samplenum = 0
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.log = lambda *a, **k: None
|
||||||
|
inst.put = lambda *a, **k: None
|
||||||
|
inst.wait = lambda cond=None: [1] * 8
|
||||||
|
inst._bytes = collections.deque(byte_seq)
|
||||||
|
inst.read_byte = lambda: (inst._bytes.popleft()
|
||||||
|
if inst._bytes else None)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def _block(self, nad, pcb, inf):
|
||||||
|
body = [nad, pcb, len(inf)] + list(inf)
|
||||||
|
lrc = 0
|
||||||
|
for b in body:
|
||||||
|
lrc ^= b
|
||||||
|
return body + [lrc]
|
||||||
|
|
||||||
|
def test_i_block(self):
|
||||||
|
inst = self._make(self._block(0x00, 0x00, [0x6f, 0x00]))
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (True, [0x6f, 0x00]))
|
||||||
|
|
||||||
|
def test_r_and_s_blocks_have_no_inf(self):
|
||||||
|
inst = self._make(self._block(0x00, 0x80, []))
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||||
|
inst = self._make(self._block(0x00, 0xc0, []))
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||||
|
|
||||||
|
def test_m_chain_concatenates_inf(self):
|
||||||
|
seq = (self._block(0x00, 0x20, [0x01])
|
||||||
|
+ self._block(0x00, 0x00, [0x02]))
|
||||||
|
inst = self._make(seq)
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (True, [0x01, 0x02]))
|
||||||
|
|
||||||
|
def test_truncated_block_before_lrc_is_clean(self):
|
||||||
|
seq = self._block(0x00, 0x00, [0x6f, 0x00])[:5] # no LRC
|
||||||
|
inst = self._make(seq)
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||||
|
|
||||||
|
def test_truncated_inf_is_clean(self):
|
||||||
|
inst = self._make([0x00, 0x00, 0x05, 0x01, 0x02])
|
||||||
|
self.assertEqual(inst.t1_parse_block(0), (False, []))
|
||||||
|
|
||||||
|
def test_decode_step_reads_exactly_one_block(self):
|
||||||
|
# The T=1 caller used to run an extra chain loop after every
|
||||||
|
# I-block: it consumed the peer's R-block and merged the following
|
||||||
|
# I-block's INF into this packet. One step must parse one block.
|
||||||
|
apdu = [0x00, 0xa4, 0x00, 0x00, 0x00]
|
||||||
|
rblock = self._block(0x00, 0x80, [])
|
||||||
|
inst = self._make(self._block(0x00, 0x00, apdu) + rblock)
|
||||||
|
inst.state = 'DATA'
|
||||||
|
inst.hasT0 = False
|
||||||
|
inst.hasT1 = True
|
||||||
|
inst.bit_samples = 100
|
||||||
|
inst._pps_speed_changed = False
|
||||||
|
inst._resync = False
|
||||||
|
inst._at_eof = lambda: False
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.emitted = []
|
||||||
|
inst.emit_packet = lambda st, pl, ss, es, flags=0: \
|
||||||
|
inst.emitted.append((bytes(pl), flags))
|
||||||
|
inst._note_discard = lambda *a, **k: None
|
||||||
|
inst._note_pre_sw = lambda *a, **k: None
|
||||||
|
inst._consume_first = lambda: (inst._bytes.popleft()
|
||||||
|
if inst._bytes else None)
|
||||||
|
inst.peek_byte = lambda: inst._bytes[0] if inst._bytes else None
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(len(inst.emitted), 1)
|
||||||
|
self.assertEqual(inst.emitted[0][0], bytes(apdu))
|
||||||
|
# The peer's R-block is left for the next step.
|
||||||
|
self.assertEqual(list(inst._bytes), rblock)
|
||||||
|
|
||||||
|
def test_handle_pps_truncated_is_clean(self):
|
||||||
|
inst = self._make([0x55])
|
||||||
|
inst.peeked_samplenum = 0
|
||||||
|
inst.handle_pps() # pps0 missing: must not raise
|
||||||
|
|
||||||
|
def test_handle_pps_truncated_optional_is_clean(self):
|
||||||
|
inst = self._make([0x55, 0x70]) # PPS0 asks for PPS1..3, then EOF
|
||||||
|
inst.peeked_samplenum = 0
|
||||||
|
inst.handle_pps() # PPS1 missing: must not raise
|
||||||
|
|
||||||
|
|
||||||
|
class TestSamplerateScaling(unittest.TestCase):
|
||||||
|
"""The pulse/glitch thresholds scale with the capture rate instead of
|
||||||
|
assuming 16 MHz."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def test_samples_for_us(self):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.samplerate = 16000000
|
||||||
|
self.assertEqual(inst._samples_for_us(0.125), 2)
|
||||||
|
self.assertEqual(inst._samples_for_us(0.25), 4)
|
||||||
|
self.assertEqual(inst._samples_for_us(0.75), 12)
|
||||||
|
inst.samplerate = 96000000
|
||||||
|
self.assertEqual(inst._samples_for_us(0.125), 12)
|
||||||
|
self.assertEqual(inst._samples_for_us(0.75), 72)
|
||||||
|
inst.samplerate = None
|
||||||
|
self.assertEqual(inst._samples_for_us(0.75), 12) # 16 MHz fallback
|
||||||
|
|
||||||
|
def test_etu_glitch_floor_scales(self):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.samplerate = 96000000
|
||||||
|
# 12-sample spacings are sub-ETU glitches at 96 MHz (floor 72).
|
||||||
|
self.assertEqual(inst._etu_candidate([(12, 0), (12, 1)]),
|
||||||
|
(None, False))
|
||||||
|
inst.samplerate = 16000000
|
||||||
|
self.assertEqual(inst._etu_candidate([(12, 0), (12, 1)]),
|
||||||
|
(12, True))
|
||||||
|
|
||||||
|
|
||||||
class TestValidateT0Apdu(unittest.TestCase):
|
class TestValidateT0Apdu(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
@@ -158,14 +439,39 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
self._valid(b'\x00\xd6\x00\x00\x04\x11\x22\x33\x44\x90\x00')
|
self._valid(b'\x00\xd6\x00\x00\x04\x11\x22\x33\x44\x90\x00')
|
||||||
|
|
||||||
def test_valid_cla_interindustry_classes(self):
|
def test_valid_cla_interindustry_classes(self):
|
||||||
# Interindustry CLA digits 0x/1x/4x/5x are structurally valid
|
# Interindustry CLA (ISO 7816-4 §5.1.1): 000x xxxx 0x00-0x1F
|
||||||
# (ISO 7816-4): includes logical channels 0x01-0x03, secure
|
# (channels 0-3 in b2b1, SM 0x0C, chaining 0x10-0x1F) and
|
||||||
# messaging 0x0C, chaining 0x10, and the SM/structural 0x4x/0x5x.
|
# 01xx xxxx 0x40-0x5F (channels 4-19 without CLA-level SM:
|
||||||
for cla in (0x01, 0x02, 0x03, 0x0C, 0x10, 0x14, 0x44, 0x50, 0x54):
|
# b4..b1 = channel-4). 0x20-0x3F is reserved by ISO; the
|
||||||
|
# SM-indicating 0x60-0x6F/0x70-0x7F stay rejected -- see
|
||||||
|
# test_invalid_cla_untraced_variants.
|
||||||
|
for cla in (0x01, 0x02, 0x03, 0x0C, 0x10, 0x14,
|
||||||
|
0x40, 0x44, 0x4F, 0x50, 0x54):
|
||||||
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
with self.subTest(cla=cla):
|
with self.subTest(cla=cla):
|
||||||
self._valid(pkt)
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_valid_cla_uicc_classes(self):
|
||||||
|
# UICC extended classes (TS 102 221 §10.1.1 Table 10.4a): 'CX'/'EX'
|
||||||
|
# (0xC0-0xCF/0xE0-0xEF) carry the extended channels 4-19 in b4..b1
|
||||||
|
# (SM in b6). The legacy SIM/UICC values and 0xB0 (no pinned-spec
|
||||||
|
# basis, kept for compatibility) are unchanged.
|
||||||
|
for cla in (0x80, 0x84, 0xA0, 0xB0,
|
||||||
|
0xC0, 0xC4, 0xCF, 0xE0, 0xE4, 0xEF):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_valid_cla_extended_channel_exchanges(self):
|
||||||
|
# Real higher-channel shapes from TS 102 221 Tables 10.4a/10.5:
|
||||||
|
# SELECT on channel 8 ('4X'), SELECT on channel 19 ('4X'),
|
||||||
|
# STATUS on channel 4 ('CX'), STATUS on channel 6 with SM
|
||||||
|
# ('EX' -- the UICC extended class accepts b6 = SM).
|
||||||
|
self._valid(b'\x44\xa4\x00\x04\x00\x90\x00')
|
||||||
|
self._valid(b'\x4f\xa4\x00\x04\x00\x90\x00')
|
||||||
|
self._valid(b'\xc0\xf2\x00\x00\x00\x90\x00')
|
||||||
|
self._valid(b'\xe2\xf2\x00\x00\x00\x90\x00')
|
||||||
|
|
||||||
def test_valid_cla_channel1_select_after_manage_channel(self):
|
def test_valid_cla_channel1_select_after_manage_channel(self):
|
||||||
# The command that follows MANAGE CHANNEL (00 70 00 00 01) opening
|
# The command that follows MANAGE CHANNEL (00 70 00 00 01) opening
|
||||||
# logical channel 1 legitimately uses CLA 0x01. This exact type
|
# logical channel 1 legitimately uses CLA 0x01. This exact type
|
||||||
@@ -174,9 +480,59 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
b'\xa0\x00\x00\x00\x63\x50\x4b\x43\x53\x2d\x31\x35'
|
b'\xa0\x00\x00\x00\x63\x50\x4b\x43\x53\x2d\x31\x35'
|
||||||
b'\x6a\x82')
|
b'\x6a\x82')
|
||||||
|
|
||||||
def test_invalid_cla_proprietary_unseen(self):
|
def test_invalid_cla_reserved_and_unseen(self):
|
||||||
# 0xC0 is proprietary but not a class these cards emit; stays invalid
|
# 0x20-0x3F (001x xxxx) is reserved by ISO 7816-4; 0x90-0x9F,
|
||||||
self._invalid(b'\xc0\xa4\x00\x04\x00\x90\x00', 'invalid CLA')
|
# 0xD0-0xDF and 0xF0-0xFF are not SIM/UICC classes ('11x0' needs
|
||||||
|
# b5 = 0). All stay invalid.
|
||||||
|
for cla in (0x20, 0x2F, 0x3F, 0x90, 0x9F, 0xD0, 0xDF, 0xF0, 0xFE):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._invalid(pkt, 'invalid CLA')
|
||||||
|
|
||||||
|
def test_invalid_cla_untraced_variants(self):
|
||||||
|
# Accepted-set precision (v1.9.3): the higher-channel codings that
|
||||||
|
# cost frame-start precision stay rejected. 0x60-0x6F/0x70-0x7F
|
||||||
|
# are the SM-indicating extended classes -- 0x60-0x6F overlaps
|
||||||
|
# printable ASCII ('a'-'o'), and the text byte 0x61 started a bogus
|
||||||
|
# frame on xiaomi_mi_a1_coldboot; 0x81-0x83/0x85-0x8F/0xA1-0xAF
|
||||||
|
# are the '8X'/'AX' standard-channel variants (channels 0-3) other
|
||||||
|
# than the legacy 0x80/0x84/0xA0, where the FCP byte 0xA5 did the
|
||||||
|
# same.
|
||||||
|
for cla in (0x60, 0x61, 0x6F, 0x70, 0x73, 0x7F,
|
||||||
|
0x81, 0x85, 0x8F, 0xA1, 0xA5, 0xAF):
|
||||||
|
pkt = bytes([cla, 0xa4, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(cla=cla):
|
||||||
|
self._invalid(pkt, 'invalid CLA')
|
||||||
|
|
||||||
|
def test_odd_ins_set_pinned(self):
|
||||||
|
# Exactly the odd codes the pinned specs define: the ISO 7816-4
|
||||||
|
# Table 4 pairs (0F ERASE BINARY, 21 VERIFY, 87 GENERAL
|
||||||
|
# AUTHENTICATE, A1 SEARCH BINARY, B1 READ BINARY, B3 READ RECORD,
|
||||||
|
# C3 ENVELOPE, CB GET DATA, D1 WRITE BINARY, D7 UPDATE BINARY,
|
||||||
|
# DB PUT DATA, DD UPDATE RECORD), TS 102 221 AUTHENTICATE 89, and
|
||||||
|
# TS 102 221 Table 10.5 MANAGE SECURE CHANNEL 73 / TRANSACT DATA 75
|
||||||
|
# (the v1.9.6 audit found these two missing).
|
||||||
|
self.assertEqual(self.pd.ODD_INS, frozenset((
|
||||||
|
0x0F, 0x21, 0x73, 0x75, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD)))
|
||||||
|
for ins in (0x73, 0x75):
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self.assertTrue(self.pd.plausible_ins(ins))
|
||||||
|
|
||||||
|
def test_vs_reader_mirror(self):
|
||||||
|
# tools/vs_reader.py duplicates the acceptance predicates for its
|
||||||
|
# standalone pcap analysis; the copies must not drift from pd.py
|
||||||
|
# (a drift would make its "suspicious CLA" / validity metrics and
|
||||||
|
# the --pcap-only verdict disagree with the decoder).
|
||||||
|
path = os.path.join(REPO, 'tools', 'vs_reader.py')
|
||||||
|
spec = importlib.util.spec_from_file_location('vs_reader', path)
|
||||||
|
vs = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(vs)
|
||||||
|
self.assertEqual(
|
||||||
|
vs.VALID_CLA,
|
||||||
|
frozenset(c for c in range(256) if self.pd.plausible_cla(c)))
|
||||||
|
self.assertEqual(vs.ODD_INS, self.pd.ODD_INS)
|
||||||
|
self.assertEqual(vs.EMBEDDED_CLA, self.pd.EMBEDDED_CLA)
|
||||||
|
|
||||||
def test_invalid_too_short(self):
|
def test_invalid_too_short(self):
|
||||||
self._invalid(b'\x00\xa4\x00\x04\x00\x90', 'too short')
|
self._invalid(b'\x00\xa4\x00\x04\x00\x90', 'too short')
|
||||||
@@ -187,11 +543,31 @@ class TestValidateT0Apdu(unittest.TestCase):
|
|||||||
def test_invalid_ins_zero(self):
|
def test_invalid_ins_zero(self):
|
||||||
self._invalid(b'\x00\x00\x00\x04\x00\x90\x00', 'invalid INS')
|
self._invalid(b'\x00\x00\x00\x04\x00\x90\x00', 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_ins_lsb_odd(self):
|
def test_valid_ins_odd_variants(self):
|
||||||
self._invalid(b'\x00\xa5\x00\x04\x00\x90\x00', 'invalid INS')
|
# Odd INS codes are accepted as the defined alternate variants:
|
||||||
|
# ISO 7816-4 Table 4 pairs (READ BINARY 'B1', ENVELOPE 'C3',
|
||||||
|
# UPDATE RECORD 'DD', ...) and TS 102 221 AUTHENTICATE '89'.
|
||||||
|
for ins in (0x0F, 0x21, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xD1, 0xD7, 0xDB, 0xDD):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x00, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._valid(pkt)
|
||||||
|
|
||||||
|
def test_invalid_ins_odd_undefined(self):
|
||||||
|
# Odd INS values with no defined command stay rejected: a
|
||||||
|
# mis-framed boundary exposes data bytes, and the text run
|
||||||
|
# "I M" (0x49 0x4D) started a bogus frame on xiaomi_phone_sample
|
||||||
|
# when every odd INS was accepted.
|
||||||
|
for ins in (0x03, 0x4D, 0x4F, 0xA5, 0xD3):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x00, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._invalid(pkt, 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_ins_sw_collision(self):
|
def test_invalid_ins_sw_collision(self):
|
||||||
self._invalid(b'\x00\x64\x00\x04\x00\x90\x00', 'invalid INS')
|
for ins in (0x64, 0x9F):
|
||||||
|
pkt = bytes([0x00, ins, 0x00, 0x04, 0x00, 0x90, 0x00])
|
||||||
|
with self.subTest(ins=ins):
|
||||||
|
self._invalid(pkt, 'invalid INS')
|
||||||
|
|
||||||
def test_invalid_sw1(self):
|
def test_invalid_sw1(self):
|
||||||
self._invalid(b'\x00\xa4\x00\x04\x00\xff\x00', 'invalid SW1')
|
self._invalid(b'\x00\xa4\x00\x04\x00\xff\x00', 'invalid SW1')
|
||||||
@@ -278,6 +654,7 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
self.pd = pd
|
self.pd = pd
|
||||||
self.inst = object.__new__(pd.Decoder)
|
self.inst = object.__new__(pd.Decoder)
|
||||||
self.inst.bit_samples = None
|
self.inst.bit_samples = None
|
||||||
|
self.inst._etu_measured = False
|
||||||
self.inst._atr_hunt_count = 0
|
self.inst._atr_hunt_count = 0
|
||||||
self.inst._atr_fail_total = 0
|
self.inst._atr_fail_total = 0
|
||||||
self.inst._hunt_etu_attempts = 0
|
self.inst._hunt_etu_attempts = 0
|
||||||
@@ -299,6 +676,22 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
self.assertEqual(self._hunt_round(0x00), 'recover')
|
self.assertEqual(self._hunt_round(0x00), 'recover')
|
||||||
self.assertEqual(self.inst._hunt_etu_attempts, 1)
|
self.assertEqual(self.inst._hunt_etu_attempts, 1)
|
||||||
|
|
||||||
|
def test_seeded_bit_samples_does_not_block_recovery(self):
|
||||||
|
# handle_atr pre-seeds bit_samples with the default-rate guess; the
|
||||||
|
# recovery must key off a real DATA measurement, not on bit_samples
|
||||||
|
# being None (the pre-seed used to defeat the v1.6.0 recovery).
|
||||||
|
self.inst.bit_samples = 1488
|
||||||
|
self.inst._etu_measured = False
|
||||||
|
self.assertEqual(self._hunt_round(0x00), 'retry')
|
||||||
|
self.assertEqual(self._hunt_round(0x00), 'recover')
|
||||||
|
|
||||||
|
def test_measured_etu_skips_recovery(self):
|
||||||
|
# A real measurement means the rate is known: no re-measurement.
|
||||||
|
self.inst.bit_samples = 1488
|
||||||
|
self.inst._etu_measured = True
|
||||||
|
actions = [self._hunt_round(0x00) for _ in range(6)]
|
||||||
|
self.assertNotIn('recover', actions)
|
||||||
|
|
||||||
def test_recovery_attempts_capped_then_hard_limit(self):
|
def test_recovery_attempts_capped_then_hard_limit(self):
|
||||||
actions = [self._hunt_round(0x00) for _ in range(200)]
|
actions = [self._hunt_round(0x00) for _ in range(200)]
|
||||||
actions = actions[:actions.index('resume_data') + 1]
|
actions = actions[:actions.index('resume_data') + 1]
|
||||||
@@ -337,6 +730,7 @@ class TestAtrHuntEscape(unittest.TestCase):
|
|||||||
|
|
||||||
def test_no_recovery_when_etu_known(self):
|
def test_no_recovery_when_etu_known(self):
|
||||||
self.inst.bit_samples = 100
|
self.inst.bit_samples = 100
|
||||||
|
self.inst._etu_measured = True
|
||||||
for _ in range(6):
|
for _ in range(6):
|
||||||
self.assertEqual(self._hunt_round(0x00), 'retry')
|
self.assertEqual(self._hunt_round(0x00), 'retry')
|
||||||
self.assertEqual(self.inst._hunt_etu_attempts, 0)
|
self.assertEqual(self.inst._hunt_etu_attempts, 0)
|
||||||
@@ -406,13 +800,24 @@ class TestEmbeddedExchanges(unittest.TestCase):
|
|||||||
# alcor_reader[46] (n=87): GET RESPONSE whose 80-byte payload holds a
|
# alcor_reader[46] (n=87): GET RESPONSE whose 80-byte payload holds a
|
||||||
# secured-packet run "40 70 70 61 15" 21 bytes before an SW-like byte;
|
# secured-packet run "40 70 70 61 15" 21 bytes before an SW-like byte;
|
||||||
# the reader follows with 00 c0 00 00 19 -- a clean single exchange.
|
# the reader follows with 00 c0 00 00 19 -- a clean single exchange.
|
||||||
# The structural 0x4x CLA must not turn payload bytes into a hit.
|
# The extended-channel 0x4x CLA must not turn payload bytes into a
|
||||||
|
# hit: the CONCAT detector is deliberately blind to those classes.
|
||||||
h = ('80c2000050d14e82028381060291978b4440048111227ff64070706115'
|
h = ('80c2000050d14e82028381060291978b4440048111227ff64070706115'
|
||||||
'3500350270000030151601252500000018ed15e8476126aa9cacbbe8f4'
|
'3500350270000030151601252500000018ed15e8476126aa9cacbbe8f4'
|
||||||
'735f962016e3c6e30a26af20c02bad2a34bdf49f152e263bc7390a6119')
|
'735f962016e3c6e30a26af20c02bad2a34bdf49f152e263bc7390a6119')
|
||||||
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
self.assertTrue(self.pd.validate_t0_apdu(bytes.fromhex(h))[0])
|
||||||
self.assertEqual(self._hits(h), [])
|
self.assertEqual(self._hits(h), [])
|
||||||
|
|
||||||
|
def test_embedded_cla_membership_pinned(self):
|
||||||
|
# CONCAT precision (v1.9.3): the detector's CLA set is deliberately
|
||||||
|
# NARROWER than plausible_cla() -- it must not follow the widened
|
||||||
|
# acceptance into the extended-channel classes (0x40-0x7F,
|
||||||
|
# 0xC0-0xEF) that false-hit on payload bytes (the Alcor vector
|
||||||
|
# above). Pin the exact membership.
|
||||||
|
expected = frozenset(list(range(0x00, 0x20)) +
|
||||||
|
[0x80, 0x84, 0xA0, 0xB0])
|
||||||
|
self.assertEqual(self.pd.EMBEDDED_CLA, expected)
|
||||||
|
|
||||||
def test_a55_u1_concat(self):
|
def test_a55_u1_concat(self):
|
||||||
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
|
# A55[296] (n=70): phantom-00 (buffer {01}) CLA + slip prefix.
|
||||||
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
|
h = ('000402a43f00612501c0000025c062238202782183023f00a5098001'
|
||||||
@@ -626,5 +1031,393 @@ class TestLineEventPayload(unittest.TestCase):
|
|||||||
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecodeStep(unittest.TestCase):
|
||||||
|
"""Byte-replay harness for the DATA-state gate chain in decode_step().
|
||||||
|
|
||||||
|
The rest of the suite exercises only pure functions and leaf methods;
|
||||||
|
these tests drive the real state machine with a scripted byte queue so
|
||||||
|
the CLA/INS acceptance rules are pinned on the framing path (gate
|
||||||
|
ordering, header read, one-byte re-frame, exchange assembly), not just
|
||||||
|
in the predicates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def _make(self, byte_seq):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.samplenum = 0
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._atr_parsed = True # keep 0x3B on the DATA path
|
||||||
|
inst._primed = True # skip the first-sample RST/VCC priming
|
||||||
|
inst._at_eof = lambda: False
|
||||||
|
inst.state = 'DATA'
|
||||||
|
inst.hasT0 = True
|
||||||
|
inst.hasT1 = False
|
||||||
|
inst.bit_samples = 100 # ETU known: skip _measure_etu()
|
||||||
|
inst._pps_speed_changed = False
|
||||||
|
inst._resync = False
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.emitted = []
|
||||||
|
inst.discarded = []
|
||||||
|
inst._bytes = collections.deque(byte_seq)
|
||||||
|
|
||||||
|
def pop_byte():
|
||||||
|
if inst._replay:
|
||||||
|
return inst._replay.popleft()
|
||||||
|
return inst._bytes.popleft() if inst._bytes else None
|
||||||
|
|
||||||
|
def peek_byte():
|
||||||
|
b = inst._replay[0] if inst._replay else (
|
||||||
|
inst._bytes[0] if inst._bytes else None)
|
||||||
|
inst.peeked_samplenum = inst.samplenum
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = pop_byte
|
||||||
|
inst.read_first_byte = pop_byte
|
||||||
|
inst._consume_first = pop_byte
|
||||||
|
inst.peek_byte = peek_byte
|
||||||
|
# The re-align / PPS / ATR / annotation paths are not under test.
|
||||||
|
inst.resync_idle = lambda: None
|
||||||
|
inst.handle_pps = lambda: None
|
||||||
|
inst.handle_atr = lambda pins: None
|
||||||
|
inst.put = lambda *a, **k: None
|
||||||
|
inst.log = lambda *a, **k: None
|
||||||
|
inst._note_discard = lambda value, reason, idle=False: \
|
||||||
|
inst.discarded.append((value, reason, idle))
|
||||||
|
inst._note_pre_sw = lambda value, reason: \
|
||||||
|
inst.discarded.append((value, reason, True))
|
||||||
|
inst.emit_packet = lambda sub_type, payload, ss, es, flags=0: \
|
||||||
|
inst.emitted.append((bytes(payload), flags))
|
||||||
|
# The vectors never depend on sample positions; a benign wait()
|
||||||
|
# keeps an unrelated wait call from crashing the harness.
|
||||||
|
inst.wait = lambda cond=None: [1] * 8
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def test_9e_9f_are_status_words(self):
|
||||||
|
# ISO 7816-3 10.3.3 Table 11: '9X' is SW1. The old code treated
|
||||||
|
# 9E/9F as an "extended-length" procedure byte and consumed SW2 as
|
||||||
|
# a bogus length, losing the status word.
|
||||||
|
for sw1 in (0x9e, 0x9f):
|
||||||
|
with self.subTest(sw1=sw1):
|
||||||
|
inst = self._make(bytes.fromhex('00a40000023f00')
|
||||||
|
+ bytes([sw1, 0x00]))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(
|
||||||
|
inst.emitted,
|
||||||
|
[(bytes.fromhex('00a40000023f00') + bytes([sw1, 0x00]), 0)])
|
||||||
|
|
||||||
|
def test_parity_error_flags_bad_fcs(self):
|
||||||
|
# A parity error is annotated and must also reach downstream as
|
||||||
|
# GSMTAP_FLAG_BAD_FCS on the emitted packet.
|
||||||
|
inst = self._make(bytes.fromhex('00a40000023f009000'))
|
||||||
|
orig = inst.read_byte
|
||||||
|
|
||||||
|
def read():
|
||||||
|
b = orig()
|
||||||
|
if b == 0x3f:
|
||||||
|
inst._parity_errors += 1
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = read
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(len(inst.emitted), 1)
|
||||||
|
self.assertTrue(inst.emitted[0][1] & self.pd.GSMTAP_FLAG_BAD_FCS)
|
||||||
|
|
||||||
|
def test_extended_channel_exchange_frames(self):
|
||||||
|
# 'CX' STATUS on channel 4 (v1.9.3): the whole exchange must be
|
||||||
|
# read and emitted clean.
|
||||||
|
inst = self._make(bytes.fromhex('c0f20000009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('c0f20000009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_ex_class_exchange_frames(self):
|
||||||
|
# 'EX' STATUS on channel 6 with SM (b6 = 1): same.
|
||||||
|
inst = self._make(bytes.fromhex('e2f20000009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('e2f20000009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_bit_dense_valid_cla_is_not_idle_noise(self):
|
||||||
|
# 0x5F was accepted by plausible_cla() all along but always
|
||||||
|
# discarded by the idle-noise gate before v1.9.3 (dead code): the
|
||||||
|
# framing path must consume it as a command byte.
|
||||||
|
inst = self._make(bytes.fromhex('5fa40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('5fa40004009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_text_byte_boundary_is_discarded_not_framed(self):
|
||||||
|
# xiaomi_mi_a1_coldboot regression byte: the menu-text 'a' (0x61)
|
||||||
|
# at a mis-framed boundary is a suspicious CLA (not a frame start
|
||||||
|
# and not idle noise) -- exactly one byte is consumed.
|
||||||
|
inst = self._make(bytes.fromhex('612000c000621e'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x61)
|
||||||
|
self.assertIn('suspicious CLA', inst.discarded[0][1])
|
||||||
|
self.assertEqual(bytes(inst._bytes), bytes.fromhex('2000c000621e'))
|
||||||
|
|
||||||
|
def test_invalid_ins_re_frame_recovers_command(self):
|
||||||
|
# A stray leading byte (0x1C) shifts the header; the invalid-INS
|
||||||
|
# window (INS 0x00) drops only that byte and re-frames the real
|
||||||
|
# command, which is emitted intact on the next step.
|
||||||
|
inst = self._make(bytes.fromhex('1c00a40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x1c)
|
||||||
|
self.assertIn('invalid INS', inst.discarded[0][1])
|
||||||
|
self.assertEqual(list(inst._replay), [0x00, 0xA4, 0x00, 0x04])
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('00a40004009000'), 0)])
|
||||||
|
|
||||||
|
def test_data_0x3b_is_not_an_atr_after_traffic(self):
|
||||||
|
# Once an APDU has been emitted, a DATA-state 0x3B is data misread
|
||||||
|
# at a frame boundary, not an ATR: it falls through to the
|
||||||
|
# suspicious-CLA discard instead of calling handle_atr.
|
||||||
|
inst = self._make(bytes.fromhex('3b80900000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = True
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(called, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x3B)
|
||||||
|
|
||||||
|
def test_data_0x3b_before_traffic_is_atr_candidate(self):
|
||||||
|
# Before any APDU the byte is still a legitimate ATR candidate
|
||||||
|
# (a capture starting between RST deassert and the ATR).
|
||||||
|
inst = self._make(bytes.fromhex('3b80900000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
|
def test_data_0x3f_before_traffic_is_atr_candidate(self):
|
||||||
|
# Inverse-convention ATRs (TS=0x3F) are candidates too.
|
||||||
|
inst = self._make(bytes.fromhex('3f100000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
|
def test_inverse_ts_raw_pattern_is_an_atr_candidate(self):
|
||||||
|
# A real inverse TS decodes as 0x03 through the direct reader; the
|
||||||
|
# raw moments 5-7 (LLL) mark it as the 0x3F candidate.
|
||||||
|
inst = self._make(bytes.fromhex('03100000'))
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst._apdu_seen = False
|
||||||
|
inst.bits = [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
called = []
|
||||||
|
inst.handle_atr = lambda pins: called.append(pins)
|
||||||
|
inst.decode_step()
|
||||||
|
self.assertEqual(len(called), 1)
|
||||||
|
|
||||||
|
def test_idle_noise_still_discarded(self):
|
||||||
|
# 0xFE (all-1s minus one bit) is not a valid class: unchanged
|
||||||
|
# idle-noise discard.
|
||||||
|
inst = self._make(bytes.fromhex('fea40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0xfe)
|
||||||
|
self.assertIn('idle-line noise', inst.discarded[0][1])
|
||||||
|
|
||||||
|
def test_immediate_sw_rejection_frames(self):
|
||||||
|
# A command rejected with SW1 SW2 instead of an ACK (ISO 7816-3
|
||||||
|
# §10.3.3) must not be read as command data; the exchange and the
|
||||||
|
# following one must frame separately.
|
||||||
|
inst = self._make(bytes.fromhex('00b00000106a82' +
|
||||||
|
'00a40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('00b00000106a82'), 0)])
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted[1],
|
||||||
|
(bytes.fromhex('00a40004009000'), 0))
|
||||||
|
|
||||||
|
def test_short_response_is_split_and_flagged(self):
|
||||||
|
# Lr < Le (6282 end-of-file): the SW inside the P3-byte read ends
|
||||||
|
# the response, and the packet is flagged BAD_FCS (it used to be a
|
||||||
|
# silent total loss).
|
||||||
|
inst = self._make(bytes.fromhex('00b0000010b012349000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('00b000001012349000'),
|
||||||
|
self.gs.GSMTAP_FLAG_BAD_FCS)])
|
||||||
|
|
||||||
|
def test_short_response_replays_following_exchange(self):
|
||||||
|
# A short response followed by another command: the split must
|
||||||
|
# replay the tail so the next exchange is framed intact.
|
||||||
|
inst = self._make(bytes.fromhex('00b0000010b012349000' +
|
||||||
|
'00a40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted[0],
|
||||||
|
(bytes.fromhex('00b000001012349000'),
|
||||||
|
self.gs.GSMTAP_FLAG_BAD_FCS))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted[1],
|
||||||
|
(bytes.fromhex('00a40004009000'), 0))
|
||||||
|
|
||||||
|
def test_le_256_response_data_is_kept(self):
|
||||||
|
# P3=0 in an outgoing command is Le=256 (ISO 7816-3 §10.3.2): the
|
||||||
|
# data after the ACK must be appended, not discarded — including a
|
||||||
|
# data byte that happens to equal INS.
|
||||||
|
payload = 'a5' * 8 + 'b0' + 'a5' * 7
|
||||||
|
inst = self._make(bytes.fromhex('00b0000000b0' + payload + '9000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('00b0000000' + payload + '9000'), 0)])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAtrHuntDetect(unittest.TestCase):
|
||||||
|
"""The ATR-hunt command-detect handoff in handle_atr(), driven by a
|
||||||
|
scripted byte queue (the hunt logic itself is not otherwise covered)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def _make(self, byte_seq):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.samplenum = 0
|
||||||
|
inst.ss = 0
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._samples_per_clock = 4.0 # skip _measure_clock_period()
|
||||||
|
inst.bit_samples = 100 # skip _compute_bit_samples()
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.out_python = 0
|
||||||
|
inst.options = {'protocol': 'T=0'}
|
||||||
|
inst._atr_parsed = False
|
||||||
|
inst.emitted = []
|
||||||
|
inst.emit_packet = lambda st, pl, ss, es, flags=0: \
|
||||||
|
inst.emitted.append((st, bytes(pl), flags))
|
||||||
|
inst.hasT0 = False # start() defaults; a handoff flips it
|
||||||
|
inst.hasT1 = False
|
||||||
|
inst._bytes = collections.deque(byte_seq)
|
||||||
|
|
||||||
|
def pop_byte():
|
||||||
|
if inst._replay:
|
||||||
|
return inst._replay.popleft()
|
||||||
|
return inst._bytes.popleft() if inst._bytes else None
|
||||||
|
|
||||||
|
def peek_byte():
|
||||||
|
b = inst._replay[0] if inst._replay else (
|
||||||
|
inst._bytes[0] if inst._bytes else None)
|
||||||
|
inst.peeked_samplenum = inst.samplenum
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = pop_byte
|
||||||
|
inst.read_first_byte = pop_byte
|
||||||
|
inst._consume_first = pop_byte
|
||||||
|
inst.peek_byte = peek_byte
|
||||||
|
inst.put = lambda *a, **k: None
|
||||||
|
inst.log = lambda *a, **k: None
|
||||||
|
inst.wait = lambda cond=None: [1] * 8
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def test_handoff_on_extended_channel_cla(self):
|
||||||
|
# 0xC4 (channel 8, no SM) + 0xA4 (SELECT): a command header, not a
|
||||||
|
# non-TS byte; it is replayed to the DATA state.
|
||||||
|
inst = self._make([0xC4, 0xA4])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
self.assertEqual(list(inst._replay), [0xC4])
|
||||||
|
self.assertTrue(inst.hasT0)
|
||||||
|
|
||||||
|
def test_handoff_on_odd_ins_89(self):
|
||||||
|
# The TS 102 221 AUTHENTICATE '89' form (v1.9.3): the second byte
|
||||||
|
# only qualifies because ODD_INS accepts it.
|
||||||
|
inst = self._make([0xC4, 0x89])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
self.assertEqual(list(inst._replay), [0xC4])
|
||||||
|
|
||||||
|
def test_no_handoff_on_basic_channel_cla(self):
|
||||||
|
# CLA 0x00 is excluded by design (pre-activation noise).
|
||||||
|
inst = self._make([0x00, 0xA4])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_no_handoff_on_invalid_ins(self):
|
||||||
|
# 0x61 is not a valid INS: the pair stays on the hunt path.
|
||||||
|
inst = self._make([0xC4, 0x61])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_eof_after_plausible_cla_is_clean(self):
|
||||||
|
# Capture ends right after a plausible CLA byte: the hunt must
|
||||||
|
# return cleanly. It used to call plausible_ins(None) -> TypeError
|
||||||
|
# (seen only by decode()'s crash protection).
|
||||||
|
inst = self._make([0xC4])
|
||||||
|
inst.handle_atr(None) # must not raise
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_bare_ts_t0_is_not_committed_as_an_atr(self):
|
||||||
|
# A data byte 0x3B followed by 0x00 looks like a bare TS+T0 ATR.
|
||||||
|
# It must not be emitted or force clock_skip=372 (a mid-session
|
||||||
|
# data 0x3B used to fabricate an ATR).
|
||||||
|
inst = self._make([0x3B, 0x00])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertFalse(inst._atr_parsed)
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
|
||||||
|
def test_truncated_atr_ta_is_clean(self):
|
||||||
|
# T0 says TA1 present but the capture ends: the parse must return
|
||||||
|
# without committing an ATR (or raising on the None byte).
|
||||||
|
inst = self._make([0x3B, 0x10])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertFalse(inst._atr_parsed)
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
|
||||||
|
def test_inverse_ts_pattern_is_parsed(self):
|
||||||
|
# 0x03 + the raw inverse TS pattern must be committed as 0x3F and
|
||||||
|
# switch the readers to the inverse convention.
|
||||||
|
inst = self._make([0x03, 0x10, 0x95])
|
||||||
|
inst.bits = [0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertTrue(inst._atr_parsed)
|
||||||
|
self.assertTrue(inst._inverse_convention)
|
||||||
|
self.assertEqual(inst.ATR, [0x3f, 0x10, 0x95])
|
||||||
|
self.assertEqual(len(inst.emitted), 1)
|
||||||
|
|
||||||
|
def test_atr_with_parity_error_is_ignored(self):
|
||||||
|
# A real ATR is parity-clean. A data run that parses as an ATR but
|
||||||
|
# has a parity error must not commit the rate (e.g. test_8's
|
||||||
|
# fabricated 3b80900000).
|
||||||
|
inst = self._make([0x3B, 0x10, 0x95])
|
||||||
|
orig = inst.read_byte
|
||||||
|
|
||||||
|
def read():
|
||||||
|
b = orig()
|
||||||
|
if b == 0x10:
|
||||||
|
inst._parity_errors += 1
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = read
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertFalse(inst._atr_parsed)
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Executable
+216
@@ -0,0 +1,216 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the GitHub release archives from a git ref.
|
||||||
|
# Usage: ./tools/build_release.sh [REF]
|
||||||
|
#
|
||||||
|
# REF defaults to HEAD. The archives carry an explicit path allowlist: the
|
||||||
|
# decoder, the setup/start scripts, tools/vs_reader.py, the two READMEs, the
|
||||||
|
# license and the images the READMEs reference. The allowlist is passed to
|
||||||
|
# `git archive` as its pathspec, so tests/, AGENTS.md, the CI config and the
|
||||||
|
# development tools never enter the release — and the repository's own
|
||||||
|
# (auto-generated) source archives are not affected.
|
||||||
|
#
|
||||||
|
# A v* tag must match the VERSION line in that ref's pd.py.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
die() { echo "build_release: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
REF="${1:-HEAD}"
|
||||||
|
git rev-parse --verify "$REF^{commit}" >/dev/null 2>&1 || die "unknown ref: $REF"
|
||||||
|
|
||||||
|
VERSION="$(git show "$REF:pd.py" | sed -n "s/^VERSION = '\([0-9][0-9.]*\)'.*/\1/p")"
|
||||||
|
[ -n "$VERSION" ] || die "cannot read VERSION from $REF:pd.py"
|
||||||
|
|
||||||
|
case "$REF" in
|
||||||
|
v*)
|
||||||
|
[ "${REF#v}" = "$VERSION" ] || \
|
||||||
|
die "tag $REF does not match pd.py VERSION '$VERSION'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PREFIX="sigrok_iso7816_stream-${VERSION}"
|
||||||
|
|
||||||
|
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/sigrok_release.XXXXXX")"
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
|
||||||
|
# --- expected release contents (allowlist) -------------------------------
|
||||||
|
# The base runtime files plus every image referenced from the READMEs.
|
||||||
|
# Each README embeds its own language variant of the pipeline diagram, so the
|
||||||
|
# two diagrams are added explicitly instead of joining the shared-photo check.
|
||||||
|
LANG_RES="resources/diagram_en.svg resources/diagram_ru.svg"
|
||||||
|
README_RES="$(git show "$REF:README.md" \
|
||||||
|
| grep -oE '\]\(resources/[^)]+' | sed 's/](//' \
|
||||||
|
| grep -v -e '^resources/diagram_en\.svg$' -e '^resources/diagram_ru\.svg$' \
|
||||||
|
| LC_ALL=C sort -u || true)"
|
||||||
|
[ -n "$README_RES" ] || die "no resources/ images referenced from $REF:README.md"
|
||||||
|
README_RUS_RES="$(git show "$REF:README_RUS.md" \
|
||||||
|
| grep -oE '\]\(resources/[^)]+' | sed 's/](//' \
|
||||||
|
| grep -v -e '^resources/diagram_en\.svg$' -e '^resources/diagram_ru\.svg$' \
|
||||||
|
| LC_ALL=C sort -u || true)"
|
||||||
|
[ "$README_RES" = "$README_RUS_RES" ] || \
|
||||||
|
die "README.md and README_RUS.md reference different resources/ images"
|
||||||
|
for res in $LANG_RES; do
|
||||||
|
git cat-file -e "$REF:$res" 2>/dev/null || die "missing $res at $REF"
|
||||||
|
done
|
||||||
|
|
||||||
|
EXPECTED="$STAGE/expected.list"
|
||||||
|
{
|
||||||
|
printf '%s\n' \
|
||||||
|
__init__.py pd.py gsmtap_stream.py \
|
||||||
|
setup.sh start.sh setup.bat start.bat \
|
||||||
|
tools/vs_reader.py README.md README_RUS.md LICENSE
|
||||||
|
printf '%s\n' "$README_RES"
|
||||||
|
printf '%s\n' resources/diagram_en.svg resources/diagram_ru.svg
|
||||||
|
} | LC_ALL=C sort -u > "$EXPECTED"
|
||||||
|
|
||||||
|
PATHS=()
|
||||||
|
while IFS= read -r path; do
|
||||||
|
PATHS+=("$path")
|
||||||
|
done < "$EXPECTED"
|
||||||
|
|
||||||
|
# Build into the staging directory; dist/ is only touched after every check
|
||||||
|
# passes, so a failed build never leaves unverified archives behind.
|
||||||
|
git archive --format=tar.gz --prefix="${PREFIX}/" "$REF" \
|
||||||
|
-o "$STAGE/${PREFIX}.tar.gz" -- "${PATHS[@]}"
|
||||||
|
git archive --format=zip --prefix="${PREFIX}/" "$REF" \
|
||||||
|
-o "$STAGE/${PREFIX}.zip" -- "${PATHS[@]}"
|
||||||
|
|
||||||
|
# --- archive listings ----------------------------------------------------
|
||||||
|
list_archive() {
|
||||||
|
case "$1" in
|
||||||
|
*.zip)
|
||||||
|
if command -v unzip >/dev/null 2>&1; then
|
||||||
|
unzip -Z1 "$1"
|
||||||
|
else
|
||||||
|
python3 - "$1" <<'PY'
|
||||||
|
import sys, zipfile
|
||||||
|
with zipfile.ZipFile(sys.argv[1]) as z:
|
||||||
|
for name in z.namelist():
|
||||||
|
print(name)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
tar tzf "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_listing() {
|
||||||
|
# Strip the top-level prefix, drop directory entries, sort.
|
||||||
|
awk -v p="$PREFIX/" '
|
||||||
|
index($0, p) == 1 {
|
||||||
|
name = substr($0, length(p) + 1)
|
||||||
|
if (name != "" && name !~ /\/$/) print name
|
||||||
|
}' | LC_ALL=C sort
|
||||||
|
}
|
||||||
|
|
||||||
|
DEV_PATHS=(
|
||||||
|
tests/
|
||||||
|
AGENTS.md
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
examples/
|
||||||
|
tools/build_release.sh
|
||||||
|
tools/capture_baseline.sh
|
||||||
|
tools/compare_baseline.sh
|
||||||
|
tools/signal_diagnostic.py
|
||||||
|
)
|
||||||
|
|
||||||
|
check_archive_listing() {
|
||||||
|
local archive="$1" listing="$2" actual="$3" path
|
||||||
|
# Defense in depth: the pathspec already excludes these, but a future
|
||||||
|
# refactor must not silently ship them.
|
||||||
|
for path in "${DEV_PATHS[@]}"; do
|
||||||
|
if grep -qF "${PREFIX}/${path}" "$listing"; then
|
||||||
|
die "$archive unexpectedly contains dev-only path ${path}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
normalize_listing < "$listing" > "$actual"
|
||||||
|
if ! diff -u "$EXPECTED" "$actual" > "$STAGE/list.diff"; then
|
||||||
|
cat "$STAGE/list.diff" >&2
|
||||||
|
die "$archive contents do not match the release allowlist (expected vs actual above)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
list_archive "$STAGE/${PREFIX}.tar.gz" > "$STAGE/tar.list"
|
||||||
|
list_archive "$STAGE/${PREFIX}.zip" > "$STAGE/zip.list"
|
||||||
|
check_archive_listing "$STAGE/${PREFIX}.tar.gz" "$STAGE/tar.list" "$STAGE/tar.files"
|
||||||
|
check_archive_listing "$STAGE/${PREFIX}.zip" "$STAGE/zip.list" "$STAGE/zip.files"
|
||||||
|
|
||||||
|
# --- unpack and verify both archives -------------------------------------
|
||||||
|
mkdir -p "$STAGE/tar" "$STAGE/zip"
|
||||||
|
tar xzf "$STAGE/${PREFIX}.tar.gz" -C "$STAGE/tar"
|
||||||
|
TAR_DIR="$STAGE/tar/$PREFIX"
|
||||||
|
[ -f "$TAR_DIR/pd.py" ] || die "tar.gz did not unpack ${PREFIX}/pd.py"
|
||||||
|
|
||||||
|
if command -v unzip >/dev/null 2>&1; then
|
||||||
|
unzip -q "$STAGE/${PREFIX}.zip" -d "$STAGE/zip"
|
||||||
|
else
|
||||||
|
python3 - "$STAGE/${PREFIX}.zip" "$STAGE/zip" <<'PY'
|
||||||
|
import sys, zipfile
|
||||||
|
with zipfile.ZipFile(sys.argv[1]) as z:
|
||||||
|
z.extractall(sys.argv[2])
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
ZIP_DIR="$STAGE/zip/$PREFIX"
|
||||||
|
[ -f "$ZIP_DIR/pd.py" ] || die "zip did not unpack ${PREFIX}/pd.py"
|
||||||
|
|
||||||
|
check_tree() {
|
||||||
|
local src="$1" label="$2" file found
|
||||||
|
found="$(sed -n "s/^VERSION = '\([0-9][0-9.]*\)'.*/\1/p" "$src/pd.py")"
|
||||||
|
[ "$found" = "$VERSION" ] || \
|
||||||
|
die "$label: pd.py VERSION '$found', expected '$VERSION'"
|
||||||
|
python3 -m py_compile "$src/pd.py" "$src/gsmtap_stream.py" "$src/__init__.py" || \
|
||||||
|
die "$label: py_compile failed"
|
||||||
|
for file in setup.sh start.sh; do
|
||||||
|
bash -n "$src/$file" || die "$label: bash -n $file failed"
|
||||||
|
done
|
||||||
|
for file in setup.bat start.bat; do
|
||||||
|
python3 - "$src/$file" <<'PY' || die "$label: $file is not CRLF-terminated"
|
||||||
|
import sys
|
||||||
|
data = open(sys.argv[1], 'rb').read()
|
||||||
|
if b'\n' not in data or data.count(b'\n') != data.count(b'\r\n'):
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
done
|
||||||
|
for file in setup.sh start.sh; do
|
||||||
|
if grep -q $'\r' "$src/$file"; then
|
||||||
|
die "$label: $file contains CR (expected LF only)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_tree "$TAR_DIR" "tar.gz"
|
||||||
|
check_tree "$ZIP_DIR" "zip"
|
||||||
|
|
||||||
|
[ -x "$TAR_DIR/setup.sh" ] || die "setup.sh is not executable in the tar.gz"
|
||||||
|
[ -x "$TAR_DIR/start.sh" ] || die "start.sh is not executable in the tar.gz"
|
||||||
|
|
||||||
|
# --- checksums and publish ----------------------------------------------
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
SHA256=(sha256sum)
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
SHA256=(shasum -a 256)
|
||||||
|
else
|
||||||
|
die "neither sha256sum nor shasum is available"
|
||||||
|
fi
|
||||||
|
(cd "$STAGE" && "${SHA256[@]}" "${PREFIX}.tar.gz" "${PREFIX}.zip" > SHA256SUMS)
|
||||||
|
|
||||||
|
# Verified: replace any previous release files so a later `gh release upload
|
||||||
|
# dist/*` can never attach a stale archive.
|
||||||
|
mkdir -p dist
|
||||||
|
rm -f dist/sigrok_iso7816_stream-*.tar.gz \
|
||||||
|
dist/sigrok_iso7816_stream-*.zip \
|
||||||
|
dist/SHA256SUMS
|
||||||
|
mv "$STAGE/${PREFIX}.tar.gz" "$STAGE/${PREFIX}.zip" "$STAGE/SHA256SUMS" dist/
|
||||||
|
|
||||||
|
echo "Built release archives (VERSION $VERSION):"
|
||||||
|
du -h "dist/${PREFIX}.tar.gz" "dist/${PREFIX}.zip"
|
||||||
|
echo ""
|
||||||
|
echo "dist/SHA256SUMS:"
|
||||||
|
cat dist/SHA256SUMS
|
||||||
+32
-11
@@ -44,14 +44,18 @@ CAT_INS = {
|
|||||||
# GSMTAP_FLAG_BAD_FCS from gsmtap_stream.py
|
# GSMTAP_FLAG_BAD_FCS from gsmtap_stream.py
|
||||||
GSMTAP_FLAG_BAD_FCS = 0x01
|
GSMTAP_FLAG_BAD_FCS = 0x01
|
||||||
|
|
||||||
# Valid CLA values for ISO 7816 / 3GPP. Mirror of plausible_cla in pd.py:
|
# Valid CLA values for ISO 7816-4 / TS 102 221. Mirror of plausible_cla in
|
||||||
# interindustry structures (high nibble 0x0/0x1/0x4/0x5), which covers the
|
# pd.py: interindustry 0x00-0x1F (channels 0-3) and 0x40-0x5F (channels
|
||||||
# logical-channel classes 0x01-0x03 (e.g. the command after MANAGE CHANNEL
|
# 4-19 without CLA-level SM), the UICC extended classes 'CX'/'EX'
|
||||||
# opens channel 1), plus the proprietary SIM/UICC CAT classes.
|
# (0xC0-0xCF/0xE0-0xEF, channels 4-19), plus 0x80/0x84/0xA0/0xB0 (0xB0 has
|
||||||
|
# no pinned-spec basis; kept for backward compatibility). 0x60-0x6F
|
||||||
|
# ('6X': SM; overlaps printable ASCII 'a'-'o'), 0x70-0x7F and the '8X'/'AX'
|
||||||
|
# standard-channel variants stay rejected: stray data bytes with those
|
||||||
|
# classes started bogus frames on the mis-framed phone traces.
|
||||||
def _plausible_cla(cla):
|
def _plausible_cla(cla):
|
||||||
if not (cla & 0x80):
|
if not (cla & 0x80):
|
||||||
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
return (cla & 0xF0) in (0x00, 0x10, 0x40, 0x50)
|
||||||
return cla in (0x80, 0x84, 0xA0, 0xB0)
|
return (cla & 0xF0) in (0xC0, 0xE0) or cla in (0x80, 0x84, 0xA0, 0xB0)
|
||||||
|
|
||||||
|
|
||||||
VALID_CLA = frozenset(cla for cla in range(256) if _plausible_cla(cla))
|
VALID_CLA = frozenset(cla for cla in range(256) if _plausible_cla(cla))
|
||||||
@@ -65,10 +69,12 @@ MAX_TPDU_LEN = 271
|
|||||||
# data region, followed by its P3 data bytes and an interior status word.
|
# data region, followed by its P3 data bytes and an interior status word.
|
||||||
# These constants bound detector precision (empirically tuned on all 10
|
# These constants bound detector precision (empirically tuned on all 10
|
||||||
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
# example traces: 0 hits on clean traces, every Samsung/xiaomi family hit).
|
||||||
# Mirror of pd.py: real command classes only -- the structural 0x4x/0x5x
|
# Pinned in both pd.py and here -- deliberately narrower than VALID_CLA:
|
||||||
# forms that _plausible_cla() tolerates only add payload false hits.
|
# the extended-channel classes false-hit on payload bytes (the Alcor
|
||||||
|
# "40 70 70 61 15" secured-packet run). Only the command classes that
|
||||||
|
# really occur on these wires stay covered.
|
||||||
EMBEDDED_CLA = frozenset(
|
EMBEDDED_CLA = frozenset(
|
||||||
cla for cla in VALID_CLA if (cla & 0xF0) not in (0x40, 0x50))
|
list(range(0x00, 0x20)) + [0x80, 0x84, 0xA0, 0xB0])
|
||||||
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
# Commands that actually occur on these cards' bus (T=0 + STK/CAT).
|
||||||
EMBEDDED_INS = frozenset((
|
EMBEDDED_INS = frozenset((
|
||||||
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
0x12, 0x14, 0x20, 0x44, 0x70, 0x88,
|
||||||
@@ -89,8 +95,9 @@ def embedded_exchanges(buf):
|
|||||||
embedded inside a packet's data region (index 5 onward).
|
embedded inside a packet's data region (index 5 onward).
|
||||||
|
|
||||||
Pattern: header (CLA INS P1 P2 P3) + P3 data bytes + interior SW1/SW2
|
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
|
before the packet's own final status word. CLA must be one of the
|
||||||
valid (same rule as _plausible_cla); INS must be a real command byte;
|
pinned EMBEDDED_CLA classes (deliberately narrower than
|
||||||
|
_plausible_cla()); INS must be a real command byte;
|
||||||
SW1 is accepted within a small slack window after the nominal data end
|
SW1 is accepted within a small slack window after the nominal data end
|
||||||
and must be interior (trailing bytes follow it).
|
and must be interior (trailing bytes follow it).
|
||||||
"""
|
"""
|
||||||
@@ -134,12 +141,26 @@ def _plausible_sw(sw1):
|
|||||||
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||||
|
|
||||||
|
|
||||||
|
# Odd INS codes defined by the pinned specs: the alternate variants of the
|
||||||
|
# ISO 7816-4 Table 4 pairs plus TS 102 221 AUTHENTICATE '89'. Other odd
|
||||||
|
# values stay rejected (no command uses them; mis-framed boundary data
|
||||||
|
# bytes with odd INS started bogus frames on the phone traces).
|
||||||
|
ODD_INS = frozenset((
|
||||||
|
0x0F, 0x21, 0x73, 0x75, 0x87, 0x89, 0xA1, 0xB1, 0xB3,
|
||||||
|
0xC3, 0xCB, 0xDB, 0xD1, 0xD7, 0xDD,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
def _plausible_ins(ins):
|
def _plausible_ins(ins):
|
||||||
|
# Mirror of plausible_ins in pd.py: 0x00 is invalid and 0x6x/0x9x
|
||||||
|
# collide with status words; odd INS values are accepted only as the
|
||||||
|
# defined alternate command variants (READ BINARY 'B1', AUTHENTICATE
|
||||||
|
# '89', ...).
|
||||||
if ins == 0x00:
|
if ins == 0x00:
|
||||||
return False
|
return False
|
||||||
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
if (ins & 0xF0) == 0x60 or (ins & 0xF0) == 0x90:
|
||||||
return False
|
return False
|
||||||
if (ins & 0x01) != 0:
|
if (ins & 0x01) != 0 and ins not in ODD_INS:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user