feat(pairing): authorize kind-22242 (NIP-42 AUTH) in spire policy (#52)
Some checks failed
ci.yml / feat(pairing): authorize kind-22242 (NIP-42 AUTH) in spire policy (#52) (pull_request) Failing after 0s
ci.yml / feat(pairing): authorize kind-22242 (NIP-42 AUTH) in spire policy (#52) (push) Failing after 0s

bitspire#52 consumer review (2026-06-18) enumerated the kinds the spire
signs as its OWN identity and found NIP-42 relay AUTH (kind 22242) missing
from SPIRE_POLICY_RULES — a silent bunker reject the moment a relay
challenges with AUTH. It must be bunker-signed (AUTH proves control of
spire_pubkey, which only the bunker holds; can't use the local client_nsec).

Adds 22242. Records the confirmed set in the policy comment: live = 21000 +
30078 + 22242; CLINK 21001-21003 dormant but kept; nip04 unused (v1 path is
dead code). New test locks the required-kinds contract so 22242 can't
silently regress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-18 19:27:29 +02:00 committed by padreug
commit 22678dfb4f
2 changed files with 22 additions and 6 deletions

View file

@ -57,20 +57,25 @@ SEED_URL_SCHEME = "spire-seed:v1:"
# Policy granted to every spire's connect token. Scoped to exactly what a
# bitSpire signs as itself:
# - 21000 nostr-transport cash RPC envelope to lnbits
# - 21001-21003 CLINK Offer / Debit / Manage (payment flow)
# - 22242 NIP-42 relay AUTH — the spire authenticates to its relays
# (must be bunker-signed: AUTH proves control of spire_pubkey,
# which only the bunker holds; can't be done with client_nsec)
# - 21001-21003 CLINK Offer / Debit / Manage (dormant on dev; kept)
# - 30078 NIP-78 beacon + bitspire-cassettes-state hello-event
# Kind-scoped rules go in create_new_policy; kind-less methods (nip44, for
# encrypting cassette-state to the operator) are added via add_policy_rule
# because nsecbunkerd's create_new_policy chokes on null `kind`
# (rule.kind.toString()). Mirrors lnbits' DEFAULT_POLICY_* split.
# (rule.kind.toString()). Mirrors lnbits' DEFAULT_POLICY_* split. nip04 is
# deliberately absent — the v1/nip04 path is dead code (bitspire#52).
#
# NOTE (reconcile when bitspire#52 lands): confirm this kind set against the
# spire's actual createSignedEvent / finalizeEvent call sites. Over-granting
# here only widens what a spire may sign *as its own key* — low blast radius —
# but under-granting makes the bunker reject the spire's events.
# Kind set confirmed against the spire's signing sites in bitspire#52
# (2026-06-18): live = 21000 + 30078 + 22242; CLINK 21001-21003 dormant but
# kept; nip04 unused. Under-granting = silent bunker reject, so err toward
# inclusion (low blast radius — only widens what a spire signs as its OWN key).
SPIRE_POLICY_NAME = "spirekeeper-spire"
SPIRE_POLICY_RULES = [
{"method": "sign_event", "kind": 21000},
{"method": "sign_event", "kind": 22242}, # NIP-42 relay AUTH (bitspire#52)
{"method": "sign_event", "kind": 21001},
{"method": "sign_event", "kind": 21002},
{"method": "sign_event", "kind": 21003},

View file

@ -305,3 +305,14 @@ def test_revoke_spire_maps_bunker_error():
bunker.revoke_key_user = _boom
with pytest.raises(PairingError, match="revoke"):
asyncio.run(revoke_spire(_machine(), admin_client=bunker))
def test_policy_authorizes_required_signing_kinds():
# Kinds the spire signs as its OWN identity, confirmed against the
# consumer signing sites in bitspire#52 (2026-06-18). A missing kind is a
# silent bunker reject. 22242 = NIP-42 relay AUTH (must be bunker-signed —
# it proves control of spire_pubkey). nip04 stays out (v1 path is dead).
kinds = {r["kind"] for r in SPIRE_POLICY_RULES if r["method"] == "sign_event"}
assert {21000, 30078, 22242} <= kinds
assert "nip04_encrypt" not in SPIRE_POLICY_METHODS_NO_KIND
assert "nip04_decrypt" not in SPIRE_POLICY_METHODS_NO_KIND