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
+236
-58
No files matched your search
+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()
|
||||
Reference in new issue
Block a user