Files
pm3py/tests/test_sim_advanced.py
michael 668170457e 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>
2026-03-18 20:04:55 -07:00

190 lines
6.9 KiB
Python

"""Tests for advanced applications: fuzzer, relay, replay."""
import asyncio
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.iso14443a import Tag14443A_3, Reader14443A
from pm3py.sim.fuzzer import MutationFuzzer, GrammarFuzzer
from pm3py.sim.relay import RelayTransponder, MitMProxy
from pm3py.sim.replay import TraceRecorder, TraceReplayer, TraceEntry
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# MutationFuzzer
# ---------------------------------------------------------------------------
class TestMutationFuzzer:
def test_mutate_produces_different_frame(self):
seed = RFFrame.from_hex("26")
fuzzer = MutationFuzzer([seed], seed=42)
mutated = fuzzer.mutate(seed)
# At least sometimes should differ (with enough attempts)
found_different = False
for _ in range(20):
m = fuzzer.mutate(seed)
if m.data != seed.data or m.bit_count != seed.bit_count:
found_different = True
break
assert found_different
def test_generate_yields_frames(self):
seeds = [RFFrame.from_hex("26"), RFFrame.from_hex("9320")]
fuzzer = MutationFuzzer(seeds, seed=42)
frames = list(fuzzer.generate(count=10))
assert len(frames) == 10
assert all(isinstance(f, RFFrame) for f in frames)
def test_deterministic_with_seed(self):
seeds = [RFFrame.from_hex("A5B6")]
f1 = list(MutationFuzzer(seeds, seed=123).generate(5))
f2 = list(MutationFuzzer(seeds, seed=123).generate(5))
assert [f.data for f in f1] == [f.data for f in f2]
def test_strategies_include_flip_truncate_extend(self):
fuzzer = MutationFuzzer([RFFrame.from_hex("DEADBEEF")], seed=0)
strategies_seen = set()
for _ in range(100):
frame = RFFrame.from_hex("DEADBEEF")
m = fuzzer.mutate(frame)
if m.bit_count < 32:
strategies_seen.add("truncate")
elif m.bit_count > 32:
strategies_seen.add("extend")
elif m.data != b"\xDE\xAD\xBE\xEF":
strategies_seen.add("flip")
assert len(strategies_seen) >= 2 # at least 2 strategies observed
# ---------------------------------------------------------------------------
# GrammarFuzzer
# ---------------------------------------------------------------------------
class TestGrammarFuzzer:
def test_generate_14443a_reqa(self):
fuzzer = GrammarFuzzer("14443a")
frame = fuzzer.generate("REQA")
assert frame is not None
assert frame.bit_count > 0
def test_generate_with_overrides(self):
fuzzer = GrammarFuzzer("14443a")
frame = fuzzer.generate("SELECT", uid=b"\xFF\xFF\xFF\xFF")
assert frame is not None
def test_unknown_command_returns_none(self):
fuzzer = GrammarFuzzer("14443a")
frame = fuzzer.generate("NONEXISTENT")
assert frame is None
# ---------------------------------------------------------------------------
# RelayTransponder
# ---------------------------------------------------------------------------
class TestRelayTransponder:
def test_relay_forwards_frames(self):
"""RelayTransponder forwards reader commands to upstream reader."""
# Set up "real card" on upstream medium
upstream_medium = SoftwareMedium()
uid = b"\x01\x02\x03\x04"
run(upstream_medium.attach(Tag14443A_3(uid=uid, sak=0x08)))
upstream_reader = Reader14443A(upstream_medium)
# Set up relay on local medium
relay = RelayTransponder(upstream_reader)
local_medium = SoftwareMedium()
run(local_medium.attach(relay))
# Send REQA through local medium
run(local_medium.transmit_reader(RFFrame.from_hex("26")))
resp = run(local_medium.receive_reader())
# Should get ATQA back from the real card
assert resp is not None
assert len(resp.data) == 2 # ATQA
# ---------------------------------------------------------------------------
# MitMProxy
# ---------------------------------------------------------------------------
class TestMitMProxy:
def test_proxy_logs_traffic(self):
reader_medium = SoftwareMedium()
tag_medium = SoftwareMedium()
run(tag_medium.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
proxy = MitMProxy(reader_medium, tag_medium)
# Simulate reader → proxy → tag
run(proxy.forward_to_tag(RFFrame.from_hex("26")))
assert len(proxy.trace) == 2 # request + response
def test_proxy_with_intercept(self):
reader_medium = SoftwareMedium()
tag_medium = SoftwareMedium()
run(tag_medium.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
modified = []
def intercept(frame, direction):
modified.append(direction)
return frame # pass through
proxy = MitMProxy(reader_medium, tag_medium, intercept=intercept)
run(proxy.forward_to_tag(RFFrame.from_hex("26")))
assert "reader" in modified
assert "tag" in modified
# ---------------------------------------------------------------------------
# TraceRecorder / TraceReplayer
# ---------------------------------------------------------------------------
class TestTraceRecorder:
def test_records_traffic(self):
medium = SoftwareMedium()
run(medium.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
recorder = TraceRecorder(medium)
# Exchange through recorder
run(recorder.transmit_reader(RFFrame.from_hex("26")))
run(recorder.receive_reader())
assert len(recorder.trace) >= 1
def test_trace_entries_have_direction(self):
medium = SoftwareMedium()
run(medium.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
recorder = TraceRecorder(medium)
run(recorder.transmit_reader(RFFrame.from_hex("26")))
resp = run(recorder.receive_reader())
directions = [e.direction for e in recorder.trace]
assert "reader" in directions
if resp is not None:
assert "tag" in directions
class TestTraceReplayer:
def test_replay_against_tag(self):
"""Record a trace, then replay it against a fresh tag."""
# Record
medium1 = SoftwareMedium()
run(medium1.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
recorder = TraceRecorder(medium1)
run(recorder.transmit_reader(RFFrame.from_hex("26")))
run(recorder.receive_reader())
# Replay against fresh tag
medium2 = SoftwareMedium()
run(medium2.attach(Tag14443A_3(uid=b"\x01\x02\x03\x04")))
replayer = TraceReplayer(recorder.trace, medium2)
results = run(replayer.replay())
assert len(results) > 0
assert all(r["matched"] for r in results)