Compare commits

..

No commits in common. "47b7efc53c7e31035387dce0a5aa2e6a53ec0af9" and "cc8d786331c5ec66e0efd5f19355ddf853e0f45d" have entirely different histories.

2 changed files with 20 additions and 21 deletions

View file

@ -166,13 +166,14 @@ async def pair_spire(
return the seed URL the spire redeems at first boot. return the seed URL the spire redeems at first boot.
`duration_hours` (optional, aiolabs/lnbits#54 item 2) stamps `expiresAt` `duration_hours` (optional, aiolabs/lnbits#54 item 2) stamps `expiresAt`
on the spire's connect token, bounding the established binding's lifetime. on the spire's connect token. NOTE: this bounds ONLY the window in which
Since aiolabs/nsecbunkerd#27 (deployed 2026-06-19) the sign-time ACL an *un-redeemed* token can first connect nsecbunkerd reads `expiresAt`
evaluates token lifecycle on EVERY request (`checkIfPubkeyAllowed` step 4 solely in `validateToken` at redeem time. Once the spire has connected
joins through a `liveWhere` filter; `applyToken` no longer photocopies and its per-KeyUser grants are materialized, an expired token keeps
grants), so an expired token stops signing post-bind, not just at connect. signing (the sign-time ACL never checks `expiresAt`; same ACL-ordering
The spire must re-pair to keep signing once the token lapses. None = subtlety as the revoke finding, #22). The real post-bind cutoff is
non-expiring (the only invalidation path is then `revoke_spire`). `revoke_spire` (`revoke_key_user`), not TTL. Post-bind TTL enforcement is
tracked at aiolabs/nsecbunkerd#24. None = non-expiring connect window.
`admin_client` must already be connected (the caller owns the `admin_client` must already be connected (the caller owns the
`async with NsecBunkerAdminClient.from_settings()` context) keeps `async with NsecBunkerAdminClient.from_settings()` context) keeps
@ -254,16 +255,16 @@ async def revoke_spire(
machine: Machine, *, admin_client: NsecBunkerAdminClient machine: Machine, *, admin_client: NsecBunkerAdminClient
) -> int: ) -> int:
"""Revoke a spire's bunker access (the "Revoke spire access" UX, """Revoke a spire's bunker access (the "Revoke spire access" UX,
aiolabs/spirekeeper#9/#12). aiolabs/spirekeeper#9/#12; security model per #22).
Calls `revoke_key_user` (sets `KeyUser.revokedAt`) the subject-level Calls `revoke_key_user` NOT `revoke_token` / `revoke_key_token`.
sticky ban that's checked at step 2 of `checkIfPubkeyAllowed`, beating lnbits eager-binds (redeems) the connect token at provision time
every grant. This cuts the WHOLE binding regardless of how many tokens (aiolabs/lnbits#32), so nsecbunkerd has already materialised the
were issued to the spire, which is the right semantics for "revoke this token's policy into standing per-`KeyUser` `SigningCondition` grants;
spire." (Since aiolabs/nsecbunkerd#27 token-revoke also works post-bind — its sign-time ACL checks those *before* the `Token.revokedAt` filter,
the sign-time ACL now evaluates `Token.revokedAt`/`expiresAt` live every so revoking the token is a silent no-op (the spire keeps signing).
request, closing the #22 no-op — but per-token revoke only cuts one Only `KeyUser.revokedAt` set by `revoke_user` / `revoke_key_user`
token's grant, so `revoke_key_user` remains the correct full-deauth call.) actually cuts off signing (verified live 2026-06-18, #22).
Returns the number of KeyUsers revoked: >= 1 means the spire's signing Returns the number of KeyUsers revoked: >= 1 means the spire's signing
access is now cut; 0 means nothing was bound (token minted but the access is now cut; 0 means nothing was bound (token minted but the

View file

@ -287,10 +287,8 @@ def test_pair_default_duration_is_none():
def test_revoke_spire_calls_revoke_key_user(): def test_revoke_spire_calls_revoke_key_user():
# revoke goes through revoke_key_user (KeyUser.revokedAt) — the subject- # revoke MUST go through revoke_key_user (KeyUser.revokedAt), not token
# level ban that cuts the whole binding, not just one token's grant. # revoke — token revoke is a no-op once redeemed (spirekeeper#22).
# (Token-revoke also works post-bind since nsecbunkerd#27, but only
# severs a single token; revoke_key_user is the full-deauth call.)
bunker = FakeBunker(revoke_count=2) bunker = FakeBunker(revoke_count=2)
count = asyncio.run(revoke_spire(_machine(), admin_client=bunker)) count = asyncio.run(revoke_spire(_machine(), admin_client=bunker))
assert count == 2 assert count == 2