test: AN12196 vectors and a software NTAG 424 simulator
The vector tests replay the application note's worked examples (auth key 0/3, IV, ChangeKey 0/2, WriteData) byte-for-byte. The simulator implements the card side of secure messaging so the provision and wipe flows run end to end without hardware. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7d35d3e2c7
commit
f37026793b
5 changed files with 547 additions and 0 deletions
88
tests/test_flows.py
Normal file
88
tests/test_flows.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import pytest
|
||||
|
||||
from boltcard_writer import boltcard as bc
|
||||
from boltcard_writer.ntag424 import Ntag424, Ntag424Error
|
||||
from tests.simcard import SimCard
|
||||
|
||||
LNURLW = "lnurlw://lnbits.example.com/boltcards/api/v1/scan/9f2c1d0e8b7a6c5d4e3f2a1b0c9d8e7f"
|
||||
KEYS = bc.CardKeys.from_json(
|
||||
{
|
||||
"card_name": "sim",
|
||||
"k0": "a0" * 16,
|
||||
"k1": "a1" * 16,
|
||||
"k2": "a2" * 16,
|
||||
"k3": "a1" * 16,
|
||||
"k4": "a2" * 16,
|
||||
"lnurlw_base": LNURLW,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_inspect_blank_card():
|
||||
card = SimCard()
|
||||
info = bc.inspect(card)
|
||||
assert info.uid == card.uid
|
||||
assert info.key_versions == [0] * 5
|
||||
assert not info.ndef_settings.sdm_enabled
|
||||
assert info.url is None
|
||||
assert not info.looks_provisioned
|
||||
|
||||
|
||||
def test_provision_then_inspect_then_wipe():
|
||||
card = SimCard()
|
||||
steps = []
|
||||
uid, counter = bc.provision(card, KEYS, progress=steps.append)
|
||||
assert uid == card.uid
|
||||
assert counter >= 1 # each ReadBinary bumps SDMReadCtr; the read-back takes two
|
||||
assert card.keys == [KEYS.k0, KEYS.k1, KEYS.k2, KEYS.k3, KEYS.k4]
|
||||
assert card.key_versions == [1] * 5
|
||||
assert card.file_option == 0x40 and card.access_rights == bytes.fromhex("00E0")
|
||||
assert card.sdm_options == 0xC1 and card.sdm_access == bytes.fromhex("FF12")
|
||||
url = bc.bolt_url(LNURLW)
|
||||
assert (card.picc_off, card.mac_off) == bc.sdm_offsets(url)
|
||||
assert card.mac_in_off == card.mac_off
|
||||
|
||||
# what a POS would read: a fresh p/c that verifies with k1/k2 and a bumped counter
|
||||
info = bc.inspect(card)
|
||||
assert info.looks_provisioned
|
||||
assert info.url and info.url.startswith(LNURLW + "?p=")
|
||||
uid2, counter2 = bc.verify_sun_url(info.url, KEYS.k1, KEYS.k2)
|
||||
assert uid2 == card.uid and counter2 > counter
|
||||
|
||||
# plain writes are locked now
|
||||
with pytest.raises(Ntag424Error):
|
||||
Ntag424(card).write_ndef_file(b"")
|
||||
|
||||
bc.wipe(card, KEYS, progress=steps.append)
|
||||
assert card.keys == [bytes(16)] * 5
|
||||
assert card.key_versions == [0] * 5
|
||||
assert card.file_option == 0x00 and card.access_rights == bytes.fromhex("E0EE")
|
||||
info = bc.inspect(card)
|
||||
assert not info.looks_provisioned and info.url is None
|
||||
|
||||
|
||||
def test_provision_command_sequence():
|
||||
card = SimCard()
|
||||
bc.provision(card, KEYS, verify=False)
|
||||
ins = [bytes.fromhex(a)[1] for a in card.log if a.startswith("90")]
|
||||
# auth (71, AF), file settings (5F), keys 1..4 then 0 (C4 x5)
|
||||
assert ins[-7:] == [0x71, 0xAF, 0x5F, 0xC4, 0xC4, 0xC4, 0xC4, 0xC4][-7:]
|
||||
key_nos = [bytes.fromhex(a)[5] for a in card.log if a.startswith("90C4")]
|
||||
assert key_nos == [1, 2, 3, 4, 0]
|
||||
|
||||
|
||||
def test_wipe_with_wrong_keys_fails_cleanly():
|
||||
card = SimCard()
|
||||
bc.provision(card, KEYS, verify=False)
|
||||
wrong = bc.CardKeys(bytes(16), KEYS.k1, KEYS.k2, KEYS.k3, KEYS.k4)
|
||||
with pytest.raises(Ntag424Error, match="RndA|AUTHENTICATION"):
|
||||
bc.wipe(card, wrong)
|
||||
assert card.keys[0] == KEYS.k0 # untouched
|
||||
|
||||
|
||||
def test_get_card_uid_encrypted_response():
|
||||
card = SimCard()
|
||||
tag = Ntag424(card)
|
||||
tag.select_application()
|
||||
tag.authenticate_ev2_first(0, bytes(16))
|
||||
assert tag.get_card_uid() == card.uid
|
||||
Loading…
Add table
Add a link
Reference in a new issue