v1.9.5: T=0 framing — immediate SW, short responses, Le=256
- Decode the first post-header byte as SW1 before the ACK/data decision: a case-2 rejection (6A82/6CXX) carries no ACK and used to be read as command data, silently consuming the exchange and the next one. - Short responses (Lr < Le): after the P3-byte read, split the buffer at the first plausible SW1 followed by a plausible CLA, replay the tail and flag BAD_FCS; previously the SW scan discarded the real SW, merged the following exchanges and lost the exchange at EOF. - P3=0 is Le=256 for an outgoing command: keep the response data (read until SW) instead of discarding it, including data bytes equal to INS. - ~INS ACK: one data byte per ACK via the new helpers; case-4 inline data is appended (permissive; ISO 7816-3 §12.2 maps case 4 to GET RESPONSE). - tests: four TestDecodeStep vectors (immediate SW, short read + split + replay, Le=256 data incl. an INS-valued byte); 99/99. - AGENTS v1.9.5 + T=0 section, README/README_RUS bullets; baseline regenerated (xiaomi_mi_a1_coldboot 473 -> 475 APDUs, all else identical); compare_baseline: IDENTICAL.
This commit is contained in:
1 parent
883033e712
commit
b574f900a1
6 files changed
+293
-97
No files matched your search
@@ -333,6 +333,37 @@ 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.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
|
||||
@@ -883,7 +914,7 @@ append the measured CLK frequency when known
|
||||
|
||||
### Versioning
|
||||
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.4'`.
|
||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.5'`.
|
||||
The version is printed to the log on decoder startup.
|
||||
|
||||
### Testing after decoder changes
|
||||
@@ -1064,23 +1095,34 @@ no `0xff`/`0xfc` garbage.
|
||||
|
||||
Commands that carry command-data (e.g. `80 12 … d0`, P3=208) send those
|
||||
P3 bytes on the wire **before** the card's first procedure byte. The
|
||||
framing now distinguishes the cases by the card's first procedure byte:
|
||||
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
|
||||
terminal sent no command data; read `P3` response bytes (+SW).
|
||||
- Otherwise the byte is terminal command data → case 3/4: read `P3`
|
||||
- **SW1** (`0x61-0x6F`/`0x90-0x9F`) → the card finished with SW1 SW2 at the
|
||||
first procedure byte (ISO 7816-3 §10.3.3; a case-2 rejection such as
|
||||
`6A82`/`6CXX` carries no ACK).
|
||||
- **ACK (== INS)** → case 2/4: the terminal sent no command data. Read the
|
||||
`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`).
|
||||
On ACK (case 4) the response is read `P3` response bytes (+SW).
|
||||
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
|
||||
commands) into one C-APDU = header + command data, R-APDU = response + SW
|
||||
APDU, instead of fragmenting them across frames. It is gated to
|
||||
`_edge_read` (mid-session / gated-CLK captures) so ATR-based captures
|
||||
(`test_8` / `test_7`) keep the original framing unchanged and stay at
|
||||
0 garbage.
|
||||
APDU, instead of fragmenting them across frames.
|
||||
|
||||
All paths use P3-bounded reads (v1.1.1 reverted the read-until-SW
|
||||
experiment from v1.1.0 which mis-identified FCP `0x62` as SW1).
|
||||
P3-bounded reads are kept for case 2 (`Lr == Le`): the read-until-SW
|
||||
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
|
||||
|
||||
@@ -1137,11 +1179,12 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
|
||||
ATR/PPS/DATA + mid-session. Reliable; current architecture.
|
||||
- **`hasT0`/`hasT1` guards + init.** Prevents `AttributeError` and skips byte
|
||||
reading until a protocol is known. Reliable.
|
||||
- **T=0 case-3/4 ACK==INS discriminator.** First post-header byte == INS ⇒
|
||||
case 2/4 (response bounded by `P3`); else command data (read `P3`, then
|
||||
the procedure byte). Reliable for consolidating STK envelope commands.
|
||||
Gated to `_edge_read`. P3-bounded reads avoid FCP tag misidentification
|
||||
(v1.1.1 reverted read-until-SW from v1.1.0).
|
||||
- **T=0 procedure-byte classification (v1.9.5).** The first post-header
|
||||
byte is SW1 ⇒ immediate status word; ACK (== INS) ⇒ case 2/4 (P3
|
||||
response bytes, short-read split + BAD_FCS); ~INS ⇒ one byte per ACK;
|
||||
else command data (case 3/4). Reliable for consolidating STK envelope
|
||||
commands. P3-bounded reads avoid FCP tag misidentification (v1.1.1
|
||||
reverted read-until-SW from v1.1.0).
|
||||
- **1.5-ETU start-bit guard in `wait_data_falling`.** `min_high = 1.5 *
|
||||
bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
|
||||
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
|
||||
|
||||
@@ -230,6 +230,12 @@ filtered from stderr.
|
||||
recovering the real command. Every consumed-but-unemitted byte is
|
||||
explicitly logged (`discarded byte 0x…: reason`); idle 0x00/0xFF discards
|
||||
are rate-limited, non-idle discards always produce a decoder annotation.
|
||||
- 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
|
||||
edge once from the ATR's first frame, then advances bit timing with
|
||||
|
||||
@@ -243,6 +243,12 @@ USB у FX2 при длительной высокой частоте дискр
|
||||
(`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 декодер один раз калибрует число
|
||||
отсчётов на фронт CLK по первому кадру ATR, а затем продвигает битовую
|
||||
|
||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||
GSMTAP_FLAG_BAD_FCS)
|
||||
|
||||
VERSION = '1.9.4'
|
||||
VERSION = '1.9.5'
|
||||
|
||||
|
||||
|
||||
@@ -2177,6 +2177,69 @@ class Decoder(srd.Decoder):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _t0_read_data_until_sw(self, packet):
|
||||
'''Append every card byte until SW1 SW2, keeping the data.
|
||||
|
||||
Used after an INS ACK ("all remaining data follows", ISO 7816-3
|
||||
Table 11) and for P3=0 / case-4 response data: from this point on
|
||||
the card sends data bytes, then the status word. A plausible SW1
|
||||
ends the response; data bytes are appended verbatim. Returns True
|
||||
when the status word was read.'''
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return False
|
||||
if plausible_sw(pb):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return False
|
||||
packet.extend((pb, sw2))
|
||||
return True
|
||||
packet.append(pb)
|
||||
return False
|
||||
|
||||
|
||||
def _t0_read_response_data(self, packet, ins):
|
||||
'''Append the card's T=0 response data bytes until SW1 SW2.
|
||||
|
||||
Used for the per-byte (~INS) ACK form, where every data byte is
|
||||
followed by another procedure byte. Procedure bytes are consumed
|
||||
and never stored: NULL 0x60 / turnaround 0xFF are skipped, ~INS
|
||||
means "the next data byte only", and the deprecated INS^0x01 forms
|
||||
consume one data byte. An INS ACK ("all remaining data follows")
|
||||
switches to _t0_read_data_until_sw(). Any other byte is response
|
||||
data. Returns True when the status word was read.'''
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
pb = self.read_byte()
|
||||
if pb is None:
|
||||
return False
|
||||
if pb in (0x60, 0xFF):
|
||||
self._note_pre_sw(pb, 'NULL/turnaround before response data')
|
||||
continue
|
||||
if pb == ins:
|
||||
return self._t0_read_data_until_sw(packet)
|
||||
if pb == (ins ^ 0xFF):
|
||||
d = self.read_byte()
|
||||
if d is None:
|
||||
return False
|
||||
packet.append(d)
|
||||
continue
|
||||
if pb == (ins ^ 0x01) or pb == ((ins ^ 0xFF) ^ 0x01):
|
||||
d = self.read_byte()
|
||||
if d is None:
|
||||
return False
|
||||
packet.append(d)
|
||||
continue
|
||||
if plausible_sw(pb):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return False
|
||||
packet.extend((pb, sw2))
|
||||
return True
|
||||
packet.append(pb)
|
||||
return False
|
||||
|
||||
|
||||
def decode_step(self):
|
||||
'''Run one state-machine step. Returns False when decoding ends.'''
|
||||
if self._at_eof():
|
||||
@@ -2238,6 +2301,7 @@ class Decoder(srd.Decoder):
|
||||
self.handle_atr(self.wait({'skip': 0}))
|
||||
elif self.state == 'DATA':
|
||||
packet = [];
|
||||
forced_bad = False
|
||||
|
||||
# Mid-session capture with an explicit protocol: there is no ATR to
|
||||
# derive the bit period from, so recover it once from the live line
|
||||
@@ -2432,66 +2496,100 @@ class Decoder(srd.Decoder):
|
||||
pb0 = self.read_byte()
|
||||
if pb0 is None:
|
||||
return True
|
||||
if (pb0 == bIns or pb0 == (bIns ^ 0xFF)):
|
||||
# ACK (full match or ~INS): case 2/4 (response data
|
||||
# follows). Read exactly P3 response bytes, then
|
||||
# scan for SW1 SW2, skipping any turnaround idle
|
||||
# bytes (e.g. 0xFF) between data and SW.
|
||||
if plausible_sw(pb0):
|
||||
# Immediate status word: ISO 7816-3 §10.3.3 lets the
|
||||
# card finish the command at the first procedure
|
||||
# byte; a case-2 rejection ('6A82', '6CXX', ...)
|
||||
# carries no ACK and must not be read as data.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(pb0)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
elif pb0 == bIns:
|
||||
if (p3 == 0):
|
||||
# P3=0 in an outgoing command is Le=256
|
||||
# (ISO 7816-3 §10.3.2): the card ACKs and sends
|
||||
# response data until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# ACK: read the P3 response bytes, then the SW.
|
||||
# The card may return FEWER bytes than Le
|
||||
# (6282 end-of-file; Lr <= Le, TS 102 221
|
||||
# §10.1.6): if the byte after the read is not
|
||||
# SW1, split the buffer at the first plausible
|
||||
# SW1 followed by a plausible CLA and replay the
|
||||
# tail (flagged BAD_FCS).
|
||||
resp = []
|
||||
eof = False
|
||||
for _ in range(p3):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
sw1 = None
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
sw1 = self.read_byte()
|
||||
if sw1 is None:
|
||||
return True
|
||||
if plausible_sw(sw1):
|
||||
eof = True
|
||||
break
|
||||
self._note_pre_sw(
|
||||
sw1, 'non-SW byte before status word '
|
||||
'(P3-bounded response)')
|
||||
if sw1 is None:
|
||||
return True
|
||||
resp.append(b)
|
||||
s1 = None if eof else self.read_byte()
|
||||
if s1 is None:
|
||||
eof = True
|
||||
while s1 in (0x60, 0xFF):
|
||||
s1 = self.read_byte()
|
||||
if s1 is None:
|
||||
eof = True
|
||||
break
|
||||
if s1 is not None and plausible_sw(s1):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(sw1) # SW1
|
||||
packet.append(sw2) # SW2
|
||||
packet.extend(resp)
|
||||
packet.extend((s1, sw2))
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 2/3/4: determine data direction from P3 and
|
||||
# the first post-header byte.
|
||||
buf = resp + ([s1] if s1 is not None else [])
|
||||
split = None
|
||||
for j in range(len(buf) - 1):
|
||||
if not plausible_sw(buf[j]):
|
||||
continue
|
||||
k = j + 2
|
||||
if k >= len(buf) or plausible_cla(buf[k]):
|
||||
split = j
|
||||
break
|
||||
if split is not None:
|
||||
packet.extend(buf[:split])
|
||||
packet.extend(buf[split:split + 2])
|
||||
for t in reversed(buf[split + 2:]):
|
||||
self._replay.appendleft(t)
|
||||
got_sw = True
|
||||
forced_bad = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: split %d data byte(s) "
|
||||
"before SW1 0x%02x (Le %d)"
|
||||
% (split, buf[split], p3))
|
||||
else:
|
||||
packet.extend(buf)
|
||||
forced_bad = True
|
||||
self._resync = True
|
||||
self.log(
|
||||
"SHORT RESPONSE: no status word in "
|
||||
"%d byte(s) read for Le %d"
|
||||
% (len(buf), p3))
|
||||
elif pb0 == (bIns ^ 0xFF):
|
||||
# ~INS ACK: one data byte at a time. Read data
|
||||
# until the SW, consuming the procedure bytes in
|
||||
# between (ISO 7816-3 Table 11).
|
||||
if self._t0_read_response_data(packet, bIns):
|
||||
got_sw = True
|
||||
else:
|
||||
# Case 3/4: the first post-header byte is command
|
||||
# data (P3 = Lc).
|
||||
packet.append(pb0)
|
||||
if (p3 == 0):
|
||||
# P3=0 (Le=0): no command data. Card sends
|
||||
# response data + SW, or SW directly (no ACK).
|
||||
if plausible_sw(pb0):
|
||||
# pb0 is SW1: card sent SW directly.
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(sw2)
|
||||
# P3=0 and no procedure byte: treat the byte as
|
||||
# response data (Le=256, permissive) and read
|
||||
# until the status word.
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
else:
|
||||
# pb0 is response data: read until SW,
|
||||
# skipping turnaround idle bytes.
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
if plausible_sw(b):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
packet.append(sw2)
|
||||
got_sw = True
|
||||
break
|
||||
self._note_pre_sw(b,
|
||||
'non-SW byte in P3=0 '
|
||||
'response')
|
||||
else:
|
||||
# Case 3/4: P3 command-data bytes, then
|
||||
# procedure byte.
|
||||
@@ -2505,23 +2603,15 @@ class Decoder(srd.Decoder):
|
||||
if pb is None:
|
||||
return True
|
||||
if (pb == bIns or pb == (bIns ^ 0xFF)):
|
||||
# ACK (full match / ~INS): case 4 -> read response until SW,
|
||||
# skipping turnaround idle bytes.
|
||||
for _ in range(MAX_TPDU_LEN):
|
||||
b = self.read_byte()
|
||||
if b is None:
|
||||
return True
|
||||
if plausible_sw(b):
|
||||
sw2 = self.read_byte()
|
||||
if sw2 is None:
|
||||
return True
|
||||
packet.append(b)
|
||||
packet.append(sw2)
|
||||
# ACK: case-4 response data follows.
|
||||
# Append it until the status word (it
|
||||
# used to be discarded). INS means all
|
||||
# remaining data; ~INS is per byte.
|
||||
if pb == bIns:
|
||||
if self._t0_read_data_until_sw(packet):
|
||||
got_sw = True
|
||||
elif self._t0_read_response_data(packet, bIns):
|
||||
got_sw = True
|
||||
break
|
||||
self._note_pre_sw(b,
|
||||
'non-SW byte in case-4 '
|
||||
'response')
|
||||
break
|
||||
elif (pb == (bIns ^ 0x01)
|
||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||
@@ -2697,7 +2787,8 @@ class Decoder(srd.Decoder):
|
||||
else:
|
||||
self._idle_low = 0
|
||||
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet),
|
||||
es, self.samplenum)
|
||||
es, self.samplenum,
|
||||
flags=GSMTAP_FLAG_BAD_FCS if forced_bad else 0)
|
||||
|
||||
self.log("PACKETEND", codecs.encode(bytes(packet), 'hex'))
|
||||
else:
|
||||
|
||||
+18
-14
@@ -1,7 +1,7 @@
|
||||
=== Baseline captured 2026-10-18T02:13:20+03:00 ===
|
||||
=== Baseline captured 2026-10-20T16:56:58+03:00 ===
|
||||
|
||||
=== test_8 ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('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.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('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.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('etu recovered (samples):', 53)
|
||||
(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]
|
||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== xiaomi_mi_a1_coldboot ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -112,12 +112,12 @@ CHKSUM ERROR count: 0
|
||||
CONCAT APDU count: 0
|
||||
reader exchanges : 0
|
||||
capture ATRs : 1
|
||||
capture APDUs : 473
|
||||
capture APDUs : 475
|
||||
byte-exact matches (LCS) : 0
|
||||
reader exchanges not in capture (pre-capture / dropped): 0
|
||||
capture APDUs not in reader log:
|
||||
- proactive/CAT (unlogged by reader SDK): 10
|
||||
- valid exchanges (reader retries/dupes): 463
|
||||
- valid exchanges (reader retries/dupes): 465
|
||||
- GARBAGE (mis-framed/desync): 0
|
||||
|
||||
Degradation metrics:
|
||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== 4gmodem_coldboot ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== alcor_reader_sample ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||
|
||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||
('=== iso7816 decoder v1.9.4 ===',)
|
||||
('=== iso7816 decoder v1.9.5 ===',)
|
||||
('RST low at start: expecting cold-boot ATR',)
|
||||
iso7816-1: RST deasserted
|
||||
('RST deasserted',)
|
||||
@@ -359,7 +359,11 @@ test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_den
|
||||
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_le_256_response_data_is_kept (__main__.TestDecodeStep.test_le_256_response_data_is_kept) ... 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_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
||||
@@ -435,6 +439,6 @@ test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_vari
|
||||
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 95 tests in 0.028s
|
||||
Ran 99 tests in 0.032s
|
||||
|
||||
OK
|
||||
@@ -880,6 +880,52 @@ class TestDecodeStep(unittest.TestCase):
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user