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>
88 lines
3.1 KiB
Python
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
|