boltcard-writer/tests/test_boltcard.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

129 lines
4.4 KiB
Python

import json
import pytest
from boltcard_writer import boltcard as bc
LNURLW = "lnurlw://lnbits.example.com/boltcards/api/v1/scan/9f2c1d0e8b7a6c5d4e3f2a1b0c9d8e7f"
def test_bolt_url_and_offsets():
url = bc.bolt_url(LNURLW)
assert url.endswith("?p=00000000000000000000000000000000&c=0000000000000000")
picc, mac = bc.sdm_offsets(url)
# 7-byte NDEF header + position of the value after "p=" / "c="
assert picc == 7 + url.index("p=") + 2
assert mac == 7 + url.index("c=") + 2
assert mac == picc + 32 + len("&c=")
def test_bolt_url_with_existing_query():
assert bc.bolt_url("lnurlw://x/y?z=1").startswith("lnurlw://x/y?z=1&p=")
def test_ndef_roundtrip():
url = bc.bolt_url(LNURLW)
msg = bc.ndef_uri_message(url)
assert msg[:4] == bytes([0xD1, 0x01, len(url) + 1, 0x55])
assert msg[4] == 0x00
assert bc.parse_ndef_uri(msg) == url
# p= lands exactly at the advertised offset once the 2-byte length prefix is in front
image = len(msg).to_bytes(2, "big") + msg
picc, mac = bc.sdm_offsets(url)
assert image[picc : picc + 32] == b"0" * 32
assert image[mac : mac + 16] == b"0" * 16
def test_parse_ndef_uri_https_prefix():
msg = bytes([0xD1, 0x01, 0x0C, 0x55, 0x04]) + b"example.com"
assert bc.parse_ndef_uri(msg) == "https://example.com"
assert bc.parse_ndef_uri(b"") is None
assert bc.parse_ndef_uri(bytes([0xD1, 0x01, 0x02, 0x54, 0x02, 0x65])) is None # text record
def test_file_settings_bytes():
assert bc.bolt_file_settings(0x20, 0x43).hex().upper() == "4000E0C1FF12200000430000430000"
assert bc.factory_file_settings().hex().upper() == "00E0EE"
def test_sun_verification_matches_lnbits():
"""p/c produced with the LNbits extension's own nxp424.py (k1/k2 below,
UID 04A1B2C3D4E5F6, counter 42) must verify here."""
k1 = bytes.fromhex("0f0e0d0c0b0a09080706050403020100")
k2 = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
url = LNURLW + "?p=579FCC0440F8ACABB2EBD2F287C04DF1&c=E70AA58C59ECD814"
uid, counter = bc.verify_sun_url(url, k1, k2)
assert uid == bytes.fromhex("04A1B2C3D4E5F6")
assert counter == 42
with pytest.raises(bc.BoltcardError, match="CMAC"):
bc.verify_sun_url(url, k1, bytes(16))
with pytest.raises(bc.BoltcardError, match="C7"):
bc.verify_sun_url(url, bytes(16), k2)
def test_keys_from_lnbits_get_response():
obj = {
"card_name": "test",
"id": "1",
"k0": "11" * 16,
"k1": "22" * 16,
"k2": "33" * 16,
"k3": "22" * 16,
"k4": "33" * 16,
"lnurlw_base": LNURLW,
"protocol_name": "new_bolt_card_response",
"protocol_version": "1",
}
keys = bc.CardKeys.from_json(obj)
assert keys.k0 == b"\x11" * 16 and keys.k4 == b"\x33" * 16
assert keys.lnurlw_base == LNURLW
assert keys.card_name == "test"
assert not keys.all_default
def test_keys_from_lnbits_post_response_uppercase():
obj = {
"K0": "aa" * 16,
"K1": "bb" * 16,
"K2": "cc" * 16,
"K3": "bb" * 16,
"K4": "cc" * 16,
"LNURLW_BASE": "LNURLW://x",
"LNURLW": "LNURLW://x",
}
keys = bc.CardKeys.from_json(obj)
assert keys.k2 == b"\xcc" * 16 and keys.lnurlw_base == "LNURLW://x"
def test_keys_from_lnbits_wipe_json(tmp_path):
wipe = {
"action": "wipe",
"k0": "00" * 16,
"k1": "0a" * 16,
"k2": "0b" * 16,
"k3": "0a" * 16,
"k4": "0b" * 16,
"uid": "04A1B2C3D4E5F6",
"version": 1,
}
p = tmp_path / "wipe.json"
p.write_text(json.dumps(wipe))
keys = bc.load_keys(str(p))
assert keys.lnurlw_base is None
assert keys.raw["uid"] == "04A1B2C3D4E5F6"
assert bc.load_keys(json.dumps(wipe)).k1 == b"\x0a" * 16
def test_keys_validation():
with pytest.raises(bc.BoltcardError, match="missing k3"):
bc.CardKeys.from_json({"k0": "00" * 16, "k1": "00" * 16, "k2": "00" * 16})
with pytest.raises(bc.BoltcardError, match="32 hex"):
bc.CardKeys.from_json({f"k{i}": "zz" for i in range(5)})
def test_deeplink_unwrap():
link = "boltcard://program?url=https%3A%2F%2Flnbits.example.com%2Fboltcards%2Fapi%2Fv1%2Fauth%3Fa%3Dabc"
assert bc._unwrap_deeplink(link) == "https://lnbits.example.com/boltcards/api/v1/auth?a=abc"
assert bc._unwrap_deeplink("https://x") == "https://x"
with pytest.raises(bc.BoltcardError):
bc._unwrap_deeplink("boltcard://program")