ACL: an expired/exhausted bound token should hard-reject (false), not fall through to prompt-admin (undefined) — clients time out instead of re-pairing #36
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
Surfaced during the bitspire bunker-pairing smoke on the Sintra (2026-06-21; legs 1/2 ✅, leg 7 revoke ✅, leg 8 TTL). When a token's lifecycle lapses post-bind — expiry (#24), per-token revoke, or usage exhaustion (#28) —
checkIfPubkeyAllowedfalls through to step 5 →undefined("no auto-decision, ask an admin"), notfalse("reject").For an unattended NIP-46 client (a spire/ATM, no admin watching to approve),
undefinedroutes to therequestAuthorizationpath → nobody approves → the request hangs → the client times out. That's the wrong client-facing outcome for what is a permanent condition.The two ACL exits diverge — they shouldn't, for a lapsed binding
revoke_key_user(subject ban)KeyUser.revokedAtfalseBunkerRejectedError→ "Pairing Required" ✅undefinedBunkerTimeoutError→ "Signer Unreachable" ❌undefinedrevokedAt(not the KeyUser)undefinedSo
revoke_key_user(subject-level) rejects cleanly, but every grant-level lapse (the token's own expiry/revoke/usage) surfaces as a transient-looking timeout.Why the fix belongs bunker-side (bitspire's argument, smoke thread 2026-06-21)
A
BunkerTimeoutError("Signer Unreachable", transient) makes a customer/operator wait forever; aBunkerRejectedError("Pairing Required", permanent) prompts a re-pair — the only sane outcome for a spent/lapsed binding.Proposed change
At step 5, before returning
undefined, distinguish:false(the binding lapsed — reject so the client re-pairs), vsundefined(the client may be asking for a new permission an admin could grant).Concretely: if step 4 found no LIVE token, query for a non-live token (past
expiresAt/revokedAtset / usage-exhausted) bound to thisKeyUserwhose policy has a matchingPolicyRulefor the request. If one exists →false. Else →undefined.This covers expiry, per-token revoke, and usage exhaustion (#28) uniformly — all three are "the grant was here and is now spent/lapsed."
Keep
undefinedfor the genuine ask-admin caseThe prompt-admin path (
undefined→requestAuthorization) is correct for "a known/paired client requests a permission its policy never granted" (e.g. a new kind an admin might allow). That must stay. This change only converts lapsed-grant misses tofalse; it does not touch never-granted misses.Tests
The #29/#33 integration harness is the right home: add cases asserting
checkIfPubkeyAllowedreturnsfalse(notundefined) when the only matching token is expired / token-revoked / usage-exhausted, while a method with no matching rule at all still returnsundefined.Smoke evidence
connect→signon boot 1;signwith noconnecton boot 2) — #27 working.false→BunkerRejectedError(correct).Cross-refs
#27 (the live-lifecycle ACL this refines), #24 (TTL), #28 (usage caps — same exit, PR #34), #29/#33 (test harness);
aiolabs/bitspire#52/#61 (theBunkerRejectedErrorvsBunkerTimeoutErrorconsumer mapping); coordination threadsmoke-bunker-pairing-sintra.md(2026-06-21).Fixed via PR #38 (merged to
dev@3468972). Verified on hardware during the Sintra smoke: an expired/revoked token binding now hard-rejects (BunkerRejectedError→ "Pairing Required") instead of hanging to aBunkerTimeoutError. Closing.