v1.9.0: CLK rate in RST events; v1.9.1: T=0 NULL is not an SW1
v1.9.0 -- CLK frequency in RST events:
- RST events (sub_type 0x10) append the measured CLK frequency when known:
[direction, level, flags, clk_hz_be32], flags bit 0 = rate present; the
3-byte form is kept when the rate is not yet measured. Value is
samplerate / _samples_per_clock from the existing 16-edge measurement (PPS
changes F/D, not the CLK rate). VCC events unchanged. Backward
compatible (old consumers read the first two bytes only); consumers can
compute data_rate = clk_hz x D / F from the ATR/PPS Fi/Di.
- Unit tests for the payload codec; README/README_RUS/AGENTS updated.
v1.9.1 -- T=0 NULL is not an SW1:
- The 'scan for SW1' loops accepted any 0x6X byte, including the NULL
procedure byte 0x60 (card busy), so 'NULL + real SW1' was mis-framed as
the status word: the phone traces' SELECT MF came out as ...3f00 6061 —
real SW1 0x61 eaten as SW2, real SW2 0x29 left on the wire and discarded
as a suspicious CLA (desync cascade).
- plausible_sw() now accepts 0x61-0x6F and 0x90-0x9F only, per
ISO 7816-3:2006 10.3.3 Table 11 ('6X' or '9X', EXCEPT '60'; ISO 7816-4
enforces '60' as an invalid SW1) and UICC_SPECS.md 1.8. All seven
SW1-detection sites use plausible_sw(); NULLs met while scanning are
logged as rate-limited wait extensions (_note_pre_sw) instead of
suspicious bytes.
- tools/vs_reader.py::_plausible_sw() mirrors the fix; regression vector
for the 00a40004023f006061 artifact.
Validation: 74/74 unit tests; baseline regenerated and IDENTICAL (the SW
fix is count-neutral -- only the SW bytes change); all 9 example pcaps
regenerated -- vs_reader OK, zero APDUs ending in 60 61, SELECT MF now
61 29 / 61 25.
This commit is contained in:
1 parent
e5a2b7c83b
commit
250c09c9b7
7 files changed
+233
-55
No files matched your search
@@ -182,7 +182,11 @@ data), so the false-positive rate is very low. The checks are:
|
||||
`0x84`, `0xA0`, `0xB0`).
|
||||
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
||||
collision), and the least-significant bit is `0`.
|
||||
4. The penultimate byte (SW1) is in `0x6x` or `0x9x`.
|
||||
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
||||
`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-4 enforces `'60'` as an invalid SW1), so a "NULL + real SW1"
|
||||
pair must never be accepted as a status word.
|
||||
|
||||
The current validation deliberately does **not** enforce P3-based length
|
||||
constraints, because a card may reject a command and send the status word
|
||||
@@ -304,6 +308,58 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||
|
||||
### v1.9.1: T=0 NULL is not an SW1 (fixes bogus "60 61" APDU endings)
|
||||
|
||||
The "scan for SW1" loops accepted any byte with high nibble `0x6` as a
|
||||
status word, including the T=0 **NULL procedure byte `0x60`** (card busy /
|
||||
waiting-time extension). A card that holds the terminal with NULLs while
|
||||
busy was therefore mis-framed: on the phone/cold-boot traces the SELECT MF
|
||||
wire flow is `00 a4 00 04 02` → ACK `a4` → command data `3f 00` → ~1.4 s of
|
||||
NULL `60` → real SW `61 29`. The decoder took `60` as SW1 and the real
|
||||
`0x61` as SW2, emitting `…3f00 6061` and leaving the real SW2 (`0x29`) on
|
||||
the wire, where it was discarded as a suspicious CLA — and the bogus packet
|
||||
even passed `validate_t0_apdu()` because `plausible_sw()` accepted `0x60`.
|
||||
|
||||
- `plausible_sw()` now accepts `0x61`–`0x6F` and `0x90`–`0x9F` only.
|
||||
Authority: ISO 7816-3:2006 §10.3.3 Table 11 — "If the value is `'6X'`
|
||||
or `'9X'`, **except for `'60'`**, it is a SW1 byte"; the note adds that
|
||||
ISO 7816-4 enforces `'60'` as an invalid SW1. (UICC_SPECS.md §1.8
|
||||
classes: normal `'9000'`/`'61XX'`; warning `'62XX'`/`'63XX'`; execution
|
||||
error `'64XX'`–`'66XX'`; checking error `'67XX'`–`'6FXX'`.)
|
||||
- Every SW1-detection site now uses `plausible_sw()`: the edge ACK scan,
|
||||
the P3=0 direct-SW check and response scan, the case-4 response scan,
|
||||
the case-3 SW1 check, the unexpected-byte scan, and the ATR path.
|
||||
- NULLs met while scanning for the status word are logged as rate-limited
|
||||
wait extensions via the new `_note_pre_sw()` (cards emit them in bursts,
|
||||
so they must not flood the log or raise warnings); other non-SW bytes
|
||||
keep their call-site reason.
|
||||
- `tools/vs_reader.py::_plausible_sw()` mirrors the fix.
|
||||
- Tests: `plausible_sw` vectors updated (`0x60` now rejected; accepts
|
||||
start at `0x61`), plus a regression vector for the
|
||||
`00 a4 00 04 02 3f 00 60 61` artifact (invalid SW1 / desynced).
|
||||
|
||||
The fix is count-neutral (the same bytes are consumed; only the SW bytes
|
||||
change), so `tests/baseline.txt` differs only by the version line and the
|
||||
new test listing. 74/74 unit tests. VERSION -> 1.9.1.
|
||||
|
||||
### v1.9.0: CLK frequency in RST events
|
||||
|
||||
RST line events (sub_type 0x10) now append the measured CLK frequency when
|
||||
it is known: `[direction, level, flags, clk_hz_be32]`, with `flags` bit 0
|
||||
(`LINE_EVENT_FLAG_CLK_HZ`) set and the frequency as a big-endian uint32 in
|
||||
Hz (7-byte payload). An unknown rate keeps the 3-byte payload, so
|
||||
consumers that read only the first two bytes (e.g. pysniff's
|
||||
`decode_line_event`) are unaffected. The value is the existing 16-edge
|
||||
average (`samplerate / _samples_per_clock`, see `_measure_clock_period`);
|
||||
PPS changes F/D, not the CLK rate, so one measurement per session is
|
||||
enough. VCC events (0x11) are unchanged. Purpose: consumers can compute
|
||||
the actual data rate as `clk_hz × D / F` from the Fi/Di negotiated in the
|
||||
ATR/PPS. Ordering: the RST deassert event usually precedes the ATR event
|
||||
(the 10 ms hysteresis lands mid-ATR for full ATRs), but a short ATR can
|
||||
invert that, so consumers take the rate from any RST event. Wire
|
||||
extension is backward compatible; 73/73 unit tests; baseline regenerated
|
||||
and IDENTICAL (it tracks no RST payload metrics). VERSION -> 1.9.0.
|
||||
|
||||
### v1.8.0: PPS emitted as standard request + response packets
|
||||
|
||||
The PPS exchange was previously emitted as a single packet under sub_type
|
||||
@@ -559,9 +615,9 @@ window may be filtering real start bits.
|
||||
|------|---------|
|
||||
| ACK (== INS or == ~INS) | Full data match: card acknowledges and expects all P3 data bytes |
|
||||
| INS^0x01 or ~INS^0x01 | Single-byte match: card validates syntax but processes data one byte at a time |
|
||||
| 0x60 | NULL: card busy, wait for next procedure byte |
|
||||
| 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 (0x6x / 0x9x) | Status word: exchange complete |
|
||||
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
||||
|
||||
##### CLA (Class) Byte — Interindustry Structure
|
||||
|
||||
@@ -642,11 +698,13 @@ comments in `gsmtap_stream.py`). It deviates in two documented ways:
|
||||
`GSMTAP_FLAG_BAD_FCS` writes flags into the header `res` byte, which
|
||||
official gsmtap.h reserves. The `res`-byte carriage is this decoder's own
|
||||
extension (upstream simtrace2 never sets `res`); the semantics mirror
|
||||
simtrace2's per-message USB data flags.
|
||||
simtrace2's per-message USB data flags. 0x10 RST events additionally
|
||||
append the measured CLK frequency when known
|
||||
(`[direction, level, flags, clk_hz_be32]`; see "v1.9.0" below).
|
||||
|
||||
### Versioning
|
||||
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.8.0'`.
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.1'`.
|
||||
The version is printed to the log on decoder startup.
|
||||
|
||||
### Testing after decoder changes
|
||||
|
||||
@@ -83,7 +83,7 @@ GSMTAP header: version 2, type SIM (0x04), UDP port 4729.
|
||||
| 0x01 | ATR | raw ATR bytes |
|
||||
| 0x02 | PPS request | raw PPS request bytes |
|
||||
| 0x03 | PPS response | raw PPS response bytes |
|
||||
| 0x10 | RST event (custom) | `[direction, level, 0x00]` |
|
||||
| 0x10 | RST event (custom) | `[direction, level, flags, clk_hz_be32]` — CLK frequency appended when known |
|
||||
| 0x11 | VCC event (custom) | `[direction, level, 0x00]` |
|
||||
|
||||
Event payload byte semantics:
|
||||
@@ -96,6 +96,13 @@ Event payload byte semantics:
|
||||
(PPS request and response are emitted as separate packets). 0x10/0x11
|
||||
are custom extensions not defined in libosmocore's gsmtap.h.
|
||||
|
||||
RST events carry the measured CLK frequency when it is known: `flags`
|
||||
bit 0 is then set and the frequency follows as a big-endian uint32 in Hz
|
||||
(7-byte payload). When the rate is not yet measured the payload stays
|
||||
3 bytes, so consumers that read only the first two bytes are unaffected.
|
||||
Combined with the F/D negotiated in the ATR/PPS, the data rate is
|
||||
`clk_hz × D / F` bits/s.
|
||||
|
||||
Header extensions: flags such as `GSMTAP_FLAG_BAD_FCS` are written into
|
||||
the header `res` byte, which official gsmtap.h marks reserved (RFU).
|
||||
This carriage is this decoder's own extension (upstream simtrace2-sniff
|
||||
|
||||
+8
-1
@@ -86,7 +86,7 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
||||
| 0x01 | ATR | сырые байты ATR |
|
||||
| 0x02 | Запрос PPS | сырые байты запроса PPS |
|
||||
| 0x03 | Ответ PPS | сырые байты ответа PPS |
|
||||
| 0x10 | Событие RST (кастомное) | `[direction, level, 0x00]` |
|
||||
| 0x10 | Событие RST (кастомное) | `[direction, level, flags, clk_hz_be32]` — частота CLK добавляется, когда известна |
|
||||
| 0x11 | Событие VCC (кастомное) | `[direction, level, 0x00]` |
|
||||
|
||||
Семантика байтов полезной нагрузки:
|
||||
@@ -100,6 +100,13 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
||||
0x10/0x11 — кастомные расширения, не определённые в gsmtap.h из
|
||||
libosmocore.
|
||||
|
||||
События RST несут измеренную частоту CLK, когда она известна: тогда
|
||||
устанавливается бит 0 байта `flags`, а следом идёт частота как big-endian
|
||||
uint32 в герцах (полезная нагрузка 7 байт). Пока частота не измерена,
|
||||
нагрузка остаётся 3 байта, поэтому потребители, читающие только первые два
|
||||
байта, не затрагиваются. Вместе с F/D, согласованными в ATR/PPS, скорость
|
||||
передачи данных равна `clk_hz × D / F` бит/с.
|
||||
|
||||
Расширения заголовка: флаги вроде `GSMTAP_FLAG_BAD_FCS` пишутся в байт
|
||||
`res` заголовка, который официальный gsmtap.h помечает как зарезервированный
|
||||
(RFU). Это собственное расширение данного декодера (upstream simtrace2-sniff
|
||||
|
||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||
GSMTAP_FLAG_BAD_FCS)
|
||||
|
||||
VERSION = '1.8.0'
|
||||
VERSION = '1.9.1'
|
||||
|
||||
|
||||
|
||||
@@ -106,10 +106,17 @@ class pcap_udp_pkt():
|
||||
return len(self.h) + len(self.data)
|
||||
|
||||
def plausible_sw(sw1):
|
||||
'''T=0 status word first byte sanity check. SW1 is 0x9x (normal
|
||||
processing) or 0x6x (warning/error classes). Anything else means
|
||||
the byte stream was almost certainly mis-framed.'''
|
||||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||
'''T=0 status word first byte sanity check.
|
||||
|
||||
Per ISO 7816-3:2006 §10.3.3 Table 11, an SW1 byte is '6X' or '9X'
|
||||
EXCEPT '60': that value is the NULL procedure byte (card busy /
|
||||
waiting-time extension), and ISO 7816-4 enforces '60' as an invalid
|
||||
SW1. Accepting it mis-frames "NULL + real SW1" as the status word --
|
||||
the bogus "60 61" APDU ending, where the real SW1 (0x61) is eaten as
|
||||
SW2. The 6X classes are '61'..'6F' (normal '61XX'; warning '62XX'/
|
||||
'63XX'; execution error '64XX'-'66XX'; checking error '67XX'-'6FXX'),
|
||||
plus '9X' for normal processing.'''
|
||||
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||
|
||||
|
||||
def plausible_cla(cla):
|
||||
@@ -272,6 +279,29 @@ def embedded_exchanges(buf):
|
||||
return out
|
||||
|
||||
|
||||
# --- line-event payload extension (RST/VCC, sub_type 0x10/0x11) ---
|
||||
# The base payload is [direction, level, flags]; byte 2 used to be reserved
|
||||
# and always 0. Bit 0 of it now marks an appended CLK frequency (big-endian
|
||||
# uint32 Hz), so consumers that only read the first two bytes keep working.
|
||||
LINE_EVENT_FLAG_CLK_HZ = 0x01
|
||||
|
||||
|
||||
def line_event_payload(direction, level, clk_hz=None):
|
||||
'''Build a RST/VCC line-event payload (GSMTAP sub_type 0x10/0x11).
|
||||
|
||||
Base form: [direction, level, flags] (3 bytes). When `clk_hz` is
|
||||
known, flags bit 0 is set and the frequency follows as a big-endian
|
||||
uint32 in Hz, giving a 7-byte payload. Old consumers read only the
|
||||
first two bytes and ignore the rest, so the extension is backward
|
||||
compatible.'''
|
||||
flags = 0
|
||||
extra = b''
|
||||
if clk_hz:
|
||||
flags |= LINE_EVENT_FLAG_CLK_HZ
|
||||
extra = struct.pack('>I', int(clk_hz))
|
||||
return bytes([direction & 0xFF, level & 0xFF, flags]) + extra
|
||||
|
||||
|
||||
MAX_TPDU_LEN = 271 # ISO 7816-3 T=0: header(5) + proc + 255 + SW(2) < 271
|
||||
IDLE_RESYNC_ETU = 4 # idle bit-periods marking an exchange boundary
|
||||
MAX_RESYNC_ATTEMPTS = 1024 # bail out if no idle gap found in that many tries
|
||||
@@ -405,6 +435,20 @@ class Decoder(srd.Decoder):
|
||||
[Ann.ANN_WARN, ["dropped 0x%02x: %s" % (value, reason)]])
|
||||
return value
|
||||
|
||||
def _note_pre_sw(self, value, reason):
|
||||
'''Log a byte consumed while scanning for the status word.
|
||||
|
||||
A T=0 NULL (0x60) is a legitimate wait extension -- cards emit them
|
||||
in bursts while busy -- so it is reported like idle noise
|
||||
(rate-limited) with its own reason instead of raising a warning.
|
||||
Other non-SW bytes keep the call-site reason.'''
|
||||
if value == 0x60:
|
||||
self._note_discard(value,
|
||||
'NULL procedure byte (card busy) before '
|
||||
'status word', idle=True)
|
||||
else:
|
||||
self._note_discard(value, reason, idle=(value in (0x00, 0xFF)))
|
||||
|
||||
def _consume_first(self):
|
||||
'''Consume the byte the last peek_byte() returned, removing it from
|
||||
whichever source it came from (the _replay buffer or the peeked
|
||||
@@ -668,6 +712,21 @@ class Decoder(srd.Decoder):
|
||||
return (cand, cand_sn, cand, lvl, now)
|
||||
return (None, 0, confirmed, lvl, now)
|
||||
|
||||
def _clk_hz(self):
|
||||
'''Return the measured CLK frequency in Hz, or None when unknown.
|
||||
|
||||
Uses the 16-edge average from _measure_clock_period(). The value is
|
||||
a session constant: PPS changes F/D (CLK cycles per bit), not the
|
||||
physical CLK rate. Native mode only (the measurement is skipped in
|
||||
sample_as_clock / detect modes).'''
|
||||
spc = self._samples_per_clock
|
||||
sr = self.samplerate
|
||||
if spc and sr:
|
||||
hz = int(round(float(sr) / float(spc)))
|
||||
if 1 <= hz <= 100000000:
|
||||
return hz
|
||||
return None
|
||||
|
||||
def line_events(self, pins):
|
||||
'''Check RST/VCC levels via level-stability hysteresis and emit
|
||||
events + annotations for confirmed level changes.
|
||||
@@ -684,7 +743,9 @@ class Decoder(srd.Decoder):
|
||||
if emit_lvl is not None:
|
||||
direction = 1 if emit_lvl == 0 else 0 # 1=asserted, 0=deasserted
|
||||
self.emit_packet(GSMTAP_SIM_RST_EVENT,
|
||||
bytes([direction, emit_lvl, 0]), edge_sn, edge_sn)
|
||||
line_event_payload(direction, emit_lvl,
|
||||
self._clk_hz()),
|
||||
edge_sn, edge_sn)
|
||||
text = ('RST asserted' if emit_lvl == 0 else 'RST deasserted')
|
||||
self.put(edge_sn, edge_sn, self.out_ann, [Ann.ANN_RST, [text]])
|
||||
self.log(text)
|
||||
@@ -2319,13 +2380,11 @@ class Decoder(srd.Decoder):
|
||||
sw1 = self.read_byte()
|
||||
if sw1 is None:
|
||||
return True
|
||||
if ((sw1 & 0xF0) == 0x60
|
||||
or (sw1 & 0xF0) == 0x90):
|
||||
if plausible_sw(sw1):
|
||||
break
|
||||
self._note_discard(sw1,
|
||||
'non-SW byte before status word '
|
||||
'(P3-bounded response)',
|
||||
idle=(sw1 in (0x00, 0xFF)))
|
||||
self._note_pre_sw(
|
||||
sw1, 'non-SW byte before status word '
|
||||
'(P3-bounded response)')
|
||||
if sw1 is None:
|
||||
return True
|
||||
sw2 = self.read_byte()
|
||||
@@ -2341,8 +2400,7 @@ class Decoder(srd.Decoder):
|
||||
if (p3 == 0):
|
||||
# P3=0 (Le=0): no command data. Card sends
|
||||
# response data + SW, or SW directly (no ACK).
|
||||
if ((pb0 & 0xF0) == 0x60
|
||||
or (pb0 & 0xF0) == 0x90):
|
||||
if plausible_sw(pb0):
|
||||
# pb0 is SW1: card sent SW directly.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
@@ -2356,8 +2414,7 @@ class Decoder(srd.Decoder):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
if ((b & 0xF0) == 0x60
|
||||
or (b & 0xF0) == 0x90):
|
||||
if plausible_sw(b):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
@@ -2365,10 +2422,9 @@ class Decoder(srd.Decoder):
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
self._note_discard(b,
|
||||
self._note_pre_sw(b,
|
||||
'non-SW byte in P3=0 '
|
||||
'response',
|
||||
idle=(b in (0x00, 0xFF)))
|
||||
'response')
|
||||
else:
|
||||
# Case 3/4: P3 command-data bytes, then
|
||||
# procedure byte.
|
||||
@@ -2388,8 +2444,7 @@ class Decoder(srd.Decoder):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
if ((b & 0xF0) == 0x60
|
||||
or (b & 0xF0) == 0x90):
|
||||
if plausible_sw(b):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
@@ -2397,10 +2452,9 @@ class Decoder(srd.Decoder):
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
self._note_discard(b,
|
||||
self._note_pre_sw(b,
|
||||
'non-SW byte in case-4 '
|
||||
'response',
|
||||
idle=(b in (0x00, 0xFF)))
|
||||
'response')
|
||||
break
|
||||
elif (pb == (bIns ^ 0x01)
|
||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||
@@ -2430,8 +2484,7 @@ class Decoder(srd.Decoder):
|
||||
return True
|
||||
packet.append(b)
|
||||
continue
|
||||
elif ((pb & 0xF0) == 0x60
|
||||
or (pb & 0xF0) == 0x90):
|
||||
elif plausible_sw(pb):
|
||||
# SW1: case 3, no response data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
@@ -2445,8 +2498,7 @@ class Decoder(srd.Decoder):
|
||||
# skip non-SW bytes (e.g. 0xFF
|
||||
# turnaround artifacts).
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
if ((pb & 0xF0) == 0x60
|
||||
or (pb & 0xF0) == 0x90):
|
||||
if plausible_sw(pb):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
@@ -2454,10 +2506,9 @@ class Decoder(srd.Decoder):
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
self._note_discard(pb,
|
||||
self._note_pre_sw(pb,
|
||||
'non-SW byte during '
|
||||
'unexpected-byte scan',
|
||||
idle=(pb in (0x00, 0xFF)))
|
||||
'unexpected-byte scan')
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return True
|
||||
@@ -2503,7 +2554,7 @@ class Decoder(srd.Decoder):
|
||||
if b is None:
|
||||
break
|
||||
packet.append(b)
|
||||
elif ((pb & 0xF0) == 0x60 or (pb & 0xF0) == 0x90):
|
||||
elif plausible_sw(pb):
|
||||
# SW1: append SW2; the exchange is complete.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
|
||||
+17
-11
@@ -1,7 +1,7 @@
|
||||
=== Baseline captured 2026-09-17T21:58:59+03:00 ===
|
||||
=== Baseline captured 2026-09-19T00:47:38+03:00 ===
|
||||
|
||||
=== test_8 ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('etu recovered (samples):', 1237)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== test_7 ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('etu recovered (samples):', 1236)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_phone_sample ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('etu recovered (samples):', 53)
|
||||
(11673977, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||
@@ -118,7 +118,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_mi_a1_coldboot ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -145,7 +145,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== 4gmodem_coldboot ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -186,7 +186,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -213,7 +213,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -240,7 +240,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -267,7 +267,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.8.0 ===',)
|
||||
('=== iso7816 decoder v1.9.1 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -368,6 +368,7 @@ test_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pul
|
||||
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
||||
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
||||
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
||||
test_is_desynced_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_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
||||
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
||||
@@ -392,6 +393,11 @@ test_build_packet_payload_passthrough (__main__.TestGsmtapFlags.test_build_packe
|
||||
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_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
||||
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
||||
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
||||
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
||||
test_payload_with_clk_rate (__main__.TestLineEventPayload.test_payload_with_clk_rate) ... ok
|
||||
test_rate_absent_when_unknown (__main__.TestLineEventPayload.test_rate_absent_when_unknown) ... ok
|
||||
test_accepts_full_hold_fall (__main__.TestNextStartFall.test_accepts_full_hold_fall) ... ok
|
||||
test_finds_fall_after_threshold (__main__.TestNextStartFall.test_finds_fall_after_threshold) ... ok
|
||||
test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall_exactly_at_threshold)
|
||||
@@ -419,6 +425,6 @@ test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla
|
||||
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 68 tests in 0.027s
|
||||
Ran 74 tests in 0.027s
|
||||
|
||||
OK
|
||||
+51
-4
@@ -79,23 +79,36 @@ class TestDesyncHelpers(unittest.TestCase):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def test_plausible_sw_accepts_valid_classes(self):
|
||||
for sw1 in list(range(0x60, 0x70)) + list(range(0x90, 0xA0)):
|
||||
# ISO 7816-3 §10.3.3 Table 11: SW1 is '6X' or '9X', except '60'
|
||||
# (the NULL procedure byte).
|
||||
for sw1 in list(range(0x61, 0x70)) + list(range(0x90, 0xA0)):
|
||||
self.assertTrue(self.pd.plausible_sw(sw1), hex(sw1))
|
||||
|
||||
def test_plausible_sw_rejects_garbage(self):
|
||||
for sw1 in (0x00, 0x01, 0x3B, 0x3F, 0xA0, 0xFF, 0x88, 0x5F):
|
||||
for sw1 in (0x00, 0x01, 0x3B, 0x3F, 0x5F, 0x60, 0x88, 0xA0, 0xFF):
|
||||
self.assertFalse(self.pd.plausible_sw(sw1), hex(sw1))
|
||||
|
||||
def test_max_tpdu_len(self):
|
||||
self.assertEqual(self.pd.MAX_TPDU_LEN, 271)
|
||||
|
||||
def test_is_desynced_clean_packets(self):
|
||||
# 7-byte: header(5) + proc(1) + SW2(1) ending in a 9x SW1
|
||||
self.assertFalse(self.pd.is_desynced(b'\x00\xa4\x00\x04\x02\x60\x90'))
|
||||
# header(5) + SW1 SW2 (P3=0 direct status word)
|
||||
self.assertFalse(self.pd.is_desynced(b'\x00\xa4\x00\x04\x00\x90\x00'))
|
||||
# ACK-style GET RESPONSE (header + proc + payload + SW 9000)
|
||||
self.assertFalse(self.pd.is_desynced(
|
||||
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
||||
|
||||
def test_is_desynced_null_sw1_is_invalid(self):
|
||||
# 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
|
||||
# word "60 61" (real SW1 eaten as SW2). It must be flagged, not
|
||||
# accepted (trace: SELECT MF -> NULL wait -> 61 29).
|
||||
pkt = b'\x00\xa4\x00\x04\x02\x3f\x00\x60\x61'
|
||||
valid, reason = self.pd.validate_t0_apdu(pkt)
|
||||
self.assertFalse(valid, 'NULL must not pass as SW1')
|
||||
self.assertIn('invalid SW1', reason)
|
||||
self.assertTrue(self.pd.is_desynced(pkt))
|
||||
|
||||
def test_is_desynced_short_fragment(self):
|
||||
# 5-byte header-only (INVALID-procedure residue) -- no SW read
|
||||
self.assertTrue(self.pd.is_desynced(b'\xa4\x3f\x00\x61\x25'))
|
||||
@@ -568,5 +581,39 @@ class TestEndOfStream(unittest.TestCase):
|
||||
self._decode_raising(SystemError).decode()
|
||||
|
||||
|
||||
class TestLineEventPayload(unittest.TestCase):
|
||||
"""RST/VCC line-event payloads (GSMTAP sub_type 0x10/0x11), including the
|
||||
CLK-frequency extension (v1.9.0)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gs, cls.pd = _load_iso7816()
|
||||
|
||||
def test_base_payload_without_rate(self):
|
||||
self.assertEqual(self.pd.line_event_payload(0, 1), b'\x00\x01\x00')
|
||||
self.assertEqual(self.pd.line_event_payload(1, 0, None), b'\x01\x00\x00')
|
||||
|
||||
def test_payload_with_clk_rate(self):
|
||||
p = self.pd.line_event_payload(0, 1, 4800000)
|
||||
self.assertEqual(len(p), 7)
|
||||
self.assertEqual(p[0], 0) # deasserted
|
||||
self.assertEqual(p[1], 1) # resulting level: high
|
||||
self.assertEqual(p[2], self.pd.LINE_EVENT_FLAG_CLK_HZ)
|
||||
self.assertEqual(int.from_bytes(p[3:7], 'big'), 4800000)
|
||||
|
||||
def test_rate_absent_when_unknown(self):
|
||||
self.assertEqual(len(self.pd.line_event_payload(1, 0, None)), 3)
|
||||
self.assertEqual(len(self.pd.line_event_payload(1, 0, 0)), 3)
|
||||
|
||||
def test_legacy_prefix_preserved(self):
|
||||
# Consumers that read only bytes 0-1 (pysniff decode_line_event)
|
||||
# must see the same direction/level as before.
|
||||
p = self.pd.line_event_payload(1, 0, 3579000)
|
||||
self.assertEqual(p[:2], b'\x01\x00')
|
||||
|
||||
def test_flag_constant(self):
|
||||
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+3
-1
@@ -126,7 +126,9 @@ def embedded_exchanges(buf):
|
||||
|
||||
|
||||
def _plausible_sw(sw1):
|
||||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||
# Mirror of plausible_sw in pd.py: SW1 is '6X'/'9X' except '60'
|
||||
# (the T=0 NULL procedure byte; ISO 7816-4 makes it an invalid SW1).
|
||||
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||
|
||||
|
||||
def _plausible_ins(ins):
|
||||
|
||||
Reference in new issue
Block a user