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:
181
tests/test_sim_desfire.py
Normal file
181
tests/test_sim_desfire.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Tests for pm3py.sim.desfire — DESFire EV1/EV2 transponder and reader."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.sim.medium import SoftwareMedium
|
||||
from pm3py.sim.iso14443a import _compute_bcc
|
||||
from pm3py.sim.desfire import DesfireTag, DesfireReader
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.get_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DesfireTag — basics
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDesfireTagBasics:
|
||||
def test_sak_indicates_isodep(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
run(tag.power_on())
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
assert resp.data == b"\x44\x03" # DESFire ATQA
|
||||
|
||||
# SELECT (7-byte UID)
|
||||
ct_uid = b"\x88" + tag._uid[0:3]
|
||||
bcc1 = _compute_bcc(ct_uid)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1]))))
|
||||
cl2_uid = tag._uid[3:7]
|
||||
bcc2 = _compute_bcc(cl2_uid)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2]))))
|
||||
assert resp.data[0] & 0x20 != 0 # SAK bit 5 = ISO-DEP
|
||||
|
||||
def test_rats_returns_ats(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
run(tag.power_on())
|
||||
self._select(tag)
|
||||
resp = run(tag.handle_frame(RFFrame.from_hex("E050")))
|
||||
assert resp is not None
|
||||
assert tag.state == "PROTOCOL"
|
||||
|
||||
def _select(self, tag):
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
ct_uid = b"\x88" + tag._uid[0:3]
|
||||
bcc1 = _compute_bcc(ct_uid)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1]))))
|
||||
cl2_uid = tag._uid[3:7]
|
||||
bcc2 = _compute_bcc(cl2_uid)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2]))))
|
||||
|
||||
|
||||
class TestDesfireTagApplications:
|
||||
"""Test DESFire application/file structure."""
|
||||
|
||||
def test_get_version(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
resp = self._send_desfire(tag, b"\x60") # GetVersion
|
||||
assert resp is not None
|
||||
assert resp[0] == 0xAF # AF = additional frames follow
|
||||
|
||||
def test_get_application_ids_default(self):
|
||||
"""Default tag has only PICC application (0x000000)."""
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
resp = self._send_desfire(tag, b"\x6A") # GetApplicationIDs
|
||||
assert resp is not None
|
||||
assert resp[0] == 0x00 # OK
|
||||
|
||||
def test_create_application(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
# CreateApplication: AID=0x010203, key_settings=0x0F, num_keys=1
|
||||
resp = self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01")
|
||||
assert resp is not None
|
||||
assert resp[0] == 0x00 # OK
|
||||
|
||||
def test_select_application(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
# Create app
|
||||
self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01")
|
||||
# Select app
|
||||
resp = self._send_desfire(tag, b"\x5A\x01\x02\x03")
|
||||
assert resp is not None
|
||||
assert resp[0] == 0x00
|
||||
|
||||
def test_create_and_read_standard_file(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01") # CreateApp
|
||||
self._send_desfire(tag, b"\x5A\x01\x02\x03") # SelectApp
|
||||
|
||||
# CreateStdDataFile: file_no=0, comm_settings=0, access_rights=0xEEEE, size=16
|
||||
resp = self._send_desfire(tag, b"\xCD\x00\x00\xEE\xEE\x10\x00\x00")
|
||||
assert resp[0] == 0x00
|
||||
|
||||
# WriteData: file_no=0, offset=0, length=4, data
|
||||
resp = self._send_desfire(tag, b"\x3D\x00\x00\x00\x00\x04\x00\x00\xDE\xAD\xBE\xEF")
|
||||
assert resp[0] == 0x00
|
||||
|
||||
# ReadData: file_no=0, offset=0, length=4
|
||||
resp = self._send_desfire(tag, b"\xBD\x00\x00\x00\x00\x04\x00\x00")
|
||||
assert resp[0] == 0x00
|
||||
assert resp[1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_delete_application(self):
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x01")
|
||||
resp = self._send_desfire(tag, b"\xDA\x01\x02\x03") # DeleteApplication
|
||||
assert resp[0] == 0x00
|
||||
|
||||
def test_authenticate_default_key(self):
|
||||
"""Authenticate with default all-zero AES key."""
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
self._activate(tag)
|
||||
|
||||
self._send_desfire(tag, b"\xCA\x01\x02\x03\x0F\x81") # CreateApp, AES keys
|
||||
self._send_desfire(tag, b"\x5A\x01\x02\x03")
|
||||
|
||||
# AuthenticateAES: key_no=0
|
||||
resp = self._send_desfire(tag, b"\xAA\x00")
|
||||
assert resp is not None
|
||||
# Response: status + encrypted challenge (16 bytes for AES)
|
||||
assert resp[0] == 0xAF # AF = additional frame expected
|
||||
|
||||
def _activate(self, tag):
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
ct_uid = b"\x88" + tag._uid[0:3]
|
||||
bcc1 = _compute_bcc(ct_uid)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1]))))
|
||||
cl2_uid = tag._uid[3:7]
|
||||
bcc2 = _compute_bcc(cl2_uid)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2]))))
|
||||
run(tag.handle_frame(RFFrame.from_hex("E050"))) # RATS
|
||||
|
||||
def _send_desfire(self, tag, cmd: bytes) -> bytes | None:
|
||||
"""Send DESFire command via I-block, return response (status + data)."""
|
||||
pcb = 0x02
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([pcb]) + cmd)))
|
||||
if resp is None:
|
||||
return None
|
||||
return resp.data[1:] # strip PCB
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DesfireReader
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDesfireReader:
|
||||
def test_get_version(self):
|
||||
medium = SoftwareMedium()
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
run(medium.attach(tag))
|
||||
reader = DesfireReader(medium)
|
||||
|
||||
result = run(reader.get_version(uid=b"\x04\x01\x02\x03\x04\x05\x06"))
|
||||
assert result is not None
|
||||
assert "hw_vendor" in result
|
||||
|
||||
def test_create_and_select_app(self):
|
||||
medium = SoftwareMedium()
|
||||
tag = DesfireTag(uid=b"\x04\x01\x02\x03\x04\x05\x06")
|
||||
run(medium.attach(tag))
|
||||
reader = DesfireReader(medium)
|
||||
uid = b"\x04\x01\x02\x03\x04\x05\x06"
|
||||
|
||||
result = run(reader.create_application(uid, aid=b"\x01\x02\x03", num_keys=1))
|
||||
assert result["success"]
|
||||
|
||||
result = run(reader.select_application(uid, aid=b"\x01\x02\x03"))
|
||||
assert result["success"]
|
||||
Reference in New Issue
Block a user