Compare commits
7
Commits
3bf5cee222
...
v1.9.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cbafe715c | ||
|
|
6e6f465841 | ||
|
|
3e39032802 | ||
|
|
b574f900a1 | ||
|
|
883033e712 | ||
|
|
54dfc6586d | ||
|
|
9f42a88c01 |
No files matched your search
@@ -0,0 +1,63 @@
|
|||||||
|
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: 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)
|
||||||
|
# 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.
|
||||||
|
if ! gh release upload "$GITHUB_REF_NAME" dist/* --clobber; then
|
||||||
|
gh release create "$GITHUB_REF_NAME" dist/* --draft \
|
||||||
|
--title "sigrok_iso7816_stream $version" --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,47 @@ 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`, and every `resources/` image referenced from `README.md`. 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. **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. Release notes are 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 +1235,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 +1297,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 +1378,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
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ this decoder's `pcap_file=<path>` option).
|
|||||||
| 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 | Deglitch in the start-bit detector (2 samples at 16 MHz, scaled from the sample rate since v1.9.7) eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
|
||||||
|
| Mid-session ETU lock | The DATA-edge ETU estimator rejects guard-only (all-zero) and single-spacing bursts and invalidates a rate that fails parity, so a capture at a non-default F/D locks safely |
|
||||||
|
| ATR / resync guards | A DATA-state `0x3B`/`0x3F` is an ATR candidate only before the first APDU; parity-dirty or bare (TS+T0) frames are ignored, and `resync_idle` really waits for the idle level |
|
||||||
|
| Inverse convention | TS=0x3F ATRs follow ISO 7816-3 7.1: inverted data, msb first, start bit still LOW from an idle-HIGH line |
|
||||||
|
| T=1 blocks | NAD/PCB/LEN/INF/LRC decode with M-bit chaining; the I-block INF is emitted as the APDU, R/S blocks are not; a truncated capture ends cleanly |
|
||||||
|
| Parity flag | A packet containing a parity-error byte is emitted with the GSMTAP BAD_FCS flag (not just an annotation) |
|
||||||
|
| Rate-aware thresholds | Start-bit deglitch, guard floors and the ETU glitch floor scale from the capture sample rate instead of assuming 16 MHz |
|
||||||
|
|
||||||
## Decoder options
|
## Decoder options
|
||||||
|
|
||||||
@@ -111,13 +117,17 @@ data flags and are not part of the libosmocore spec.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Quick start after cloning (see Installation below):
|
Quick start (after installing — release download or source checkout; see
|
||||||
|
Installation below):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./setup.sh # check sigrok-cli, register decoder symlink
|
./setup.sh # check sigrok-cli, register decoder symlink
|
||||||
./start.sh # capture + GSMTAP stream to 127.0.0.1:4729
|
./start.sh # capture + GSMTAP stream to 127.0.0.1:4729
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On Windows the same two steps are `setup.bat` then `start.bat` (see
|
||||||
|
Installation below).
|
||||||
|
|
||||||
`start.sh` defaults: pins D5=CLK, D0=I/O, D4=RST, D2=VCC, 16 MHz samplerate.
|
`start.sh` defaults: pins D5=CLK, D0=I/O, D4=RST, D2=VCC, 16 MHz samplerate.
|
||||||
Override via args:
|
Override via args:
|
||||||
|
|
||||||
@@ -155,7 +165,8 @@ shift the whole ATR parse off-by-one). The decoder now:
|
|||||||
- never lets a decode exception kill the capture — errors are logged,
|
- never lets a decode exception kill the capture — errors are logged,
|
||||||
annotated, and the state machine resyncs (end-of-stream still exits
|
annotated, and the state machine resyncs (end-of-stream still exits
|
||||||
cleanly);
|
cleanly);
|
||||||
- bounds T=1 block-chain follow-up so garbage cannot loop forever.
|
- decodes T=1 blocks (NAD/PCB/LEN/INF/LRC) and bounds the M-bit chain so garbage
|
||||||
|
cannot loop forever.
|
||||||
|
|
||||||
After an accepted PPS the decoder **follows the speed switch**: in
|
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.
|
native CLK mode `clock_skip` becomes F/D CLK cycles per bit (e.g.
|
||||||
@@ -167,7 +178,7 @@ answer with a fresh ATR at the default 372 cycles/bit. The decoder
|
|||||||
**re-arms the ATR hunt on RST deassert**: state back to `FIND START`,
|
**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
|
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
|
an ATR instead of being mis-read as TPDU traffic at the PPS-negotiated
|
||||||
rate. A **1.5-ETU start-bit guard** additionally rejects the phantom
|
rate. A **half-ETU start-bit guard** (edge reader) additionally rejects the phantom
|
||||||
`0x00` an idle glitch can insert before that ATR. Requires
|
`0x00` an idle glitch can insert before that ATR. Requires
|
||||||
`rst_detect=true` (the `rst` channel assigned); without it a mid-session
|
`rst_detect=true` (the `rst` channel assigned); without it a mid-session
|
||||||
reset is still mis-read.
|
reset is still mis-read.
|
||||||
@@ -183,7 +194,7 @@ mis-read as a "premature abort", but there was never a device fault.
|
|||||||
Most of the early "noise" seen in captures (parity errors, mis-framed
|
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
|
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
|
hardware: re-seating the tap did nothing, while the decoder rewrite fixed
|
||||||
it completely (warm-reset re-arm, 1.5-ETU start-bit guard, 10 ms VCC
|
it completely (warm-reset re-arm, half-ETU start-bit guard, 10 ms VCC
|
||||||
debounce, T=0 exchange reassembly). A clean capture now decodes to zero
|
debounce, T=0 exchange reassembly). A clean capture now decodes to zero
|
||||||
garbage — see the `vs_reader.py` validation below.
|
garbage — see the `vs_reader.py` validation below.
|
||||||
|
|
||||||
@@ -221,8 +232,8 @@ filtered from stderr.
|
|||||||
- Desynced TPDUs (mis-framed exchanges) are kept but flagged with
|
- Desynced TPDUs (mis-framed exchanges) are kept but flagged with
|
||||||
`GSMTAP_FLAG_BAD_FCS` in the GSMTAP header `res` byte — visible in
|
`GSMTAP_FLAG_BAD_FCS` in the GSMTAP header `res` byte — visible in
|
||||||
Wireshark as a BAD-FCS warning.
|
Wireshark as a BAD-FCS warning.
|
||||||
- A T=0 header with a structurally invalid INS (ISO 7816-4 §12.1.1:
|
- A T=0 header with a structurally invalid INS (ISO 7816-4 §5.1.2:
|
||||||
`0x00`, `0x6x`/`0x9x`, or odd LSB) is almost always a one-byte shift from
|
`0x00` or `0x6x`/`0x9x`) is almost always a one-byte shift from
|
||||||
a stray leading byte (e.g. a leftover SW2 read after a PACKETEND).
|
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
|
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
|
byte become a bogus CLA and swallow the remaining dialog into one CONCAT
|
||||||
@@ -230,6 +241,12 @@ filtered from stderr.
|
|||||||
recovering the real command. Every consumed-but-unemitted byte is
|
recovering the real command. Every consumed-but-unemitted byte is
|
||||||
explicitly logged (`discarded byte 0x…: reason`); idle 0x00/0xFF discards
|
explicitly logged (`discarded byte 0x…: reason`); idle 0x00/0xFF discards
|
||||||
are rate-limited, non-idle discards always produce a decoder annotation.
|
are rate-limited, non-idle discards always produce a decoder annotation.
|
||||||
|
- A card may finish a command with SW1 SW2 at the first procedure byte
|
||||||
|
(ISO 7816-3 §10.3.3), including a case-2 command with P3 > 0: the SW is
|
||||||
|
framed as the status word, not read as command data. A response
|
||||||
|
**shorter than Le** is split at the status word and the packet is
|
||||||
|
flagged `BAD_FCS` (the following exchanges stay intact), and `P3=0`
|
||||||
|
(Le=256) response data is kept.
|
||||||
|
|
||||||
Performance: in native CLK mode the decoder calibrates samples per CLK
|
Performance: in native CLK mode the decoder calibrates samples per CLK
|
||||||
edge once from the ATR's first frame, then advances bit timing with
|
edge once from the ATR's first frame, then advances bit timing with
|
||||||
@@ -305,27 +322,61 @@ same restriction.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Linux
|
Both ways install the same decoder — pick one:
|
||||||
|
|
||||||
|
- **Download (recommended)** — a release archive. Nothing to clone and no git
|
||||||
|
history: extract it, run `setup`, run `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`). The example traces
|
||||||
|
(`examples/`) are gitignored and stay local in either case. The release
|
||||||
|
archives intentionally 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
|
```bash
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
sha256sum -c --ignore-missing SHA256SUMS # GNU coreutils (Linux)
|
||||||
cd sigrok_iso7816_stream
|
```
|
||||||
|
|
||||||
|
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
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`setup.sh` verifies sigrok-cli and the FX2 firmware package
|
`setup.sh` verifies sigrok-cli and the FX2 firmware package
|
||||||
(`sigrok-firmware-fx2lafw`) are installed, symlinks the repo into
|
(`sigrok-firmware-fx2lafw`) are installed, symlinks the project directory into
|
||||||
`~/.local/share/libsigrokdecode/decoders/iso7816` (user-local decoders shadow
|
`~/.local/share/libsigrokdecode/decoders/iso7816` (user-local decoders shadow
|
||||||
system-wide ones), and probes the connected device for its maximum supported
|
system-wide ones), and probes the connected device for its maximum supported
|
||||||
samplerate.
|
samplerate.
|
||||||
|
|
||||||
### Windows
|
#### Windows (download)
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
1. **sigrok-cli** Windows installer (0.7.2 x86_64) from
|
1. **sigrok-cli** Windows installer (0.7.2 x86_64) from
|
||||||
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads).
|
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads) — the tested
|
||||||
If it fails to start with error `0xc0150002`, install the
|
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)
|
[Visual C++ 2010 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=26999)
|
||||||
(`msvcr100.dll` is required by the bundled Python runtime).
|
(`msvcr100.dll` is required by the bundled Python runtime).
|
||||||
2. fx2lafw firmware ships bundled with the installer — no separate download.
|
2. fx2lafw firmware ships bundled with the installer — no separate download.
|
||||||
@@ -341,7 +392,40 @@ Driver setup (FX2 dongles need WinUSB, the vendor driver won't work):
|
|||||||
- Windows binds drivers per USB port when no serial number is present:
|
- Windows binds drivers per USB port when no serial number is present:
|
||||||
moving the dongle to another port may require repeating Zadig.
|
moving the dongle to another port may require repeating Zadig.
|
||||||
|
|
||||||
Then:
|
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).
|
||||||
|
|
||||||
|
### Linux (source checkout)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
|
cd sigrok_iso7816_stream
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
A fresh clone needs the same `./setup.sh` checks and symlink as above: it
|
||||||
|
verifies sigrok-cli and the FX2 firmware package, symlinks the project
|
||||||
|
directory into `~/.local/share/libsigrokdecode/decoders/iso7816` and probes
|
||||||
|
the device's maximum samplerate.
|
||||||
|
|
||||||
|
### 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
|
```bat
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
@@ -349,10 +433,15 @@ cd sigrok_iso7816_stream
|
|||||||
setup.bat
|
setup.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
`setup.bat` verifies sigrok-cli and bundled firmware, registers this repo's
|
`setup.bat` performs the same checks and junction registration as above.
|
||||||
decoder via a junction into `%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816`
|
`start.bat` supports the core option subset (channels, samplerate, clock,
|
||||||
(confirmed by `sigrok-cli -L` listing `iso7816`), and probes the device's
|
host/port, pcap, protocol, debug; `start.sh` has the full set) — the space
|
||||||
accepted samplerates.
|
form is the primary one, and `--clk=CH` also works:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
start.bat --clk D1 --data D6
|
||||||
|
start.bat --help
|
||||||
|
```
|
||||||
|
|
||||||
Capture works the same as on Linux:
|
Capture works the same as on Linux:
|
||||||
|
|
||||||
@@ -361,7 +450,8 @@ start.bat rem capture + GSMTAP stream
|
|||||||
```
|
```
|
||||||
|
|
||||||
Note: batch files need CRLF line endings; the repo enforces this via
|
Note: batch files need CRLF line endings; the repo enforces this via
|
||||||
`.gitattributes`, so a fresh clone on Windows is already correct.
|
`.gitattributes`, so a fresh clone on Windows is already correct (the release
|
||||||
|
zip ships the same CRLF files).
|
||||||
|
|
||||||
## RST/VCC detection notes and limitations
|
## RST/VCC detection notes and limitations
|
||||||
|
|
||||||
|
|||||||
+111
-22
@@ -58,7 +58,13 @@ GSMTAP pcap-файлы (включая 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 и предотвращает смещение байтов на быстрых телефонных трассах |
|
| Деглитч стартового бита | Деглитч в детекторе стартового бита (2 отсчёта при 16 МГц, с v1.9.7 масштабируется от частоты дискретизации) убирает LOW-глитчи короче ETU от артефактов подтяжки FX2LP и предотвращает смещение байтов на быстрых телефонных трассах |
|
||||||
|
| Захват ETU в середине сессии | Оценщик ETU по фронтам DATA отбрасывает пачки только со сторожевым интервалом (нулевые данные) и одиночные интервалы, а также сбрасывает скорость, не прошедшую проверку чётности, поэтому трасса с нестандартным F/D захватывается надёжно |
|
||||||
|
| Защита ATR / ресинхронизации | `0x3B`/`0x3F` в состоянии DATA считается кандидатом в ATR только до первого APDU; кадры с ошибкой чётности или голый TS+T0 игнорируются, а `resync_idle` действительно ждёт уровень простоя |
|
||||||
|
| Инверсная конвенция | ATR с TS=0x3F разбирается по ISO 7816-3 7.1: инверсные данные, msb первым, стартовый бит по-прежнему LOW при простое линии HIGH |
|
||||||
|
| Блоки T=1 | Разбор NAD/PCB/LEN/INF/LRC с цепочкой по биту M; INF I-блока выдаётся как APDU, R/S-блоки не выдаются; обрезанная запись завершается чисто |
|
||||||
|
| Флаг чётности | Пакет с байтом с ошибкой чётности выдаётся с флагом GSMTAP BAD_FCS (не только аннотацией) |
|
||||||
|
| Пороги от частоты | Деглитч стартового бита, пороги защиты и порог глитча ETU масштабируются от частоты дискретизации, а не считаются от 16 МГц |
|
||||||
|
|
||||||
## Опции декодера
|
## Опции декодера
|
||||||
|
|
||||||
@@ -115,13 +121,17 @@ flags) USB-протокола simtrace2 и не является частью с
|
|||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
Быстрый старт после клонирования (см. «Установка» ниже):
|
Быстрый старт (после установки — скачанный архив или клон репозитория;
|
||||||
|
см. «Установка» ниже):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./setup.sh # проверить sigrok-cli, зарегистрировать симлинк декодера
|
./setup.sh # проверить sigrok-cli, зарегистрировать симлинк декодера
|
||||||
./start.sh # запись + поток GSMTAP на 127.0.0.1:4729
|
./start.sh # запись + поток GSMTAP на 127.0.0.1:4729
|
||||||
```
|
```
|
||||||
|
|
||||||
|
В Windows те же два шага — `setup.bat`, затем `start.bat` (см. «Установка»
|
||||||
|
ниже).
|
||||||
|
|
||||||
Умолчания `start.sh`: выводы D5=CLK, D0=I/O, D4=RST, D2=VCC, частота
|
Умолчания `start.sh`: выводы D5=CLK, D0=I/O, D4=RST, D2=VCC, частота
|
||||||
дискретизации 16 МГц. Переопределяются аргументами:
|
дискретизации 16 МГц. Переопределяются аргументами:
|
||||||
|
|
||||||
@@ -160,8 +170,8 @@ flags) USB-протокола simtrace2 и не является частью с
|
|||||||
- никогда не даёт исключению при декодировании убить запись — ошибки
|
- никогда не даёт исключению при декодировании убить запись — ошибки
|
||||||
логируются, аннотируются, а конечный автомат ресинхронизируется
|
логируются, аннотируются, а конечный автомат ресинхронизируется
|
||||||
(конец потока по-прежнему завершается чисто);
|
(конец потока по-прежнему завершается чисто);
|
||||||
- ограничивает продолжение цепочки T=1-блоков, чтобы мусор не мог
|
- разбирает блоки T=1 (NAD/PCB/LEN/INF/LRC) и ограничивает цепочку по биту M,
|
||||||
зациклиться навсегда.
|
чтобы мусор не мог зациклиться навсегда.
|
||||||
|
|
||||||
После принятого PPS декодер **следует переключению скорости**: в режиме
|
После принятого PPS декодер **следует переключению скорости**: в режиме
|
||||||
native CLK `clock_skip` становится равным F/D тактов CLK на бит (например,
|
native CLK `clock_skip` становится равным F/D тактов CLK на бит (например,
|
||||||
@@ -175,7 +185,7 @@ native CLK `clock_skip` становится равным F/D тактов CLK
|
|||||||
`FIND START`, native-пропуск сбрасывается в 372, счётчик охоты очищается —
|
`FIND START`, native-пропуск сбрасывается в 372, счётчик охоты очищается —
|
||||||
поэтому повторный ATR разбирается как ATR, а не ошибочно читается как
|
поэтому повторный ATR разбирается как ATR, а не ошибочно читается как
|
||||||
TPDU-трафик на скорости, согласованной PPS. **Защита стартового бита в
|
TPDU-трафик на скорости, согласованной PPS. **Защита стартового бита в
|
||||||
1.5 ETU** дополнительно отбрасывает фантомный `0x00`, который глитч на
|
0.5 ETU** (edge-ридер) дополнительно отбрасывает фантомный `0x00`, который глитч на
|
||||||
холостом ходу может вставить перед этим ATR. Требуется `rst_detect=true`
|
холостом ходу может вставить перед этим ATR. Требуется `rst_detect=true`
|
||||||
(назначенный канал `rst`); без него сброс в середине сессии всё равно
|
(назначенный канал `rst`); без него сброс в середине сессии всё равно
|
||||||
будет прочитан неверно.
|
будет прочитан неверно.
|
||||||
@@ -193,7 +203,7 @@ TPDU-трафик на скорости, согласованной PPS. **За
|
|||||||
неверного кадрирования, дребезг линии VCC) была следствием плохо
|
неверного кадрирования, дребезг линии VCC) была следствием плохо
|
||||||
декодированного конечного автомата, а не железа: переподключение съёма
|
декодированного конечного автомата, а не железа: переподключение съёма
|
||||||
ничего не давало, а переписанный декодер полностью это вылечил (re-arm по
|
ничего не давало, а переписанный декодер полностью это вылечил (re-arm по
|
||||||
тёплому сбросу, защита стартового бита 1.5 ETU, антидребезг VCC 10 мс,
|
тёплому сбросу, защита стартового бита 0.5 ETU, антидребезг VCC 10 мс,
|
||||||
сборка обмена T=0). Чистая запись теперь декодируется с нулём мусора —
|
сборка обмена T=0). Чистая запись теперь декодируется с нулём мусора —
|
||||||
см. проверку `vs_reader.py` ниже.
|
см. проверку `vs_reader.py` ниже.
|
||||||
|
|
||||||
@@ -232,8 +242,8 @@ USB у FX2 при длительной высокой частоте дискр
|
|||||||
- Десинхронизированные TPDU (обмены с ошибкой кадрирования) сохраняются,
|
- Десинхронизированные TPDU (обмены с ошибкой кадрирования) сохраняются,
|
||||||
но помечаются `GSMTAP_FLAG_BAD_FCS` в байте `res` заголовка GSMTAP —
|
но помечаются `GSMTAP_FLAG_BAD_FCS` в байте `res` заголовка GSMTAP —
|
||||||
видно в Wireshark как предупреждение BAD-FCS.
|
видно в Wireshark как предупреждение BAD-FCS.
|
||||||
- Заголовок T=0 со структурно некорректным INS (ISO 7816-4 §12.1.1:
|
- Заголовок T=0 со структурно некорректным INS (ISO 7816-4 §5.1.2:
|
||||||
`0x00`, `0x6x`/`0x9x` или нечётный младший бит) почти всегда сдвинут на
|
`0x00` или `0x6x`/`0x9x`) почти всегда сдвинут на
|
||||||
один байт из-за лишнего ведущего байта (например, дочитанного остатка
|
один байт из-за лишнего ведущего байта (например, дочитанного остатка
|
||||||
SW2 после PACKETEND). Вместо молчаливого отбрасывания всего 5-байтового
|
SW2 после PACKETEND). Вместо молчаливого отбрасывания всего 5-байтового
|
||||||
окна (из-за чего следующий байт становился фиктивным CLA и проглатывал
|
окна (из-за чего следующий байт становился фиктивным CLA и проглатывал
|
||||||
@@ -243,6 +253,12 @@ USB у FX2 при длительной высокой частоте дискр
|
|||||||
(`discarded byte 0x…: reason`); отбрасывания холостых 0x00/0xFF
|
(`discarded byte 0x…: reason`); отбрасывания холостых 0x00/0xFF
|
||||||
ограничены по частоте, отбрасывания не-холостых байтов всегда дают
|
ограничены по частоте, отбрасывания не-холостых байтов всегда дают
|
||||||
аннотацию декодера.
|
аннотацию декодера.
|
||||||
|
- Карта может завершить команду словами SW1 SW2 на первом процедурном
|
||||||
|
байте (ISO 7816-3 §10.3.3), в том числе для case-2 команды с P3 > 0:
|
||||||
|
SW оформляется как статусное слово, а не читается как данные. Ответ
|
||||||
|
**короче Le** разделяется по статусному слову и пакет помечается
|
||||||
|
`BAD_FCS` (следующие обмены сохраняются); данные ответа при `P3=0`
|
||||||
|
(Le=256) сохраняются.
|
||||||
|
|
||||||
Производительность: в режиме native CLK декодер один раз калибрует число
|
Производительность: в режиме native CLK декодер один раз калибрует число
|
||||||
отсчётов на фронт CLK по первому кадру ATR, а затем продвигает битовую
|
отсчётов на фронт CLK по первому кадру ATR, а затем продвигает битовую
|
||||||
@@ -319,27 +335,60 @@ libsigrok ограничивает 12 МГц. Поэтому `start.sh`/`start.
|
|||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
### Linux
|
Оба способа ставят один и тот же декодер — выберите один:
|
||||||
|
|
||||||
|
- **Скачать (рекомендуется)** — архив релиза. Не нужно ничего клонировать и
|
||||||
|
нет истории 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
|
```bash
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
sha256sum -c --ignore-missing SHA256SUMS # GNU coreutils (Linux)
|
||||||
cd sigrok_iso7816_stream
|
```
|
||||||
|
|
||||||
|
В 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
|
./setup.sh
|
||||||
|
./start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`setup.sh` проверяет, что установлены sigrok-cli и пакет прошивки FX2
|
`setup.sh` проверяет, что установлены sigrok-cli и пакет прошивки FX2
|
||||||
(`sigrok-firmware-fx2lafw`), создаёт симлинк репозитория в
|
(`sigrok-firmware-fx2lafw`), создаёт симлинк каталога проекта в
|
||||||
`~/.local/share/libsigrokdecode/decoders/iso7816` (пользовательские
|
`~/.local/share/libsigrokdecode/decoders/iso7816` (пользовательские
|
||||||
декодеры перекрывают системные) и опрашивает подключённое устройство на
|
декодеры перекрывают системные) и опрашивает подключённое устройство на
|
||||||
предмет максимальной поддерживаемой частоты дискретизации.
|
предмет максимальной поддерживаемой частоты дискретизации.
|
||||||
|
|
||||||
### Windows
|
#### Windows (скачать)
|
||||||
|
|
||||||
Предварительные требования:
|
Предварительные требования:
|
||||||
|
|
||||||
1. **sigrok-cli** — установщик для Windows (0.7.2 x86_64) с
|
1. **sigrok-cli** — установщик для Windows (0.7.2 x86_64) с
|
||||||
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads).
|
[sigrok.org/wiki/Downloads](https://sigrok.org/wiki/Downloads) —
|
||||||
Если он не запускается с ошибкой `0xc0150002`, установите
|
протестированная версия. На этой же странице публикуются ночные сборки
|
||||||
|
для 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)
|
[Visual C++ 2010 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=26999)
|
||||||
(`msvcr100.dll` требуется встроенному Python-рантайму).
|
(`msvcr100.dll` требуется встроенному Python-рантайму).
|
||||||
2. Прошивка fx2lafw поставляется вместе с установщиком — отдельно скачивать не нужно.
|
2. Прошивка fx2lafw поставляется вместе с установщиком — отдельно скачивать не нужно.
|
||||||
@@ -355,7 +404,42 @@ cd sigrok_iso7816_stream
|
|||||||
- Windows привязывает драйверы к USB-порту, когда нет серийного номера:
|
- Windows привязывает драйверы к USB-порту, когда нет серийного номера:
|
||||||
перенос донгла в другой порт может потребовать повторить Zadig.
|
перенос донгла в другой порт может потребовать повторить 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Свежему клону нужны те же проверки и симлинк `./setup.sh`, что и выше: он
|
||||||
|
проверяет sigrok-cli и пакет прошивки FX2, создаёт симлинк каталога проекта
|
||||||
|
в `~/.local/share/libsigrokdecode/decoders/iso7816` и опрашивает устройство
|
||||||
|
на предмет максимальной частоты дискретизации.
|
||||||
|
|
||||||
|
### Windows (из исходников)
|
||||||
|
|
||||||
|
Предварительные требования Windows (sigrok-cli и Visual C++ 2010
|
||||||
|
Redistributable, встроенная прошивка fx2lafw и настройка драйвера
|
||||||
|
Zadig/WinUSB) — те же, что в разделе
|
||||||
|
[Windows (скачать)](#windows-скачать) выше.
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
git clone git@github.com:anttro/sigrok_iso7816_stream.git
|
||||||
@@ -363,11 +447,15 @@ cd sigrok_iso7816_stream
|
|||||||
setup.bat
|
setup.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
`setup.bat` проверяет sigrok-cli и встроенную прошивку, регистрирует декодер
|
`setup.bat` выполняет те же проверки и регистрацию junction, что и выше.
|
||||||
этого репозитория через junction в
|
`start.bat` поддерживает основное подмножество опций (каналы, частота
|
||||||
`%LOCALAPPDATA%\libsigrokdecode\decoders\iso7816` (подтверждается тем, что
|
дискретизации, clock, host/port, pcap, protocol, debug; полный набор — в
|
||||||
`sigrok-cli -L` показывает `iso7816`), и опрашивает устройство на предмет
|
`start.sh`) — основной формат с пробелом, а `--clk=CH` тоже работает:
|
||||||
поддерживаемых частот дискретизации.
|
|
||||||
|
```bat
|
||||||
|
start.bat --clk D1 --data D6
|
||||||
|
start.bat --help
|
||||||
|
```
|
||||||
|
|
||||||
Запись работает так же, как в Linux:
|
Запись работает так же, как в Linux:
|
||||||
|
|
||||||
@@ -376,7 +464,8 @@ start.bat rem запись + поток GSMTAP
|
|||||||
```
|
```
|
||||||
|
|
||||||
Примечание: bat-файлы требуют переводов строк CRLF; репозиторий следит за
|
Примечание: bat-файлы требуют переводов строк CRLF; репозиторий следит за
|
||||||
этим через `.gitattributes`, поэтому свежий клон в Windows уже корректен.
|
этим через `.gitattributes`, поэтому свежий клон в Windows уже корректен
|
||||||
|
(zip-архив релиза содержит те же файлы с CRLF).
|
||||||
|
|
||||||
## Заметки и ограничения по детектированию RST/VCC
|
## Заметки и ограничения по детектированию RST/VCC
|
||||||
|
|
||||||
|
|||||||
@@ -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
+205
@@ -0,0 +1,205 @@
|
|||||||
|
#!/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.
|
||||||
|
README_RES="$(git show "$REF:README.md" \
|
||||||
|
| grep -oE '\]\(resources/[^)]+' | sed 's/](//' | 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/](//' | LC_ALL=C sort -u || true)"
|
||||||
|
[ "$README_RES" = "$README_RUS_RES" ] || \
|
||||||
|
die "README.md and README_RUS.md reference different resources/ images"
|
||||||
|
|
||||||
|
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"
|
||||||
|
} | 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