Token expiresAt (TTL) is not enforced post-bind — sign-time ACL ignores it #24
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
A token's
expiresAt(set viacreate_new_tokendurationInHours, lnbits #54) is enforced only at connect/redeem time. Once a client has connected and its per-KeyUsergrants are materialized, an expired token keeps signing indefinitely — the sign-time ACL never looks atexpiresAt. This is the same materialized-grants ACL-ordering subtlety as the token-revoke finding (aiolabs/spirekeeper#22): callers reasonably expect a TTL to bound a session's lifetime, but it bounds only the first connect.Verified against
dev@cb8dd0c.Where
expiresAtis (and isn't) readThe only enforcement read is in
validateToken, called fromapplyTokenat redeem time:At sign time (
checkIfPubkeyAllowed,src/daemon/lib/acl/index.ts:23)expiresAtis never consulted:SigningConditionfirst (method='sign_event', kind=<n>, allowed=true, written byapplyToken) and returns.SigningConditionhas no expiry column at all (prisma/schema.prisma).revokedAt: nullonly — notexpiresAt(acl/index.ts:93-106).revokedAton expiry (the onlysetIntervals are interactive-authorization request cleanup, unrelated toToken).So: redeem an expiring token →
applyTokenmaterializes the grants → the token lapses →checkIfPubkeyAllowedstill returnstrue. The lapse is invisible to signing.Impact
duration_hourson the pairing endpoint with the documented intent "rejects the token once it lapses, forcing a re-pair." That's only true for an un-redeemed token; the docstring is being corrected to say so. Consumers (bitspire#52, model A1 spire) must treat revoke (revoke_key_user→KeyUser.revokedAt, step 2, beats everything) as the only post-bind cutoff — not TTL.Options
checkIfPubkeyAllowedstep 4 token join addsexpiresAt: { equals: null }OR{ gt: now }; and the step-3b materializedSigningConditionpath needs an expiry (either copyexpiresAtonto the conditionsapplyTokenwrites, or skip the override layer for policy-derived grants so expiry-bearing tokens are always re-checked at step 4). Note step 3b currently short-circuits step 4, so a step-4-only fix is insufficient.KeyUser.revokedAt(or deletes the materialized SigningConditions) when the binding's token has lapsed — leans on the existing step-2 revoke path that already works.Option 2 is the smallest change that gives real post-bind enforcement, reusing the revoke path that's already correct.
Cross-refs
Tokenfilter);revoke_key_useris the fix.Fixed by #27 (merge
992c6a8), deployed to all servers 2026-06-19.Sign-time ACL now enforces
Token.expiresAt(and revoke) live:checkIfPubkeyAllowedstep 4 filters the token join throughliveWhere(now), andBackend.applyTokenno longer materializes theSigningConditionphotocopy that used to short-circuit the check. Redeem and sign share onegrantIsLivepredicate, so they can't drift again.Closing as delivered + deployed.