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>
129 lines
4.4 KiB
Python
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")
|