feat: migrate sim framework from worktree to master
Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests) from .worktrees/sim-framework/ into pm3py/sim/. Import fixes: - 4 files: ..protocol/..transport → ..core.protocol/..core.transport - trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef - test_sim_pm3medium.py: flat imports → core.* - test_sim_trace_fmt.py: flat imports → core.* - Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*) 751 tests passing (686 sim + 65 core). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
419
tests/test_sim_14443a.py
Normal file
419
tests/test_sim_14443a.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""Tests for pm3py.sim.iso14443a — ISO 14443-A transponder and reader state machines."""
|
||||
import asyncio
|
||||
import pytest
|
||||
from bitarray import bitarray
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.iso14443a import (
|
||||
Tag14443A, Tag14443A_3, Tag14443A_4, Reader14443A,
|
||||
State14443A, REQA, WUPA, HLTA, CL1, CL2, CL3,
|
||||
)
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.get_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tag14443A — anticollision base (Part 2+3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTag14443AStates:
|
||||
"""Test basic state transitions."""
|
||||
|
||||
def test_initial_state_is_idle(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
assert tag.state == "IDLE"
|
||||
|
||||
def test_reqa_transitions_to_ready(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("26"))) # REQA
|
||||
assert resp is not None
|
||||
assert tag.state == "READY"
|
||||
|
||||
def test_reqa_returns_atqa(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04", atqa=b"\x44\x00")
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
assert resp.data == b"\x44\x00"
|
||||
|
||||
def test_wupa_transitions_to_ready(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("52"))) # WUPA
|
||||
assert resp is not None
|
||||
assert tag.state == "READY"
|
||||
|
||||
def test_idle_ignores_non_reqa_wupa(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9320")))
|
||||
assert resp is None
|
||||
assert tag.state == "IDLE"
|
||||
|
||||
|
||||
class TestTag14443AAnticollision4Byte:
|
||||
"""Test anticollision with 4-byte UID (single cascade level)."""
|
||||
|
||||
def test_anticol_cl1_returns_uid_and_bcc(self):
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
tag = Tag14443A_3(uid=uid)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26"))) # → READY
|
||||
|
||||
# ANTICOL CL1: SEL=0x93, NVB=0x20 (2 bytes known = SEL+NVB only)
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9320")))
|
||||
assert resp is not None
|
||||
# Response: 4 UID bytes + BCC
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
assert resp.data == uid + bytes([bcc])
|
||||
|
||||
def test_select_cl1_returns_sak_and_transitions_to_active(self):
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
tag = Tag14443A_3(uid=uid, sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26"))) # → READY
|
||||
|
||||
# SELECT CL1: SEL=0x93, NVB=0x70, UID(4), BCC
|
||||
select = b"\x93\x70" + uid + bytes([bcc])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(select)))
|
||||
assert resp is not None
|
||||
# SAK response: 1 byte
|
||||
assert resp.data[0] == 0x08
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
def test_select_wrong_uid_no_response(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
wrong_uid = b"\xFF\xFF\xFF\xFF"
|
||||
bcc = wrong_uid[0] ^ wrong_uid[1] ^ wrong_uid[2] ^ wrong_uid[3]
|
||||
select = b"\x93\x70" + wrong_uid + bytes([bcc])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(select)))
|
||||
assert resp is None
|
||||
|
||||
|
||||
class TestTag14443AAnticollision7Byte:
|
||||
"""Test anticollision with 7-byte UID (two cascade levels)."""
|
||||
|
||||
def test_cl1_returns_ct_plus_first3_with_cascade_sak(self):
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
tag = Tag14443A_3(uid=uid, sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
# ANTICOL CL1
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9320")))
|
||||
assert resp is not None
|
||||
# CL1: CT(0x88) + uid[0:3] + BCC
|
||||
ct_uid = b"\x88" + uid[0:3]
|
||||
bcc = ct_uid[0] ^ ct_uid[1] ^ ct_uid[2] ^ ct_uid[3]
|
||||
assert resp.data == ct_uid + bytes([bcc])
|
||||
|
||||
def test_select_cl1_sak_indicates_cascade(self):
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
tag = Tag14443A_3(uid=uid, sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
ct_uid = b"\x88" + uid[0:3]
|
||||
bcc = ct_uid[0] ^ ct_uid[1] ^ ct_uid[2] ^ ct_uid[3]
|
||||
select = b"\x93\x70" + ct_uid + bytes([bcc])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(select)))
|
||||
# SAK bit 2 set = cascade not complete
|
||||
assert resp.data[0] & 0x04 != 0
|
||||
assert tag.state == "READY" # still in anticollision, not ACTIVE
|
||||
|
||||
def test_cl2_returns_remaining_uid(self):
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
tag = Tag14443A_3(uid=uid, sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
# Complete CL1
|
||||
ct_uid = b"\x88" + uid[0:3]
|
||||
bcc1 = ct_uid[0] ^ ct_uid[1] ^ ct_uid[2] ^ ct_uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1]))))
|
||||
|
||||
# ANTICOL CL2
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9520")))
|
||||
assert resp is not None
|
||||
cl2_uid = uid[3:7]
|
||||
bcc2 = cl2_uid[0] ^ cl2_uid[1] ^ cl2_uid[2] ^ cl2_uid[3]
|
||||
assert resp.data == cl2_uid + bytes([bcc2])
|
||||
|
||||
def test_select_cl2_completes_with_final_sak(self):
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
tag = Tag14443A_3(uid=uid, sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
# CL1
|
||||
ct_uid = b"\x88" + uid[0:3]
|
||||
bcc1 = ct_uid[0] ^ ct_uid[1] ^ ct_uid[2] ^ ct_uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1]))))
|
||||
|
||||
# CL2 SELECT
|
||||
cl2_uid = uid[3:7]
|
||||
bcc2 = cl2_uid[0] ^ cl2_uid[1] ^ cl2_uid[2] ^ cl2_uid[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2]))))
|
||||
assert resp.data[0] == 0x08 # final SAK, no cascade bit
|
||||
assert resp.data[0] & 0x04 == 0
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
class TestTag14443AAnticollision10Byte:
|
||||
"""Test anticollision with 10-byte UID (three cascade levels)."""
|
||||
|
||||
def test_full_10byte_anticollision(self):
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A"
|
||||
tag = Tag14443A_3(uid=uid, sak=0x20)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
|
||||
# CL1: CT + uid[0:3]
|
||||
ct1 = b"\x88" + uid[0:3]
|
||||
bcc1 = ct1[0] ^ ct1[1] ^ ct1[2] ^ ct1[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9320")))
|
||||
assert resp.data == ct1 + bytes([bcc1])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct1 + bytes([bcc1]))))
|
||||
assert resp.data[0] & 0x04 != 0 # cascade
|
||||
|
||||
# CL2: CT + uid[3:6]
|
||||
ct2 = b"\x88" + uid[3:6]
|
||||
bcc2 = ct2[0] ^ ct2[1] ^ ct2[2] ^ ct2[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9520")))
|
||||
assert resp.data == ct2 + bytes([bcc2])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + ct2 + bytes([bcc2]))))
|
||||
assert resp.data[0] & 0x04 != 0 # cascade
|
||||
|
||||
# CL3: uid[6:10]
|
||||
cl3 = uid[6:10]
|
||||
bcc3 = cl3[0] ^ cl3[1] ^ cl3[2] ^ cl3[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("9720")))
|
||||
assert resp.data == cl3 + bytes([bcc3])
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x97\x70" + cl3 + bytes([bcc3]))))
|
||||
# Tag14443A_3 clears SAK bit 5, so 0x20 & ~0x20 = 0x00
|
||||
assert resp.data[0] == 0x00 # final SAK (Part 3, bit 5 cleared)
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
class TestTag14443AHalt:
|
||||
"""Test HALT behavior."""
|
||||
|
||||
def test_hlta_transitions_to_halt(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26"))) # → READY
|
||||
# Select to ACTIVE
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
|
||||
# HLTA
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("5000")))
|
||||
assert resp is None # no response to HLTA
|
||||
assert tag.state == "HALT"
|
||||
|
||||
def test_halted_tag_ignores_reqa(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
run(tag.handle_frame(RFFrame.from_hex("5000"))) # HALT
|
||||
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("26"))) # REQA
|
||||
assert resp is None
|
||||
assert tag.state == "HALT"
|
||||
|
||||
def test_halted_tag_wakes_on_wupa(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
run(tag.handle_frame(RFFrame.from_hex("5000"))) # HALT
|
||||
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("52"))) # WUPA
|
||||
assert resp is not None
|
||||
assert tag.state == "READY"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tag14443A_3 — Part 3 only (rejects RATS)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTag14443A_3:
|
||||
"""Tag14443A_3 rejects RATS (no ISO-DEP)."""
|
||||
|
||||
def test_rejects_rats(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04", sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
|
||||
# RATS (0xE0, CID=0)
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("E050")))
|
||||
assert resp is None # Part 3 only, no RATS
|
||||
|
||||
def test_sak_bit5_is_zero(self):
|
||||
tag = Tag14443A_3(uid=b"\x01\x02\x03\x04", sak=0x08)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
# SAK bit 5 = 0 (no ISO-DEP)
|
||||
assert resp.data[0] & 0x20 == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tag14443A_4 — Part 3 + Part 4 (ISO-DEP capable)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTag14443A_4:
|
||||
"""Tag14443A_4 supports RATS and I-block exchange."""
|
||||
|
||||
def test_sak_bit5_is_set(self):
|
||||
tag = Tag14443A_4(uid=b"\x01\x02\x03\x04", sak=0x20, ats=b"\x05\x78\x80\x70\x02")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
assert resp.data[0] & 0x20 != 0
|
||||
|
||||
def test_rats_returns_ats(self):
|
||||
ats = b"\x05\x78\x80\x70\x02"
|
||||
tag = Tag14443A_4(uid=b"\x01\x02\x03\x04", sak=0x20, ats=ats)
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
|
||||
# RATS: 0xE0, FSD/CID byte
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("E050")))
|
||||
assert resp is not None
|
||||
assert resp.data == ats
|
||||
assert tag.state == "PROTOCOL"
|
||||
|
||||
def test_iblock_exchange(self):
|
||||
"""After RATS, I-blocks carry APDUs."""
|
||||
tag = Tag14443A_4(uid=b"\x01\x02\x03\x04", sak=0x20,
|
||||
ats=b"\x05\x78\x80\x70\x02")
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc]))))
|
||||
run(tag.handle_frame(RFFrame.from_hex("E050"))) # RATS → PROTOCOL
|
||||
|
||||
# I-block: PCB=0x02 (block 0), payload = SELECT APDU
|
||||
apdu = b"\x00\xA4\x04\x00\x07\xD2\x76\x00\x00\x85\x01\x01"
|
||||
iblock = b"\x02" + apdu
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(iblock)))
|
||||
# Default handler should return something (at minimum an I-block response)
|
||||
assert resp is not None
|
||||
# Response PCB should be I-block with toggled block number
|
||||
assert resp.data[0] & 0xC0 == 0x00 # I-block: bits 7-6 = 00
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reader14443A — anticollision tree walk
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReader14443AInventory:
|
||||
"""Test reader anticollision with single and multiple tags."""
|
||||
|
||||
def test_inventory_single_4byte_tag(self):
|
||||
medium = SoftwareMedium()
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
run(medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
uids = run(reader.inventory())
|
||||
assert len(uids) == 1
|
||||
assert uids[0] == uid
|
||||
|
||||
def test_inventory_single_7byte_tag(self):
|
||||
medium = SoftwareMedium()
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
run(medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
uids = run(reader.inventory())
|
||||
assert len(uids) == 1
|
||||
assert uids[0] == uid
|
||||
|
||||
def test_inventory_two_tags_different_uids(self):
|
||||
medium = SoftwareMedium()
|
||||
uid1 = b"\x01\x02\x03\x04"
|
||||
uid2 = b"\x05\x06\x07\x08"
|
||||
run(medium.attach(Tag14443A_3(uid=uid1, sak=0x08)))
|
||||
run(medium.attach(Tag14443A_3(uid=uid2, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
uids = run(reader.inventory())
|
||||
assert len(uids) == 2
|
||||
assert set(uids) == {uid1, uid2}
|
||||
|
||||
def test_inventory_four_tags(self):
|
||||
medium = SoftwareMedium()
|
||||
tag_uids = [bytes([i, i+1, i+2, i+3]) for i in range(0, 16, 4)]
|
||||
for uid in tag_uids:
|
||||
run(medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
uids = run(reader.inventory())
|
||||
assert len(uids) == 4
|
||||
assert set(uids) == set(tag_uids)
|
||||
|
||||
|
||||
class TestReader14443ASelect:
|
||||
"""Test reader SELECT sequence."""
|
||||
|
||||
def test_select_known_4byte_uid(self):
|
||||
medium = SoftwareMedium()
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
run(medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
result = run(reader.select_tag(uid))
|
||||
assert result["uid"] == uid
|
||||
assert result["sak"] == 0x08
|
||||
|
||||
def test_select_known_7byte_uid(self):
|
||||
medium = SoftwareMedium()
|
||||
uid = b"\x01\x02\x03\x04\x05\x06\x07"
|
||||
run(medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
result = run(reader.select_tag(uid))
|
||||
assert result["uid"] == uid
|
||||
assert result["sak"] == 0x08
|
||||
|
||||
|
||||
class TestReader14443ARATS:
|
||||
"""Test reader RATS for Layer 4."""
|
||||
|
||||
def test_rats_returns_ats(self):
|
||||
medium = SoftwareMedium()
|
||||
ats = b"\x05\x78\x80\x70\x02"
|
||||
uid = b"\x01\x02\x03\x04"
|
||||
run(medium.attach(Tag14443A_4(uid=uid, sak=0x20, ats=ats)))
|
||||
reader = Reader14443A(medium)
|
||||
|
||||
run(reader.select_tag(uid))
|
||||
result = run(reader.rats())
|
||||
assert result["ats"] == ats
|
||||
Reference in New Issue
Block a user