boltcard-writer/tests/test_flows.py
Padreug f37026793b 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>
2026-09-20 21:41:24 +02:00

88 lines
3.1 KiB
Python

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