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:
catarrh committed 2026-10-20 17:07:02 +03:00
1 parent 883033e712
commit b574f900a1
6 files changed
+294 -98

No files matched your search

+60 -17
View File
@@ -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 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.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 ### 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 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 ### 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. The version is printed to the log on decoder startup.
### Testing after decoder changes ### 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 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).
- **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`). 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 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
@@ -1137,11 +1179,12 @@ discarded ideas. "Proven" = kept in `pd.py`; "Rejected" = do not re-add.
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
reverted read-until-SW from v1.1.0).
- **1.5-ETU start-bit guard in `wait_data_falling`.** `min_high = 1.5 * - **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 bit_samples` distinguishes genuine inter-byte idle gaps (≥2 ETU per ISO
7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment 7816-3) from intra-byte HIGH periods (≤1 ETU). Prevents byte misalignment
+6
View File
@@ -230,6 +230,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
+6
View File
@@ -243,6 +243,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, а затем продвигает битовую
+158 -67
View File
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT, GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
GSMTAP_FLAG_BAD_FCS) GSMTAP_FLAG_BAD_FCS)
VERSION = '1.9.4' VERSION = '1.9.5'
@@ -2177,6 +2177,69 @@ class Decoder(srd.Decoder):
return False return False
return True 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): def decode_step(self):
'''Run one state-machine step. Returns False when decoding ends.''' '''Run one state-machine step. Returns False when decoding ends.'''
if self._at_eof(): if self._at_eof():
@@ -2238,6 +2301,7 @@ class Decoder(srd.Decoder):
self.handle_atr(self.wait({'skip': 0})) self.handle_atr(self.wait({'skip': 0}))
elif self.state == 'DATA': elif self.state == 'DATA':
packet = []; packet = [];
forced_bad = False
# Mid-session capture with an explicit protocol: there is no ATR to # 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 # 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() pb0 = self.read_byte()
if pb0 is None: if pb0 is None:
return True return True
if (pb0 == bIns or pb0 == (bIns ^ 0xFF)): if plausible_sw(pb0):
# ACK (full match or ~INS): case 2/4 (response data # Immediate status word: ISO 7816-3 §10.3.3 lets the
# follows). Read exactly P3 response bytes, then # card finish the command at the first procedure
# scan for SW1 SW2, skipping any turnaround idle # byte; a case-2 rejection ('6A82', '6CXX', ...)
# bytes (e.g. 0xFF) between data and SW. # carries no ACK and must not be read as data.
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):
break
self._note_pre_sw(
sw1, 'non-SW byte before status word '
'(P3-bounded response)')
if sw1 is None:
return True
sw2 = self.read_byte() sw2 = self.read_byte()
if sw2 is None: if sw2 is None:
return True return True
packet.append(sw1) # SW1
packet.append(sw2) # SW2
got_sw = True
else:
# Case 2/3/4: determine data direction from P3 and
# the first post-header byte.
packet.append(pb0) packet.append(pb0)
packet.append(sw2)
got_sw = True
elif pb0 == bIns:
if (p3 == 0): if (p3 == 0):
# P3=0 (Le=0): no command data. Card sends # P3=0 in an outgoing command is Le=256
# response data + SW, or SW directly (no ACK). # (ISO 7816-3 §10.3.2): the card ACKs and sends
if plausible_sw(pb0): # response data until the status word.
# pb0 is SW1: card sent SW directly. 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:
eof = True
break
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() sw2 = self.read_byte()
if sw2 is None: if sw2 is None:
return True return True
packet.append(sw2) packet.extend(resp)
packet.extend((s1, sw2))
got_sw = True got_sw = True
else: else:
# pb0 is response data: read until SW, buf = resp + ([s1] if s1 is not None else [])
# skipping turnaround idle bytes. split = None
for _ in range(MAX_TPDU_LEN): for j in range(len(buf) - 1):
b = self.read_byte() if not plausible_sw(buf[j]):
if b is None: continue
return True k = j + 2
if plausible_sw(b): if k >= len(buf) or plausible_cla(buf[k]):
sw2 = self.read_byte() split = j
if sw2 is None:
return True
packet.append(b)
packet.append(sw2)
got_sw = True
break break
self._note_pre_sw(b, if split is not None:
'non-SW byte in P3=0 ' packet.extend(buf[:split])
'response') 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 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: else:
# Case 3/4: P3 command-data bytes, then # Case 3/4: P3 command-data bytes, then
# procedure byte. # procedure byte.
@@ -2505,23 +2603,15 @@ class Decoder(srd.Decoder):
if pb is None: if pb is None:
return True return True
if (pb == bIns or pb == (bIns ^ 0xFF)): if (pb == bIns or pb == (bIns ^ 0xFF)):
# ACK (full match / ~INS): case 4 -> read response until SW, # ACK: case-4 response data follows.
# skipping turnaround idle bytes. # Append it until the status word (it
for _ in range(MAX_TPDU_LEN): # used to be discarded). INS means all
b = self.read_byte() # remaining data; ~INS is per byte.
if b is None: if pb == bIns:
return True if self._t0_read_data_until_sw(packet):
if plausible_sw(b):
sw2 = self.read_byte()
if sw2 is None:
return True
packet.append(b)
packet.append(sw2)
got_sw = True got_sw = True
break elif self._t0_read_response_data(packet, bIns):
self._note_pre_sw(b, got_sw = True
'non-SW byte in case-4 '
'response')
break break
elif (pb == (bIns ^ 0x01) elif (pb == (bIns ^ 0x01)
or pb == ((bIns ^ 0xFF) ^ 0x01)): or pb == ((bIns ^ 0xFF) ^ 0x01)):
@@ -2697,7 +2787,8 @@ class Decoder(srd.Decoder):
else: else:
self._idle_low = 0 self._idle_low = 0
self.emit_packet(GSMTAP_SIM_APDU, bytes(packet), 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')) self.log("PACKETEND", codecs.encode(bytes(packet), 'hex'))
else: else:
+18 -14
View File
@@ -1,7 +1,7 @@
=== Baseline captured 2026-10-18T02:13:20+03:00 === === Baseline captured 2026-10-20T16:56:58+03:00 ===
=== test_8 === === test_8 ===
('=== iso7816 decoder v1.9.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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.4 ===',) ('=== iso7816 decoder v1.9.5 ===',)
('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',)
@@ -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_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_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_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_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_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
@@ -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 test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
---------------------------------------------------------------------- ----------------------------------------------------------------------
Ran 95 tests in 0.028s Ran 99 tests in 0.032s
OK OK
+46
View File
@@ -880,6 +880,52 @@ class TestDecodeStep(unittest.TestCase):
self.assertEqual(inst.discarded[0][0], 0xfe) self.assertEqual(inst.discarded[0][0], 0xfe)
self.assertIn('idle-line noise', inst.discarded[0][1]) 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): class TestAtrHuntDetect(unittest.TestCase):
"""The ATR-hunt command-detect handoff in handle_atr(), driven by a """The ATR-hunt command-detect handoff in handle_atr(), driven by a