feat(rawcli): identify NTAG5 and ICODE DNA (READ_CONFIG probe)

NTAG5, ICODE DNA and SLIX2 all share E0 04 and answer READ_SIGNATURE, so name_iso15
now splits them with an addressed, retried READ_CONFIG (0xC0) probe:
  - session register 0xA0 (STATUS_REG) succeeds only on NTAG5      -> "NTAG 5"
  - 0xA0 errors but config block 0x00 succeeds (DNA has config mem) -> "ICODE DNA"
  - neither (SLIX2 has no READ_CONFIG) -> the 0xBD signature step   -> "ICODE SLIX2"
  - plain SLIX answers neither                                      -> "ICODE SLIX"

Hardware-verified against a real NTAG5 (VivoKey VK Thermo, UID E0 04 01 58, 0xA0 ->
00 01400000) and a real ICODE DNA (UID E0 04 01 18, 0xA0 -> 01 0F error, 0x00 -> 00 ...).
DNA was previously mislabeled "ICODE SLIX2". catalog_for routes DNA to the SLIX2 catalog
for now (a dedicated DNA catalog with READ_CONFIG + AES is a follow-up). Adds a mocked
unit test covering all four branches from the real captures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-16 13:33:41 -07:00
parent 9e18a10274
commit 4684d0cc83
4 changed files with 66 additions and 10 deletions

View File

@@ -268,6 +268,34 @@ class TestIdentify:
r = identify(s)
assert r["found"] is False and s.transponder is None
def test_name_iso15_ntag5_dna_slix2_slix(self):
# split the E0 04 family by READ_CONFIG (0xC0) probe, using the real hardware captures:
# NTAG5 -> session reg 0xA0 succeeds; DNA -> 0xA0 errors but config 0x00 succeeds;
# SLIX2 -> no READ_CONFIG (timeout) but READ_SIGNATURE answers; SLIX -> neither.
from pm3py.cli.rawcli.identify import name_iso15
from unittest.mock import MagicMock
def dev_with(responses):
dev = MagicMock()
def _raw(frame):
f = bytes(frame)
if f[:2] == b"\x22\xc0": # READ_CONFIG addressed: key by block
return responses.get(("cfg", f[-2]), {"raw": None})
if f[:3] == b"\x02\xbd\x04": # READ_SIGNATURE
return responses.get("sig", {"raw": None})
return {"raw": None}
dev.hf.iso15.raw.side_effect = _raw
return dev
ntag5 = dev_with({("cfg", 0xA0): {"raw": bytes.fromhex("0001400000bad5")}})
assert name_iso15(ntag5, "e0040158108ee802") == "NTAG 5"
dna = dev_with({("cfg", 0xA0): {"raw": bytes.fromhex("010f68ee")},
("cfg", 0x00): {"raw": bytes.fromhex("001e4d3235a1b8")}})
assert name_iso15(dna, "e0040118009b4ccb") == "ICODE DNA"
slix2 = dev_with({"sig": {"raw": bytes([0x00]) + bytes(34)}}) # 35-byte signature reply
assert name_iso15(slix2, "e0040203040506") == "ICODE SLIX2"
assert name_iso15(dev_with({}), "e004a2110b37c06b") == "ICODE SLIX"
def test_no_flat_lf(self):
# nothing on HF and a flat LF envelope must report not-found, not a bogus "LF tag"
assert identify(RawSession(device=_mock_device()))["found"] is False