pair/revoke endpoint must call revoke_key_user, not revoke_key_token (revoke-one-spire is a no-op otherwise) #22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The spire revoke path (the "Revoke spire/ATM access" acceptance criterion in #9/#12, surfaced while building #21) must use the bunker admin client's
revoke_key_user(key_name), NOTrevoke_token/revoke_key_token. Using token revocation will appear to succeed but will not actually stop the spire from signing — a silent security bug.Why (verified live against
nsecbunkerd@dev, 2026-06-18)On NIP-46
connect, the bunker redeems the connect token and materializes the token's policy into standing per-userSigningConditiongrants on theKeyUser. The sign-time ACL (nsecbunkerd src/daemon/lib/acl/index.ts) checks those grants before theToken.revokedAt IS NULLfilter:KeyUser.revokedAtset → deny ("binary user revoke beats everything")SigningCondition→ return itsallowed← grants live here after connectToken.revokedAt IS NULL→ allowlnbits eager-binds the spire key at provision time (the
aiolabs/lnbits#32fix redeems the token immediately), so the token is always already redeemed. Revoking the token (step 4) is bypassed by the materialized grants (step 3) → the spire keeps signing. Onlyrevoke_user(step 2,KeyUser.revokedAt) actually cuts it off.Daemon log from the failing case (revoked the token, then signed):
After
revoke_userinstead:sign_event→Not authorized. ✅The fix on the lnbits side is already shipped
aiolabs/lnbits#55(commit05f43894) adds toNsecBunkerAdminClient:get_key_users(key_name)— discovery path returningKeyUser.id(s)revoke_key_user(key_name)— resolveskey_name→ KeyUser id(s) andrevoke_users each; returns the count revoked. This is the effective "cut off this spire" op.The
revoke_token/revoke_key_tokendocstrings now carry the redeemed-token caveat.Action for spirekeeper
pair_spiresibling) should calladmin_client.revoke_key_user(<spire bunker_name / key_name>)and treat a return value>= 1as "spire access revoked",0as "nothing was bound" (token minted but spire never connected).test_live_bunker_revoked_user_cannot_sign: provision/pair → sign once →revoke_key_user→ assert a subsequent sign fails closed.aiolabs/lnbits#55merging (therevoke_key_usermethod).Related
aiolabs/lnbits#54/#55(admin-client revoke/expiry/policy-cache)aiolabs/spirekeeper#9,#12(revoke-one-spire UX),#21(seed-URL pairing)revoke_token.tsdocstring wrongly claims it stops signing "for the associated KeyUser" — worth correcting.expiresAt(TTL) is not enforced post-bind — sign-time ACL ignores it #24Resolved bunker-side by nsecbunkerd#27 (deployed 2026-06-19)
This issue's core finding — token-revoke is a silent no-op once the token is redeemed, because nsecbunkerd photocopied policy grants into per-
KeyUserSigningConditionrows that the ACL checked before theToken.revokedAtfilter — no longer holds.nsecbunkerd#27 (merge
992c6a8, Option D; closes nsecbunkerd#24/#25/#12):applyTokenstopped photocopying grants into SigningConditions;checkIfPubkeyAllowedstep 4 now joinsTokenthroughliveWhere(now)={ revokedAt: null, OR: [expiresAt null, expiresAt > now] }, evaluated every request.So token-revoke (and token expiry) are now enforced live post-bind. Verified against the deployed
devtree.Our code stays as-is:
revoke_spirekeeps callingrevoke_key_user(setsKeyUser.revokedAt, the step-2 subject-level ban) because that cuts the whole binding regardless of how many tokens were issued — the correct "revoke this spire" semantics — whereas token-revoke severs only one token's grant. Docstring/test-comment corrected in #28.Closing as resolved — the mechanism this issue guarded against is fixed upstream and our deliberate
revoke_key_userchoice is documented. (Will leave the actual close to @padreug per our manual-close convention.)